The build
The recipe
- Patch zig 0.16.0 std so process spawning avoids raw
fork(2). Android’s app seccomp policy does not allow it. - Cross-compile fx for
x86_64-linux-muslandaarch64-linux-muslwith zig. Binaries land inartifacts/and inandroid/app/src/main/jniLibs/<abi>/libfx.so. - Build the app with gradle
assembleDebug, thenadb install,adb reverse, launch. - Serve the mock gateway on the host with
py scripts/mock_gateway.py 8099.
Four commands re-run the proof afterwards, starting with the gateway in its own shell.
py scripts/mock_gateway.py 8099 # host mock gateway
adb reverse tcp:8099 tcp:8099 # loop it back on-device
powershell -File scripts/build-android.ps1 # gradle build, install, launch
adb shell "run-as com.droidfx.proof cat files/result.json" # read back the verdictA full binary rebuild rarely needs a re-run. When it does, the command is
bash scripts/build-fx.sh both (zig 0.16.0 on PATH).
Why musl
Zig 0.16 cannot target linux-android directly. There is no bionic libc
target. But static musl binaries run fine on Android, so
x86_64-linux-musl and aarch64-linux-musl are the targets. No WSL is
needed. The whole cross-compile runs on the Windows host.
The seccomp jungle
Android’s app seccomp policy traps raw fork(2), pipe(2), and
dup2(2) with SIGSYS. A small C probe confirmed it on-device.
clone(2), pipe2(2), and dup3(2) are allowed. Everything fx needs
has an allowed spelling. The work was finding each one.
The four fixes
- zig std. Raw
fork(2)sits outside the allowlist. Bionic emulates fork through clone, so the patch teachesstd/Io/Threaded.zigspawnPosix to callclone(SIGCHLD)directly.scripts/patch-zig-fork.shapplies it. - fx shell_resolver. It must accept
/system/bin/sh(mksh) as a login shell. Android has no passwd database, sogetpwuid_rnever setspw_shell, and there is no/bin/bash. - fx command_runner. The captured-command path now uses
pipe2pluscloneplusdup3pluschdirplusexecve, all allowlisted. It bypasses the foreground-session supervisor, whose self-re-exec machinery dies inside app processes. - App packaging. The fx binary ships as
jniLibs/<abi>/libfx.so, which lands innativeLibraryDir, the only app-owned location that executes.