mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-05 12:47:43 +00:00
Fix Android ARM32 QEMU personality on hosts without AArch32 EL0
This commit is contained in:
1 parent
31b91a1cdc
commit
e1d2f1ebca
8 files changed
+317
-1
No files matched your search
@@ -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, ... }:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ];
|
||||
})
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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:
|
||||
{
|
||||
Reference in new issue
Block a user