Skip to content

The build

The recipe

  1. Patch zig 0.16.0 std so process spawning avoids raw fork(2). Android’s app seccomp policy does not allow it.
  2. Cross-compile fx for x86_64-linux-musl and aarch64-linux-musl with zig. Binaries land in artifacts/ and in android/app/src/main/jniLibs/<abi>/libfx.so.
  3. Build the app with gradle assembleDebug, then adb install, adb reverse, launch.
  4. 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 verdict

A 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

  1. zig std. Raw fork(2) sits outside the allowlist. Bionic emulates fork through clone, so the patch teaches std/Io/Threaded.zig spawnPosix to call clone(SIGCHLD) directly. scripts/patch-zig-fork.sh applies it.
  2. fx shell_resolver. It must accept /system/bin/sh (mksh) as a login shell. Android has no passwd database, so getpwuid_r never sets pw_shell, and there is no /bin/bash.
  3. fx command_runner. The captured-command path now uses pipe2 plus clone plus dup3 plus chdir plus execve, all allowlisted. It bypasses the foreground-session supervisor, whose self-re-exec machinery dies inside app processes.
  4. App packaging. The fx binary ships as jniLibs/<abi>/libfx.so, which lands in nativeLibraryDir, the only app-owned location that executes.