Skip to content

android: audit 16 KB page alignment on every shipped .so#43

Merged
gmaclennan merged 4 commits into
mainfrom
claude/suspicious-noyce-5b8539
Apr 29, 2026
Merged

android: audit 16 KB page alignment on every shipped .so#43
gmaclennan merged 4 commits into
mainfrom
claude/suspicious-noyce-5b8539

Conversation

@gmaclennan

Copy link
Copy Markdown
Member

Closes #28.

Summary

  • New scripts/lib/check-16k-alignment.ts parses PT_LOAD program headers in pure JS (handles ELF32 + ELF64, validated against llvm-readelf on every architecture we ship) and verifies p_align >= 0x4000.
  • build-backend.ts runs the audit as its final Android step against both android/src/main/jniLibs/ (per-addon binaries) and android/libnode/bin/ (libnode). Any misaligned .so fails the build with the offending vaddr/align values.
  • Pure-JS parsing keeps the check working on macOS dev machines without readelf on \$PATH.

libnode allowlist

The 27 per-addon .so files all pass — the upstream -Wl,-z,max-page-size=16384 flag in nodejs-mobile-bare-prebuilds/prebuild/action.yml is doing its job. The audit caught one real, known issue: libnode.so v18.20.4 ships with p_align=0x1000 (4 KB) on all three ABIs.

A 16 KB-aligned build is being produced from the digidem/nodejs-mobile fork (currently in CI) pending the upstream release at nodejs-mobile/nodejs-mobile#155. The three libnode paths are temporarily listed in expectedMisaligned. The allowlist is self-correcting: if a listed file actually passes the audit, the audit fails — so the moment we point the downloader at the fork's tag and a 16 KB-aligned libnode lands in tree, the build will refuse the carve-out and force its removal.

Follow-up commit on this PR once the fork release is published: bump the download to the digidem fork tag and drop the allowlist entries.

Test plan

  • npx tsc --noEmit -p scripts/tsconfig.json clean
  • npx eslint scripts/ clean
  • npm run backend:build end-to-end: audit reports 27/30 .so file(s) verified and warns on the 3 allowlisted libnode files
  • Cross-checked parser output against llvm-readelf -l on both ELF32 (armeabi-v7a) and ELF64 (arm64-v8a, x86_64) shared objects
  • CI Android workflows pass (will run on PR)

🤖 Generated with Claude Code

gmaclennan and others added 4 commits April 29, 2026 13:13
Parse PT_LOAD program headers in pure JS and verify p_align >= 0x4000
on every .so under android/src/main/jniLibs and android/libnode/bin
as the final step of build-backend.ts. Catches regressions in the
upstream prebuild flag (-Wl,-z,max-page-size=16384) before they
produce an APK that Android 15+ 16 KB-page devices reject.

Pure JS parsing keeps the check working on macOS dev machines too —
readelf isn't on $PATH there. Cross-checked against llvm-readelf on
both ELF32 (armeabi-v7a) and ELF64 (arm64-v8a, x86_64) outputs.

