Fix Android ARM32 QEMU personality on hosts without AArch32 EL0

This commit is contained in:
Moritz Angermann authored and Moritz Angermann committed 2026-10-15 21:57:10 +09:00
1 parent 31b91a1cdc
commit e1d2f1ebca
8 files changed
+317 -1

No files matched your search

+2 -1
View File
@@ -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, ... }:
+25
View File
@@ -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
+12
View File
@@ -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 ];
})
+36
View File
@@ -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;
};
};
};
}
+106
View File
@@ -0,0 +1,106 @@
/* SPDX-License-Identifier: AGPL-3.0-only */
/* Copyright 2026 Moritz Angermann <moritz@zw3rk.com>, zw3rk pte. ltd. */
#include <inttypes.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/utsname.h>
#include <sys/personality.h>
#include <sys/wait.h>
#include <unistd.h>
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;
}
+10
View File
@@ -0,0 +1,10 @@
/* SPDX-License-Identifier: AGPL-3.0-only */
/* Copyright 2026 Moritz Angermann <moritz@zw3rk.com>, 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
+42
View File
@@ -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"
''
@@ -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:
{