Skip to content

Commit 9c415c3

Browse files
committed
feat(rootfs): native armhf on aarch64 host via runtime-disable of qemu-arm
When building an armhf image on an aarch64 host that supports 32-bit ARM execution (kernel CONFIG_COMPAT, CPU has 32-bit user mode), disable the qemu-arm handler in /proc/sys/fs/binfmt_misc/ for the duration of the build. The kernel's own binfmt_elf then handles 32-bit ARM ELFs natively via CONFIG_COMPAT — typically ~12x faster than qemu-arm-static emulation on Cortex-A53/A55/A72/A73/A76. Continues #9284 (arm64-compat-vdso extension + custom_kernel_make_params hook), which enabled COMPAT_VDSO in arm64 kernels. Activation point is delayed to AFTER mmdebstrap. Its cross-arch path extracts base packages and runs their postinst hooks, which require a working qemu-arm registration to populate the chroot before libc/ld-linux-armhf.so.3 exist for binfmt_elf to fall through to. The speedup applies to all subsequent chroot operations (install_distribution_specific, customize_image, update-initramfs). Killswitch: NATIVE_ARMHF_ON_ARM64=no (synonyms: never, disabled). Concurrent armbian builds on the same host kernel are coordinated purely via kernel BSD flock on the binfmt entry itself — no userspace state, no per-builder lockfiles, no /run/lock directory tree: - Each builder holds LOCK_SH on /proc/sys/fs/binfmt_misc/qemu-arm via a long-lived fd. Kernel BSD-flock counter is the refcount; the kernel releases the fd on process exit (crash-safe). - First-arrival idempotently `echo 0` to disable. Subsequent arrivals observe 0 and proceed without writing. - On exit, release LOCK_SH; last-out detects via LOCK_EX-LOCK_NB on a fresh fd, succeeds iff zero other LOCK_SH holders. Last-out re-enables qemu-arm. Trade-offs (documented): - Prior qemu-arm state is not recorded across independent builds. Last- out unconditionally re-enables. Admin's pre-existing `disabled` policy is not preserved. - No defense against an external agent toggling qemu-arm mid-build. Documented; either don't, or set NATIVE_ARMHF_ON_ARM64=no on builds that should not interfere. - Cleanup ordering invariant: BSD flock is per-OFD, so a forked subshell inheriting our SH-fd shares the same lock entry. add_cleanup_handler runs in registration order; we register after the umount/SDCARD/MOUNT teardown handlers, so by the time we run, the docker container is killed and its child-tree (with our inherited fd) gone. Documented in the trap handler's docstring; if a future refactor breaks ordering, POSIX F_SETLK or explicit descendant-kill are the documented escape hatches. Empirical (helios4 mvebu/armhf, BUILD_MINIMAL=yes BRANCH=edge RELEASE=noble): - baseline (rootfs cache-miss): 60:35 - this PR (rootfs cache-miss): 19:27 (3.12x) - this PR (rootfs cache-hit): 6:38 (~9x over expected baseline cache-hit) Concurrency verified on Hetzner CAX21 (kernel 6.8.0-90-generic): two independent armbian worktrees concurrent on same host, both enter native path, kernel SH-counter holds qemu-arm disabled across both, first- finished suppresses restore (LOCK_EX-NB blocked by other's SH), last- finished restores qemu-arm to enabled. SIGINT mid-chroot also cleans up correctly. Reviewed iteratively with codex (gpt-5.5) and coderabbitai. The earlier revision of this work coordinated via a userspace owner-flock directory (~250 lines, depended on /run/lock being host-bind-mounted into build containers, which Armbian docker does not do); replaced with the kernel- flock-on-binfmt-entry approach above. Assisted-by: Claude:claude-opus-4.7
1 parent 574b46a commit 9c415c3

3 files changed

Lines changed: 201 additions & 7 deletions

File tree

lib/functions/main/rootfs-image.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ function build_rootfs_and_image() {
1313
# get a basic rootfs, either from cache or from scratch
1414
get_or_create_rootfs_cache_chroot_sdcard # only occurrence of this; has its own logging sections
1515

16+
# Cache-hit path also benefits — the extracted rootfs has libc/ld-linux, so kernel
17+
# binfmt_elf can run 32-bit ARM ELF natively. Idempotent on cache-miss path where
18+
# create_new_rootfs_cache_via_debootstrap already activated this.
19+
_native_armhf_setup_binfmt_elf || true
20+
1621
# deploy the qemu binary, no matter where the rootfs came from (built or cached)
1722
LOG_SECTION="deploy_qemu_binary_to_chroot_image" do_with_logging deploy_qemu_binary_to_chroot "${SDCARD}" "image" # undeployed at end of this function
1823

lib/functions/rootfs/qemu-static.sh

Lines changed: 189 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ function deploy_qemu_binary_to_chroot() {
1717
return 0
1818
fi
1919

20+
# Native armhf path is active: kernel binfmt_elf executes 32-bit ARM ELF via
21+
# CONFIG_COMPAT, no qemu-arm-static needed inside the chroot.
22+
if [[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]]; then
23+
display_alert "Native armhf via binfmt_elf" "skipping qemu binary deployment during ${caller}" "info"
24+
return 0
25+
fi
26+
2027
# Source: try the historical name first (qemu-<arch>-static), fall back
2128
# to the bare name shipped by Ubuntu resolute's qemu-user-binfmt package
2229
# (e.g. /usr/bin/qemu-aarch64).
@@ -76,8 +83,19 @@ function undeploy_qemu_binary_from_chroot() {
7683
declare dst_target_bkp="${dst_target}.armbian.orig"
7784
declare dst_target_alt_bkp="${dst_target_alt}.armbian.orig"
7885

79-
# Check the binary we deployed is there. If not, panic, as we've lost control.
86+
# Check the binary we deployed is there. Two reasons it might be missing:
87+
# 1. ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF was active when the matching deploy
88+
# ran, so nothing was copied — graceful no-op.
89+
# 2. Genuine state loss — panic, we lost control.
90+
# We must NOT skip the removal solely on the native-armhf flag, because deploy
91+
# may have run before that flag was set (rootfs-create deploys at line 134,
92+
# native-armhf flips at line 149); skipping the undeploy in that case leaks
93+
# the host's qemu-arm-static into the rootfs cache tarball.
8094
if [[ ! -f "${dst_target}" ]]; then
95+
if [[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]]; then
96+
display_alert "Native armhf via binfmt_elf" "no qemu binary to remove during ${caller}" "debug"
97+
return 0
98+
fi
8199
exit_with_error "Missing qemu binary during undeploy_qemu_binary_from_chroot from ${caller}"
82100
fi
83101

@@ -132,6 +150,125 @@ function prepare_host_binfmt_qemu() {
132150
return 0
133151
}
134152

153+
# Native armhf on aarch64 host: runtime-disable qemu-arm in binfmt_misc so 32-bit
154+
# ARM ELF falls through to kernel binfmt_elf and runs natively via CONFIG_COMPAT
155+
# (~12× faster than qemu emulation). Killswitch: NATIVE_ARMHF_ON_ARM64=no.
156+
#
157+
# Multi-build coordination is purely kernel-level: each builder holds LOCK_SH on
158+
# /proc/sys/fs/binfmt_misc/qemu-arm; first-arrival `echo 0`, last-out (LOCK_EX-NB
159+
# succeeds → no other SH holders) `echo 1`. No userspace state, no per-builder
160+
# files. Trade-off: an admin's pre-existing `disabled` state is not preserved
161+
# across the build window.
162+
163+
# Read the qemu-arm 'enabled' flag without touching it. Echoes one of:
164+
# 1 — registered and enabled
165+
# 0 — registered and disabled
166+
# missing — not registered
167+
function _native_armhf_observe_qemu_arm_state() {
168+
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-arm ]]; then
169+
echo "missing"
170+
return 0
171+
fi
172+
if head -1 /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null | grep -q '^enabled'; then
173+
echo "1"
174+
else
175+
echo "0"
176+
fi
177+
}
178+
179+
function _native_armhf_setup_binfmt_elf() {
180+
case "${NATIVE_ARMHF_ON_ARM64:-auto}" in
181+
no | never | disabled) return 1 ;;
182+
esac
183+
184+
# Idempotent: callers in rootfs-create.sh and rootfs-image.sh invoke this
185+
# from both the cache-miss and cache-hit paths.
186+
[[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]] && return 0
187+
[[ "${ARCH}" == "armhf" ]] || return 1
188+
[[ "$(arch)" == "aarch64" ]] || return 1
189+
190+
# Pre-flight is unreliable when qemu-arm is enabled (it interprets the
191+
# arch-test stub); the authoritative check is post-disable below.
192+
if ! arch-test armhf > /dev/null 2>&1; then
193+
display_alert "Native armhf via binfmt_elf" "arch-test pre-flight failed; falling back to qemu-arm-static emulation" "info"
194+
return 1
195+
fi
196+
197+
# qemu-arm not registered → native already active, no anchor needed.
198+
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-arm ]]; then
199+
display_alert "Native armhf via binfmt_elf" "qemu-arm not registered; native armhf already in effect" "info"
200+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
201+
return 0
202+
fi
203+
204+
# Group-scoped 2>/dev/null: a bare `exec {fd}< file 2>/dev/null` would
205+
# persistently redirect THIS shell's stderr (since exec without a command
206+
# applies redirections to the current shell), silencing every later
207+
# display_alert that writes to stderr.
208+
if ! { exec {_native_armhf_lock_fd}< /proc/sys/fs/binfmt_misc/qemu-arm; } 2> /dev/null; then
209+
display_alert "Native armhf via binfmt_elf" "cannot open binfmt_misc/qemu-arm; falling back to qemu emulation" "wrn"
210+
return 1
211+
fi
212+
if ! flock -s -w 30 "${_native_armhf_lock_fd}"; then
213+
display_alert "Native armhf via binfmt_elf" "could not acquire shared flock on binfmt_misc/qemu-arm within 30s; falling back to qemu emulation" "wrn"
214+
exec {_native_armhf_lock_fd}>&-
215+
unset _native_armhf_lock_fd
216+
return 1
217+
fi
218+
219+
if [[ "$(_native_armhf_observe_qemu_arm_state)" == "1" ]]; then
220+
if ! echo 0 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null; then
221+
display_alert "Native armhf via binfmt_elf" "could not disable qemu-arm (no CAP_SYS_ADMIN?); falling back to qemu-arm-static emulation" "wrn"
222+
exec {_native_armhf_lock_fd}>&-
223+
unset _native_armhf_lock_fd
224+
return 1
225+
fi
226+
fi
227+
228+
# Register cleanup BEFORE the authoritative arch-test, so a failure
229+
# there still releases the lock via the trap handler.
230+
add_cleanup_handler trap_handler_native_armhf_restore_qemu_arm
231+
232+
# Post-disable check is authoritative: arch-test now faces what the
233+
# chroot exec will face. False-positive if host kernel lacks COMPAT_VDSO
234+
# (see extensions/arm64-compat-vdso, PR #9284).
235+
if ! arch-test armhf > /dev/null 2>&1; then
236+
display_alert "Native armhf via binfmt_elf" "post-disable verification failed (host kernel lacks COMPAT_VDSO — see extensions/arm64-compat-vdso); restoring and falling back to emulation" "wrn"
237+
trap_handler_native_armhf_restore_qemu_arm
238+
return 1
239+
fi
240+
241+
display_alert "Native armhf via binfmt_elf" "kernel $(uname -r), aarch64 host with COMPAT_VDSO; qemu-arm disabled, kernel binfmt_elf takes over" "info"
242+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
243+
return 0
244+
}
245+
246+
# Cleanup ordering invariant: this handler must run AFTER cleanups that kill
247+
# the build's subshells (umount / SDCARD / MOUNT teardown). BSD flock is per-
248+
# OFD, so a forked subshell inheriting our SH-fd shares the same lock entry —
249+
# the LOCK_EX-NB probe below would falsely block on the inherited fd of a
250+
# still-alive child. add_cleanup_handler runs in registration order; the
251+
# umount handlers register first, so by the time we run, the docker container
252+
# is dead and its child-tree with it. Verified empirically (SIGINT mid-chroot).
253+
function trap_handler_native_armhf_restore_qemu_arm() {
254+
[[ -n "${_native_armhf_lock_fd:-}" ]] || return 0
255+
exec {_native_armhf_lock_fd}>&-
256+
unset _native_armhf_lock_fd
257+
258+
[[ -e /proc/sys/fs/binfmt_misc/qemu-arm ]] || return 0
259+
260+
# Group-scoped 2>/dev/null on the exec — see _native_armhf_setup_binfmt_elf.
261+
declare last_fd
262+
if ! { exec {last_fd}< /proc/sys/fs/binfmt_misc/qemu-arm; } 2> /dev/null; then
263+
return 0
264+
fi
265+
if flock -x -n "${last_fd}"; then
266+
echo 1 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null || true
267+
display_alert "Native armhf via binfmt_elf" "last out; qemu-arm restored to enabled" "info"
268+
fi
269+
exec {last_fd}>&-
270+
}
271+
135272
# The actual binfmt manipulations when cross-build is confirmed above.
136273
function prepare_host_binfmt_qemu_cross() {
137274
local failed_binfmt_modprobe=0
@@ -179,6 +316,36 @@ function prepare_host_binfmt_qemu_cross() {
179316
continue
180317
fi
181318

319+
# Skip wanted_arch=arm preparation entirely when this build doesn't
320+
# target armhf. The Apple-Silicon helper below mutates global kernel
321+
# binfmt_misc/qemu-arm state, which is irrelevant for cross builds
322+
# targeting amd64/riscv64/etc and would needlessly race with any
323+
# concurrent native-armhf owner on the host.
324+
if [[ "${host_arch}" == "aarch64" && "${wanted_arch}" == "arm" && "${ARCH}" != "armhf" ]]; then
325+
display_alert "binfmt qemu-arm" "skipped: target ARCH=${ARCH} doesn't need qemu-arm" "debug"
326+
continue
327+
fi
328+
329+
# Early native-armhf claim. On aarch64 host targeting armhf, try to
330+
# become or join the native-armhf-via-binfmt_elf owner BEFORE the
331+
# Apple-Silicon special branch below. The latter mutates global kernel
332+
# binfmt_misc state via update-binfmts, which races against another
333+
# concurrent build that holds qemu-arm in its disabled state. Joining
334+
# (or becoming first) keeps qemu-arm disabled coherently and lets
335+
# /usr/share/binfmts/qemu-arm absence in this container be a non-issue.
336+
if [[ "${host_arch}" == "aarch64" && "${wanted_arch}" == "arm" && "${ARCH}" == "armhf" ]]; then
337+
if _native_armhf_setup_binfmt_elf; then
338+
display_alert "binfmt qemu-arm" "skipped: native armhf via binfmt_elf is active" "cachehit"
339+
continue
340+
fi
341+
# qemu-arm disabled means another builder native-owns it; route
342+
# through the guard so we fail fast instead of clobbering.
343+
if [[ "$(_native_armhf_observe_qemu_arm_state)" == "0" ]]; then
344+
prepare_host_binfmt_qemu_cross_arm64_host_armhf_target
345+
continue
346+
fi
347+
fi
348+
182349
if [[ ! -e "/proc/sys/fs/binfmt_misc/qemu-${wanted_arch}" || ! -e "/usr/share/binfmts/qemu-${wanted_arch}" ]]; then
183350
display_alert "Updating binfmts" "update-binfmts --enable qemu-${wanted_arch}" "debug"
184351

@@ -193,6 +360,22 @@ function prepare_host_binfmt_qemu_cross() {
193360
}
194361

195362
function prepare_host_binfmt_qemu_cross_arm64_host_armhf_target() {
363+
# Conservative guard: refuse to mutate global qemu-arm state if it is
364+
# observably disabled. That state means another concurrent armbian build
365+
# is using the native-armhf path and we'd clobber it by re-enabling
366+
# qemu-arm here. (Reachable only via NATIVE_ARMHF_ON_ARM64=no/never/
367+
# disabled opt-out — otherwise _native_armhf_setup_binfmt_elf would have
368+
# already exit'd with the "concurrent native-armhf build" error before
369+
# we got here.)
370+
if [[ -e /proc/sys/fs/binfmt_misc/qemu-arm ]]; then
371+
declare observed_qemu_arm
372+
observed_qemu_arm="$(_native_armhf_observe_qemu_arm_state)"
373+
if [[ "${observed_qemu_arm}" == "0" ]]; then
374+
display_alert "binfmt qemu-arm" "registered but observably disabled — another concurrent build likely holds native-armhf; refusing to clobber" "err"
375+
exit_with_error "qemu-arm globally disabled by another concurrent build; cannot safely re-enable. Wait for it to finish or run on a separate host."
376+
fi
377+
fi
378+
196379
display_alert "Trying to update binfmts - aarch64 mostly does 32-bit sans emulation, but Apple said no" "update-binfmts --enable qemu-${wanted_arch}" "debug"
197380
run_host_command_logged update-binfmts --enable "qemu-${wanted_arch}" "&>" "/dev/null" "||" "true" # don't fail nor produce output, which can be misleading.
198381

@@ -201,12 +384,13 @@ function prepare_host_binfmt_qemu_cross_arm64_host_armhf_target() {
201384
run_host_command_logged arch-test "||" true
202385
fi
203386

204-
# to check, we use arch-test; if will return 0 if _either_ the host can natively run armhf, or if qemu-arm is correctly working.
205-
if arch-test arm; then
387+
# to check, we use arch-test; will return 0 if _either_ the host can natively run armhf, or if qemu-arm is correctly working.
388+
# Use armhf (Debian-arch) rather than arm to match the build target and the post-disable check in _native_armhf_setup_binfmt_elf.
389+
if arch-test armhf; then
206390
display_alert "Host can run armhf natively or emulation is correctly setup already" "no need to enable qemu-arm" "debug"
207391
else
208392
display_alert "arm64 host can't run armhf natively" "importing enabling qemu-arm" "debug"
209-
cat <<-BINFMT_ARM_MAGIC >/usr/share/binfmts/qemu-arm
393+
cat <<- BINFMT_ARM_MAGIC > /usr/share/binfmts/qemu-arm
210394
package qemu-user-static
211395
interpreter /usr/bin/qemu-arm-static
212396
magic \x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00
@@ -221,7 +405,7 @@ function prepare_host_binfmt_qemu_cross_arm64_host_armhf_target() {
221405

222406
# Test again using arch-test.
223407
display_alert "Checking if arm 32-bit emulation on arm64 works after enabling" "qemu-arm emulation" "info"
224-
run_host_command_logged arch-test arm
408+
run_host_command_logged arch-test armhf
225409
display_alert "arm 32-bit emulation on arm64" "has been correctly setup" "cachehit"
226410
fi
227411
}

lib/functions/rootfs/rootfs-create.sh

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function create_new_rootfs_cache_via_debootstrap() {
6161
local debootstrap_apt_mirror="http://localhost:3142/${APT_MIRROR}"
6262
acng_check_status_or_restart
6363
;;
64-
no) ;& # do nothing, fallthrough
64+
no) ;& # do nothing, fallthrough
6565
"")
6666
: # still do nothing
6767
;; # stop falling
@@ -139,9 +139,14 @@ function create_new_rootfs_cache_via_debootstrap() {
139139

140140
skip_target_check="yes" local_apt_deb_cache_prepare "for mmdebstrap" # just for size reference in logs
141141

142-
143142
[[ ! -f "${SDCARD}/bin/bash" ]] && exit_with_error "mmdebstrap did not produce /bin/bash"
144143

144+
# mmdebstrap done, libc/ld-linux are in ${SDCARD}. Disable qemu-arm in binfmt_misc
145+
# so subsequent chroot apt-get/dpkg/customize calls fall through to kernel binfmt_elf
146+
# and run 32-bit ARM ELF natively via CONFIG_COMPAT. mmdebstrap above used qemu-arm
147+
# because its cross-arch path requires that registration to be present.
148+
_native_armhf_setup_binfmt_elf || true
149+
145150
# Done with mmdebstrap. Clean-up its litterbox.
146151
display_alert "Cleaning up after mmdebstrap" "mmdebstrap cleanup" "info"
147152
run_host_command_logged rm -rf "${SDCARD}/var/cache/apt" "${SDCARD}/var/lib/apt/lists"

0 commit comments

Comments
 (0)