The 27 per-addon binaries pass; the three libnode.so files
(v18.20.4, p_align=0x1000) are temporarily allowlisted pending a
16 KB-aligned build from the digidem/nodejs-mobile fork. The
allowlist self-clears: a passing entry fails the audit, so the
carve-out forces its own removal once the fork release lands and
NODEJS_MOBILE_VERSION is bumped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream nodejs-mobile/nodejs-mobile v18.20.4 release ships
libnode.so with `p_align=0x1000`, which Android 15+ rejects on
16 KB-page devices. digidem/nodejs-mobile is a fork that adds
`-Wl,-z,max-page-size=16384` to the same v18.20.4 sources
(tracking nodejs-mobile/nodejs-mobile#155 upstream).

Point download-nodejs-mobile.sh at the fork's release and drop the
expectedMisaligned allowlist for libnode in build-backend.ts. The
audit now reports 30/30 .so file(s) verified with no carve-outs.

Cross-checked with llvm-readelf -l on all three Android ABIs from
the fork's combined release artifact: every PT_LOAD segment has
Align=0x4000.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI was restoring `android/libnode/` from the cache keyed only on
`nodejs-mobile-v${TAG}-android`. The fork swap kept `v18.20.4` but
changed the source repo, so the key matched the stale upstream
cache and the download step short-circuited (`cache-hit == true`),
re-introducing the 4 KB-aligned libnode that the audit just rejected.

- Workflow: fold a hash of `download-nodejs-mobile.sh` (which
  contains BASE_URL) into the cache key, so any future source swap
  auto-invalidates without anyone remembering to bump a key.
- Script: marker now records `${TAG}@${SOURCE_HASH}` and the `/tmp`
  cache filename includes `${SOURCE_HASH}`, so dev machines with a
  stale `android/libnode/` or `/tmp/nodejs-mobile-*.zip` from a
  prior upstream download also recover automatically.

Verified locally: a hand-written upstream-style marker `v18.20.4` is
treated as a cache miss and the script re-downloads from the fork.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gmaclennan gmaclennan enabled auto-merge (squash) April 29, 2026 14:04
@gmaclennan gmaclennan merged commit ece4044 into main Apr 29, 2026
7 checks passed
@gmaclennan gmaclennan deleted the claude/suspicious-noyce-5b8539 branch April 29, 2026 14:12
gmaclennan added a commit that referenced this pull request Apr 29, 2026
…der-804127

* origin/main:
  Add rootkey persistence and lifecycle state management (#36)
  android: audit 16 KB page alignment on every shipped .so (#43)
  chore: fix eslint configuration (#41)

# Conflicts:
#	android/src/main/java/com/comapeo/core/ComapeoCoreModule.kt
#	backend/index.js
#	ios/ComapeoCoreModule.swift
#	src/index.ts
gmaclennan added a commit that referenced this pull request Jun 22, 2026
## Optic Release Automation

This **draft** PR is opened by Github action
[optic-release-automation-action](https://github.com/nearform-actions/optic-release-automation-action).

A new **draft** GitHub release
[v1.0.0-pre.2](https://github.com/digidem/comapeo-core-react-native/releases/tag/untagged-c499977757c9745e56b2)
has been created.

Release author: @gmaclennan

#### If you want to go ahead with the release, please merge this PR.
When you merge:

- The GitHub release will be published

- The npm package with tag pre will be published according to the
publishing rules you have configured



- No major or minor tags will be updated as configured


#### If you close the PR

- The new draft release will be deleted and nothing will change

## What's Changed
* Android Testing Infrastructure & Bug Fixes by @gmaclennan in
#3
* chore: prebuild example/android; harden instrumented tests by
@gmaclennan in
#10
* Integrate @comapeo/core via IPC over Unix sockets by @gmaclennan in
#5
* chore: adjust repo setup by @achou11 in
#12
* chore: minor fixes based on expo-doctor by @achou11 in
#13
* Add iOS support & test infrastructure by @gmaclennan in
#6
* chore: add architecture docs & plans by @gmaclennan in
#11
* update some native deps used in backend by @achou11 in
#14
* iOS Phase 1: unified JS bundle + smoke test (simulator-only) by
@gmaclennan in
#15
* iOS Phase 2: xcframework Embed & Sign for native addons by @gmaclennan
in #16
* Phase 2 Android: jniLibs packaging + unified rollup loader plugin by
@gmaclennan in
#17
* chore: post-Phase-2 cleanup — comments, plan docs, agents.md by
@gmaclennan in
#33
* android: read abiFilters from reactNativeArchitectures (#30) by
@gmaclennan in
#35
* refactor: simplify build-backend.ts; rollup writes directly to native
asset trees by @gmaclennan in
#34
* chore: fix eslint configuration by @achou11 in
#41
* android: audit 16 KB page alignment on every shipped .so by
@gmaclennan in
#43
* Add rootkey persistence and lifecycle state management by @gmaclennan
in #36
* chore: move example app into apps directory by @achou11 in
#18
* refactor: per-component lifecycle state with derived ComapeoState by
@gmaclennan in
#47
* android: fold waitForFile into connect retry loop by @gmaclennan in
#52
* chore: add e2e testing app by @achou11 in
#49
* fix(android): drop setUnlockedDeviceRequired from rootkey wrapper key
by @gmaclennan in
#57
* fix(backend): cache stopping/error frames for late joiners by
@gmaclennan in
#58
* fix(ios-tests): wait for STOPPING before signalling node exit by
@gmaclennan in
#59
* fix(android): drain JNI stdio pumps before returning from node::Start
by @gmaclennan in
#60
* Sentry integration: Phase 1 + Phase 2a + Phase 2b by @gmaclennan in
#54
* feat(backend): polywasm-backed undici on iOS, re-enable maps plugin by
@gmaclennan in
#62
* ci: drop unreliable Android emulator snapshot caching by @gmaclennan
in #64
* feat(sentry): land Phase 3 — backend loader + RPC tracing by
@gmaclennan in
#63
* fix(ios-tests): serialise STOPPING/STOPPED observers in
testFullLifecycleStateTransitions by @gmaclennan in
#71
* use npm list instead of custom traversal to get native module versions
by @achou11 in
#70
* feat(sentry): land Phases 6 + 7a — Android exit reasons & iOS
MetricKit app-exit telemetry by @gmaclennan in
#72
* fix(sentry): make exit telemetry lossless and stop cross-process
clobbering by @gmaclennan in
#84
* chore(e2e): add e2e tests on browserstack via Maestro by @achou11 in
#56
* feat(sentry): migrate to @sentry/react-native v8; exit telemetry as
Application Metrics by @gmaclennan in
#73
* Map server integration by @gmaclennan in
#86
* chore(deps): upgrade to Expo SDK 56 (React Native 0.85) by @gmaclennan
in #87
* chore(ci): add release workflow by @gmaclennan in
#90
* chore: fix npm script and release build script by @gmaclennan in
#91
* chore(pack): don't try to package build files by @gmaclennan in
#92
* fix: start fastify listening by @gmaclennan in
#93
* perf(backend): switch bundler from rollup to rolldown by @gmaclennan
in #94
* fix(ci): ignore-scripts in ios npm installs by @gmaclennan in
#96
* fix(ci): replace --ignore-scripts with npm strict-allow-scripts
allowlist by @gmaclennan in
#106
* feat(config): let the consuming app supply the default project config
by @gmaclennan in
#95
* chore(release): merge prerelease branch. by @gmaclennan in
#110

## New Contributors
* @achou11 made their first contribution in
#12

**Full Changelog**:
https://github.com/digidem/comapeo-core-react-native/commits/v1.0.0-pre.2

<!--

<release-meta>{"id":342868678,"version":"v1.0.0-pre.2","npmTag":"pre","opticUrl":"https://optic-zf3votdk5a-ew.a.run.app/api/generate/"}</release-meta>
-->
@gmaclennan gmaclennan added the maintenance Refactor / test / chore / ci / build (changelog) label Jun 22, 2026
gmaclennan added a commit that referenced this pull request Jun 22, 2026
## Optic Release Automation

This **draft** PR is opened by Github action
[optic-release-automation-action](https://github.com/nearform-actions/optic-release-automation-action).

A new **draft** GitHub release
[v1.0.0-pre.2](https://github.com/digidem/comapeo-core-react-native/releases/tag/untagged-352a6c41c12fd02dec37)
has been created.

Release author: @gmaclennan

#### If you want to go ahead with the release, please merge this PR.
When you merge:

- The GitHub release will be published

- The npm package with tag pre will be published according to the
publishing rules you have configured



- No major or minor tags will be updated as configured


#### If you close the PR

- The new draft release will be deleted and nothing will change

<!-- Release notes generated using configuration in .github/release.yml
at 7fe80b4 -->

## What's Changed
### 🚀 Features
* Integrate @comapeo/core via IPC over Unix sockets by @gmaclennan in
#5
* Add iOS support & test infrastructure by @gmaclennan in
#6
* iOS Phase 1: unified JS bundle + smoke test (simulator-only) by
@gmaclennan in
#15
* iOS Phase 2: xcframework Embed & Sign for native addons by @gmaclennan
in #16
* Phase 2 Android: jniLibs packaging + unified rollup loader plugin by
@gmaclennan in
#17
* android: read abiFilters from reactNativeArchitectures (#30) by
@gmaclennan in
#35
* Add rootkey persistence and lifecycle state management by @gmaclennan
in #36
* Sentry integration: Phase 1 + Phase 2a + Phase 2b by @gmaclennan in
#54
* feat(backend): polywasm-backed undici on iOS, re-enable maps plugin by
@gmaclennan in
#62
* feat(sentry): land Phase 3 — backend loader + RPC tracing by
@gmaclennan in
#63
* feat(sentry): land Phases 6 + 7a — Android exit reasons & iOS
MetricKit app-exit telemetry by @gmaclennan in
#72
* feat(sentry): migrate to @sentry/react-native v8; exit telemetry as
Application Metrics by @gmaclennan in
#73
* Map server integration by @gmaclennan in
#86
* feat(config): let the consuming app supply the default project config
by @gmaclennan in
#95
### 🐛 Bug Fixes
* fix(android): drop setUnlockedDeviceRequired from rootkey wrapper key
by @gmaclennan in
#57
* fix(backend): cache stopping/error frames for late joiners by
@gmaclennan in
#58
* fix(ios-tests): wait for STOPPING before signalling node exit by
@gmaclennan in
#59
* fix(android): drain JNI stdio pumps before returning from node::Start
by @gmaclennan in
#60
* fix(ios-tests): serialise STOPPING/STOPPED observers in
testFullLifecycleStateTransitions by @gmaclennan in
#71
* fix(sentry): make exit telemetry lossless and stop cross-process
clobbering by @gmaclennan in
#84
* fix: start fastify listening by @gmaclennan in
#93
* fix(ci): ignore-scripts in ios npm installs by @gmaclennan in
#96
* fix(ci): replace --ignore-scripts with npm strict-allow-scripts
allowlist by @gmaclennan in
#106
* fix(release): stop `npm pack --dry-run` leaking dry-run into backend
install by @gmaclennan in
#129
### ⚡ Performance
* perf(backend): switch bundler from rollup to rolldown by @gmaclennan
in #94
### ⬆️ Dependencies
* update some native deps used in backend by @achou11 in
#14
* chore(deps): upgrade to Expo SDK 56 (React Native 0.85) by @gmaclennan
in #87
### 🏗️ Maintenance
* Android Testing Infrastructure & Bug Fixes by @gmaclennan in
#3
* chore: prebuild example/android; harden instrumented tests by
@gmaclennan in
#10
* chore: adjust repo setup by @achou11 in
#12
* chore: minor fixes based on expo-doctor by @achou11 in
#13
* chore: add architecture docs & plans by @gmaclennan in
#11
* chore: post-Phase-2 cleanup — comments, plan docs, agents.md by
@gmaclennan in
#33
* refactor: simplify build-backend.ts; rollup writes directly to native
asset trees by @gmaclennan in
#34
* chore: fix eslint configuration by @achou11 in
#41
* android: audit 16 KB page alignment on every shipped .so by
@gmaclennan in
#43
* chore: move example app into apps directory by @achou11 in
#18
* refactor: per-component lifecycle state with derived ComapeoState by
@gmaclennan in
#47
* android: fold waitForFile into connect retry loop by @gmaclennan in
#52
* chore: add e2e testing app by @achou11 in
#49
* ci: drop unreliable Android emulator snapshot caching by @gmaclennan
in #64
* use npm list instead of custom traversal to get native module versions
by @achou11 in
#70
* chore(e2e): add e2e tests on browserstack via Maestro by @achou11 in
#56
* chore(ci): add release workflow by @gmaclennan in
#90
* chore: fix npm script and release build script by @gmaclennan in
#91
* chore(pack): don't try to package build files by @gmaclennan in
#92
* chore(release): merge prerelease branch. by @gmaclennan in
#110
* ci(e2e): retry BrowserStack builds on infra-class flakes by
@gmaclennan in
#113
### Other Changes
* ci: derive changelog labels from PR titles + add Dependabot by
@gmaclennan in
#114

## New Contributors
* @achou11 made their first contribution in
#12
* @optic-release-automation[bot] made their first contribution in
#112

**Full Changelog**:
https://github.com/digidem/comapeo-core-react-native/commits/v1.0.0-pre.2

<!--

<release-meta>{"id":342970724,"version":"v1.0.0-pre.2","npmTag":"pre","opticUrl":"https://optic-zf3votdk5a-ew.a.run.app/api/generate/"}</release-meta>
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance Refactor / test / chore / ci / build (changelog)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit 16KB page alignment on shipped native libraries

1 participant