From e1d2f1ebca1c1bfa4522cd770fdc9390b672b478 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Thu, 15 Oct 2026 21:55:49 +0900 Subject: [PATCH] Fix Android ARM32 QEMU personality on hosts without AArch32 EL0 --- flake.nix | 3 +- scripts/nix/README.md | 25 +++++ scripts/nix/android-arm32-qemu.nix | 12 ++ scripts/nix/android-emulator-overlay.nix | 36 ++++++ scripts/nix/android-personality-check.c | 106 ++++++++++++++++++ scripts/nix/android-personality-tls.S | 10 ++ scripts/nix/check-android-personality.nix | 42 +++++++ .../nix/simplex-qemu-arm-personality.patch | 84 ++++++++++++++ 8 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 scripts/nix/android-arm32-qemu.nix create mode 100644 scripts/nix/android-emulator-overlay.nix create mode 100644 scripts/nix/android-personality-check.c create mode 100644 scripts/nix/android-personality-tls.S create mode 100644 scripts/nix/check-android-personality.nix create mode 100644 scripts/nix/simplex-qemu-arm-personality.patch diff --git a/flake.nix b/flake.nix index fcec321d91..097245d9c1 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,8 @@ # `appendOverlays` with a singleton is identical to `extend`. let nativePackages = import ./scripts/nix/native-packages.nix { inherit haskellNix; }; in let compilerCache = import ./scripts/nix/compiler-cache-overlay.nix { inherit haskellNix; }; in - let pkgs = (nativePackages system).appendOverlays [android26 compilerCache]; in + let androidEmulator = import ./scripts/nix/android-emulator-overlay.nix { inherit haskellNix; }; in + let pkgs = (nativePackages system).appendOverlays [android26 compilerCache androidEmulator]; in let compilerSelection = import ./scripts/nix/compiler-selection.nix { inherit haskellNix; }; in let mobileRuntime = import ./scripts/nix/mobile-runtime.nix { inherit pkgs compilerSelection; }; in let drv' = { extra-modules, pkgs', package-flags ? {}, package-ghc-options ? {}, mobile ? false, ... }: diff --git a/scripts/nix/README.md b/scripts/nix/README.md index 3309b3ef8c..198bb02f9a 100644 --- a/scripts/nix/README.md +++ b/scripts/nix/README.md @@ -60,6 +60,31 @@ Apple A7 instruction baseline before `mac2ios` retags the archives. The cached native compiler itself stays unchanged. Android applies the RTS source patch when building its boot libraries. +Android ARM32's QEMU wrapper preserves the guest's `PER_LINUX32` execution +domain when an ARM64 host kernel rejects that domain. Upper personality flags +still go through the host, and queries and setters retain the previous guest +value. This applies only to the Android ARM32 emulator; native compiler cache +paths and the other emulator targets stay unchanged. Android's libc initializes +this state again at startup after an exec. + +A standalone C regression checks startup, full personality values, the host's +`UNAME26` effect, and independent thread/fork state. Run it from this checkout +with configured Linux builders; Nix supplies the pinned Android API26 NDK and +patched QEMU. The NDK compiles the tiny payload on x86_64 Linux; QEMU then runs +on the selected Linux architecture. For example, on an Apple Silicon Mac whose +Linux builder supports both x86_64 and ARM64: + +```sh +nix build --impure --file scripts/nix/check-android-personality.nix \ + --argstr system aarch64-linux --max-jobs 1 --cores 1 --print-build-logs +``` + +Use `--argstr system x86_64-linux` for an x86_64 Linux builder. This optional +check does not add a Hydra output. It checks C startup and personality handling; +it does not qualify GHC/base under Rosetta, cross-TH, or Android device behavior. +The static C fixture supplies the ARM TLS alignment input added to newer +Android CRTs; its minimum API stays at 26. + `NIX_ARGS` passes additional options to `nix build`, and `NIX_OUT_DIR` changes the output-link directory. Each derivation has a two-hour timeout by default; set `NIX_BUILD_TIMEOUT` in seconds to change it. For a wall-clock limit on a local diff --git a/scripts/nix/android-arm32-qemu.nix b/scripts/nix/android-arm32-qemu.nix new file mode 100644 index 0000000000..41aef4d7a4 --- /dev/null +++ b/scripts/nix/android-arm32-qemu.nix @@ -0,0 +1,12 @@ +{ haskellNix, pkgs }: +(import (haskellNix + "/overlays/qemu-linux-user.nix") { + qemu = pkgs.qemu.override { + userOnly = true; + hostCpuTargets = [ "arm-linux-user" ]; + }; + inherit (pkgs) lib; + buildPlatform = pkgs.stdenv.hostPlatform; + qemuSuffix = "arm"; +}).overrideAttrs (old: { + patches = (old.patches or []) ++ [ ./simplex-qemu-arm-personality.patch ]; +}) diff --git a/scripts/nix/android-emulator-overlay.nix b/scripts/nix/android-emulator-overlay.nix new file mode 100644 index 0000000000..4763bec50c --- /dev/null +++ b/scripts/nix/android-emulator-overlay.nix @@ -0,0 +1,36 @@ +{ haskellNix }: +final: prev: +let + lib = prev.lib; + active = prev.stdenv.hostPlatform.isAndroid + && prev.stdenv.hostPlatform.isAarch32 + && prev.stdenv.buildPlatform.isLinux; + # Keep this outside the native compiler package sets. Only Android ARM32 + # Template Haskell and test wrappers need the guest personality fallback. + emulator = import ./android-arm32-qemu.nix { + inherit haskellNix; + pkgs = prev.pkgsBuildBuild; + }; + exes = prev.haskell-nix.iserv-proxy-exes.ghc914-sh; + mkTemplateHaskell = interpreterExes: + { inherit (prev.haskell-nix) + emulatorSystemFeatures emulatorNativeBuilderPackages; } + // import (haskellNix + "/overlays/linux-cross.nix") { + inherit (prev.stdenv) hostPlatform buildPlatform; + inherit (prev) stdenv lib gmp; + inherit (prev.pkgsBuildBuild) + writeShellScriptBin symlinkJoin runCommand makeWrapper; + inherit (prev.haskell-nix) haskellLib; + qemu = emulator; + inherit (interpreterExes) + iserv-proxy iserv-proxy-interpreter iserv-proxy-interpreter-prof; + }; +in lib.optionalAttrs active { + haskell-nix = prev.haskell-nix // { + templateHaskell = prev.haskell-nix.templateHaskell // { + ghc914-sh = mkTemplateHaskell exes // { + evalWith = builtins.mapAttrs (_: mkTemplateHaskell) exes.evalWith; + }; + }; + }; +} diff --git a/scripts/nix/android-personality-check.c b/scripts/nix/android-personality-check.c new file mode 100644 index 0000000000..52bc18b3a0 --- /dev/null +++ b/scripts/nix/android-personality-check.c @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: AGPL-3.0-only */ +/* Copyright 2026 Moritz Angermann , zw3rk pte. ltd. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static uint32_t current_persona(void) +{ + return (uint32_t)personality(UINT32_MAX); +} + +static int equal_persona(uint32_t actual, uint32_t expected) +{ + if (actual == expected) { + return 0; + } + (void)fprintf(stderr, "persona got=%08" PRIx32 " expected=%08" PRIx32 "\n", + actual, expected); + return 1; +} + +struct thread_case { + uint32_t initial; + int result; +}; + +static void *check_thread(void *opaque) +{ + struct thread_case *test = opaque; + uint32_t changed; + + if (test == NULL) { + return NULL; + } + changed = test->initial ^ UNAME26; + test->result = equal_persona(current_persona(), test->initial); + test->result |= equal_persona((uint32_t)personality(changed), test->initial); + test->result |= equal_persona(current_persona(), changed); + return NULL; +} + +int main(void) +{ + uint32_t initial = current_persona(); + uint32_t changed = initial ^ UNAME26; + uint32_t linux_domain = (initial & ~(uint32_t)PER_MASK) | PER_LINUX; + struct thread_case test = { initial, 1 }; + pthread_t thread; + pid_t child; + int status; + struct utsname names; + + if ((initial & PER_MASK) != PER_LINUX32) { + (void)fprintf(stderr, "Android startup did not retain PER_LINUX32\n"); + return 10; + } + if (equal_persona((uint32_t)personality(changed), initial) || + equal_persona(current_persona(), changed) || uname(&names) != 0) { + return 11; + } + (void)fprintf(stderr, "UNAME26=%u release=%s\n", + (changed & UNAME26) != 0 ? 1U : 0U, names.release); + if (((changed & UNAME26) != 0 && + strncmp(names.release, "2.6.", 4) != 0) || + equal_persona((uint32_t)personality(initial), changed)) { + return 11; + } + if (pthread_create(&thread, NULL, check_thread, &test) != 0) { + return 12; + } + if (pthread_join(thread, NULL) != 0 || test.result != 0 || + equal_persona(current_persona(), initial)) { + return 13; + } + child = fork(); + if (child < 0) { + return 14; + } + if (child == 0) { + int result = equal_persona(current_persona(), initial); + result |= equal_persona((uint32_t)personality(linux_domain), initial); + result |= equal_persona(current_persona(), linux_domain); + _exit(result == 0 ? 0 : 15); + } + if (waitpid(child, &status, 0) != child || !WIFEXITED(status) || + WEXITSTATUS(status) != 0 || equal_persona(current_persona(), initial)) { + return 16; + } + if (equal_persona((uint32_t)personality(linux_domain), initial) || + equal_persona(current_persona(), linux_domain) || + equal_persona((uint32_t)personality(initial), linux_domain) || + equal_persona(current_persona(), initial)) { + return 17; + } + if (fputs("SIMPLEX_ARMV7_PERSONALITY_CHECK_PASS\n", stderr) < 0) { + return 18; + } + return 0; +} diff --git a/scripts/nix/android-personality-tls.S b/scripts/nix/android-personality-tls.S new file mode 100644 index 0000000000..caede64f0a --- /dev/null +++ b/scripts/nix/android-personality-tls.S @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: AGPL-3.0-only */ +/* Copyright 2026 Moritz Angermann , zw3rk pte. ltd. */ + +/* Bionic's ARM static TLS layout needs 32-byte alignment. API 26's crtbegin + * predates that input section; retain this zero-sized alignment constraint + * even when linking the standalone regression with --gc-sections. + */ +.section .tdata.simplex_android_tls_align,"awTR",%progbits +.p2align 5 +.section .note.GNU-stack,"",%progbits diff --git a/scripts/nix/check-android-personality.nix b/scripts/nix/check-android-personality.nix new file mode 100644 index 0000000000..9d8100d23b --- /dev/null +++ b/scripts/nix/check-android-personality.nix @@ -0,0 +1,42 @@ +{ system ? builtins.currentSystem }: +let + flake = builtins.getFlake (toString ../..); + haskellNix = flake.inputs.haskellNix; + nativePackages = import ./native-packages.nix { inherit haskellNix; }; + pkgs = nativePackages system; + # The pinned NDK runs on x86_64 Linux. Keep the tiny payload compilation on + # that existing target, even when QEMU itself is tested on native ARM64. + buildPkgs = nativePackages "x86_64-linux"; + target = import buildPkgs.path { + localSystem = { system = "x86_64-linux"; }; + inherit (buildPkgs) overlays; + config = buildPkgs.config // { allowUnfree = true; }; + crossSystem = buildPkgs.lib.systems.examples.armv7a-android-prebuilt + // { androidSdkVersion = "26"; }; + }; + emulator = import ./android-arm32-qemu.nix { inherit haskellNix pkgs; }; + payload = buildPkgs.runCommand "android-arm32-personality-payload" {} '' + mkdir -p "$out" + # API 26's crtbegin predates Bionic's aligned TLS input. Add the same + # layout constraint for this static C fixture without raising its API. + ${target.stdenv.cc}/bin/${target.stdenv.cc.targetPrefix}cc \ + -march=armv7-a -c ${./android-personality-tls.S} -o tls-align.o + # The NDK wrapper recognizes bare -no-pie; Clang itself reports it unused + # for this static link. Keep that wrapper flag while suppressing its warning. + ${target.stdenv.cc}/bin/${target.stdenv.cc.targetPrefix}cc \ + -std=c11 -Werror=vla -pedantic -Wall -Wextra \ + -march=armv7-a -static -no-pie -Wno-unused-command-line-argument \ + -Wl,--gc-sections -pthread \ + ${./android-personality-check.c} tls-align.o -o "$out/probe" + ''; +in assert pkgs.lib.assertMsg + (builtins.elem system [ "aarch64-linux" "x86_64-linux" ]) + "The Android personality check needs a Linux builder; pass --argstr system aarch64-linux or x86_64-linux."; +pkgs.runCommand "android-arm32-personality-check" {} '' + set -euo pipefail + mkdir -p "$out" + cp ${payload}/probe "$out/probe" + uname -m | tee "$out/builder-architecture" + ${emulator}/bin/qemu-arm -version | tee "$out/qemu-version" + timeout -k 5 60 ${emulator}/bin/qemu-arm "$out/probe" 2>&1 | tee "$out/probe.log" +'' diff --git a/scripts/nix/simplex-qemu-arm-personality.patch b/scripts/nix/simplex-qemu-arm-personality.patch new file mode 100644 index 0000000000..3b75457d47 --- /dev/null +++ b/scripts/nix/simplex-qemu-arm-personality.patch @@ -0,0 +1,84 @@ +--- a/linux-user/qemu.h ++++ b/linux-user/qemu.h +@@ -100,6 +100,11 @@ + + struct TaskState { + pid_t ts_tid; /* tid (or pid) of this task */ ++#if defined(TARGET_ARM) && !defined(TARGET_AARCH64) ++ /* Guest execution domain when the host cannot accept PER_LINUX32. */ ++ uint32_t guest_personality; ++ bool guest_personality_virtualized; ++#endif + #ifdef TARGET_ARM + # ifdef TARGET_ABI32 + /* FPA state */ +--- a/linux-user/syscall.c ++++ b/linux-user/syscall.c +@@ -6969,6 +6969,11 @@ + ts->bprm = parent_ts->bprm; + ts->info = parent_ts->info; + ts->signal_mask = parent_ts->signal_mask; ++#if defined(TARGET_ARM) && !defined(TARGET_AARCH64) ++ ts->guest_personality = parent_ts->guest_personality; ++ ts->guest_personality_virtualized = ++ parent_ts->guest_personality_virtualized; ++#endif + + #ifdef TARGET_AARCH64 + ts->gcs_el0_locked = parent_ts->gcs_el0_locked; +@@ -9653,6 +9658,43 @@ + int, __to_dfd, const char *, __to_pathname, unsigned int, flag) + #endif + ++#if defined(TARGET_ARM) && !defined(TARGET_AARCH64) ++/* ++ * Android's ARM32 libc requests PER_LINUX32 during startup. An ARM64 host ++ * without AArch32 EL0 rejects that execution domain even though TCG can run ++ * the guest. QEMU already provides the guest architecture for uname(). ++ * ++ * Only substitute the execution domain after the host rejects it with EINVAL. ++ * Keep every upper flag host-backed, and retain the full guest value so query ++ * and setter return values do not expose the substituted host domain. This ++ * per-task state is copied by clone; a new Android exec initializes it again ++ * when libc sets its personality. ++ */ ++static abi_long do_arm32_personality(CPUState *cpu, uint32_t value) ++{ ++ TaskState *ts = get_task_state(cpu); ++ uint32_t previous = ts->guest_personality; ++ bool virtualized = ts->guest_personality_virtualized; ++ int ret = personality(value); ++ ++ if (ret == -1 && errno == EINVAL && (value & PER_MASK) == PER_LINUX32) { ++ ret = personality((value & ~PER_MASK) | PER_LINUX); ++ if (ret != -1) { ++ ts->guest_personality = value; ++ ts->guest_personality_virtualized = true; ++ return virtualized ? previous : (uint32_t)ret; ++ } ++ } ++ if (ret == -1) { ++ return get_errno(ret); ++ } ++ if (value != UINT32_MAX) { ++ ts->guest_personality_virtualized = false; ++ } ++ return virtualized ? previous : (uint32_t)ret; ++} ++#endif ++ + /* This is an internal helper for do_syscall so that it is easier + * to have a single return point, so that actions, such as logging + * of syscall results, can be performed. +@@ -11732,7 +11774,11 @@ + case TARGET_NR_fchdir: + return get_errno(fchdir(arg1)); + case TARGET_NR_personality: ++#if defined(TARGET_ARM) && !defined(TARGET_AARCH64) ++ return do_arm32_personality(cpu, (uint32_t)arg1); ++#else + return get_errno(personality(arg1)); ++#endif + #ifdef TARGET_NR__llseek /* Not on alpha */ + case TARGET_NR__llseek: + {