Skip to content

Commit df7aaf9

Browse files
committed
feat(rootfs): native armhf on aarch64 host via runtime-disable of qemu-arm
Continues #9284 (arm64-compat-vdso extension + custom_kernel_make_params hook). That PR enabled COMPAT_VDSO in arm64 kernels and wired CROSS_COMPILE_COMPAT through the build; this PR uses the resulting 32-bit-capable kernels to run 32-bit ARM ELF directly instead of through qemu-arm emulation in the image-build chroot work. Disable qemu-arm in /proc/sys/fs/binfmt_misc for the duration of the image-build so 32-bit ARM ELF falls through to kernel binfmt_elf and runs natively via CONFIG_COMPAT — typically ~12x faster than qemu-arm emulation on common ARM cores (Cortex-A53/A55/A72/A73/A76). A cleanup handler re-enables qemu-arm on build exit (success or failure). Empirical: helios4 BUILD_MINIMAL armhf rootfs cache-miss build on RK3568 (Cortex-A55) drops from 60:35 to 19:27 — 3.12x speedup; same build with rootfs cache-hit drops to 6:38 (~5x). Activation point is delayed to AFTER mmdebstrap (in create_new_rootfs_cache_via_debootstrap, right past the bash-presence check) and also at build_rootfs_and_image entry for the rootfs-cache hit path. Earlier activation breaks mmdebstrap because its cross-arch machinery requires a working qemu-arm in binfmt_misc and the chroot is not yet populated for native exec. Out-of-scope cases (target arch != armhf, host arch != aarch64) return silently with no log noise. In-scope cases log the capability-check outcome at info level: native enabled, already-effective no-op, or 32-bit ARM execution unsupported (no COMPAT_VDSO in kernel, Apple Silicon, ARMv9-only cores) — falling back to qemu-arm-static emulation. Both docker and native (no-container) builds qualify; the killswitch is the user-facing opt-out. Killswitch: NATIVE_ARMHF_ON_ARM64=no disables this path entirely, before any detection runs — useful on shared build farms or whenever opting out is desired (synonyms 'never' and 'disabled' are accepted by the parser). Sets ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes when active so that deploy_qemu_binary_to_chroot / undeploy_qemu_binary_from_chroot skip copying qemu-arm-static into the chroot during image-stage (the kernel handles the ELF directly). Caveat: the container shares /proc/sys/fs/binfmt_misc with the host's init user-ns (Armbian's image-build container has CAP_SYS_ADMIN regardless of DOCKER_PRIVILEGED), so disabling qemu-arm affects the host's other concurrent processes for the duration of the build. Same applies to native (no-container) builds where the host IS the build host. Acceptable for single-user builders, problematic for shared build farms where NATIVE_ARMHF_ON_ARM64=no is the right setting. Assisted-by: Claude:claude-opus-4.7 Signed-off-by: Igor Velkov <325961+iav@users.noreply.github.com>
1 parent 3ce7dec commit df7aaf9

3 files changed

Lines changed: 130 additions & 2 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: 119 additions & 2 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
declare src_host="/usr/bin/${QEMU_BINARY}"
2128
declare dst_target="${chroot_target}/usr/bin/${QEMU_BINARY}"
2229
declare dst_target_bkp="${dst_target}.armbian.orig"
@@ -48,8 +55,19 @@ function undeploy_qemu_binary_from_chroot() {
4855
declare dst_target="${chroot_target}/usr/bin/${QEMU_BINARY}"
4956
declare dst_target_bkp="${dst_target}.armbian.orig"
5057

51-
# Check the binary we deployed is there. If not, panic, as we've lost control.
58+
# Check the binary we deployed is there. Two reasons it might be missing:
59+
# 1. ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF was active when the matching deploy
60+
# ran, so nothing was copied — graceful no-op.
61+
# 2. Genuine state loss — panic, we lost control.
62+
# We must NOT skip the removal solely on the native-armhf flag, because deploy
63+
# may have run before that flag was set (rootfs-create deploys at line 134,
64+
# native-armhf flips at line 149); skipping the undeploy in that case leaks
65+
# the host's qemu-arm-static into the rootfs cache tarball.
5266
if [[ ! -f "${dst_target}" ]]; then
67+
if [[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]]; then
68+
display_alert "Native armhf via binfmt_elf" "no qemu binary to remove during ${caller}" "debug"
69+
return 0
70+
fi
5371
exit_with_error "Missing qemu binary during undeploy_qemu_binary_from_chroot from ${caller}"
5472
fi
5573

@@ -97,6 +115,105 @@ function prepare_host_binfmt_qemu() {
97115
return 0
98116
}
99117

118+
# Native armhf on aarch64 host via runtime-disable of qemu-arm in binfmt_misc.
119+
# With qemu-arm disabled, 32-bit ARM ELF falls through to kernel binfmt_elf, which
120+
# executes it natively via CONFIG_COMPAT (~12x faster than qemu-arm emulation on common
121+
# ARM cores). Under docker --privileged, /proc/sys/fs/binfmt_misc is shared with the
122+
# host (init user-ns), so this WILL affect host's other concurrent processes for the
123+
# duration of the build — acceptable for single-user builders, problematic for shared
124+
# build farms. Setting NATIVE_ARMHF_ON_ARM64=no (or never/disabled) disables the entire
125+
# code path before any detection runs.
126+
#
127+
# Returns 0 on success (caller sets ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes); 1 if the
128+
# preconditions are not met, the user opted out, or disable failed — caller falls back
129+
# to the qemu-arm path. Cleanup (re-enable qemu-arm) is registered via add_cleanup_handler.
130+
function _native_armhf_setup_binfmt_elf() {
131+
# Killswitch — accept no/never/disabled as synonyms; bail before any detection runs.
132+
case "${NATIVE_ARMHF_ON_ARM64:-auto}" in
133+
no | never | disabled) return 1 ;;
134+
esac
135+
136+
# Idempotent: callers may invoke this both after debootstrap (in
137+
# create_new_rootfs_cache_via_debootstrap) and after rootfs-cache extract (in
138+
# build_rootfs_and_image, for cache-hit). Once active, return success.
139+
[[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]] && return 0
140+
141+
# This optimisation only applies to armhf-target on aarch64-host. Out-of-scope cases
142+
# stay completely silent — no alerts, no syscalls past the guards. Both docker and
143+
# native builds qualify; the killswitch above is the user-facing opt-out.
144+
[[ "${ARCH}" == "armhf" ]] || return 1
145+
[[ "$(arch)" == "aarch64" ]] || return 1
146+
147+
# In-scope from here on: armhf-on-aarch64, killswitch off — log the capability
148+
# check result so the user sees whether the acceleration kicked in.
149+
150+
# Pre-flight: cheap reject of obviously-wrong cases. NOT trustworthy alone —
151+
# arch-test resolves through qemu-arm in binfmt_misc when qemu-arm is registered,
152+
# so it returns 0 even when actual native execution would fail. The authoritative
153+
# check is the post-disable arch-test below.
154+
if ! arch-test armhf > /dev/null 2>&1; then
155+
display_alert "Native armhf via binfmt_elf" "arch-test pre-flight failed; falling back to qemu-arm-static emulation" "info"
156+
return 1
157+
fi
158+
159+
# Need a registered, enabled qemu-arm handler to disable; otherwise the kernel will
160+
# already fall through to binfmt_elf — mark active and report the no-op outcome.
161+
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-arm ]]; then
162+
display_alert "Native armhf via binfmt_elf" "qemu-arm not registered in binfmt_misc; native armhf already in effect" "info"
163+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
164+
return 0
165+
fi
166+
167+
if ! head -1 /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null | grep -q '^enabled'; then
168+
display_alert "Native armhf via binfmt_elf" "qemu-arm already disabled in binfmt_misc; native armhf already in effect" "info"
169+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
170+
return 0
171+
fi
172+
173+
# Register cleanup BEFORE the destructive write so an interrupt mid-setup
174+
# (SIGINT / set -e) still re-enables qemu-arm on the host's shared binfmt_misc.
175+
# State machine: pending (handler must restore) → yes (success) or no (write
176+
# failed, nothing to restore).
177+
declare -g _native_armhf_disabled_qemu_arm=pending
178+
add_cleanup_handler trap_handler_native_armhf_restore_qemu_arm
179+
180+
# Disable qemu-arm in the (shared) binfmt_misc; the kernel will fall through to
181+
# binfmt_elf for 32-bit ARM ELF and execute natively via CONFIG_COMPAT.
182+
if ! echo 0 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null; then
183+
declare -g _native_armhf_disabled_qemu_arm=no
184+
display_alert "Native armhf via binfmt_elf" "could not disable qemu-arm (no CAP_SYS_ADMIN?); falling back to qemu-arm-static emulation" "wrn"
185+
return 1
186+
fi
187+
declare -g _native_armhf_disabled_qemu_arm=yes
188+
189+
# Authoritative check: re-run arch-test with qemu-arm now disabled. This
190+
# reflects what the chroot exec will actually face. arch-test passes pre-disable
191+
# even on hosts without real native AArch32 execution because qemu-arm interprets
192+
# its stub — typically when the host kernel was built without COMPAT_VDSO
193+
# (extensions/arm64-compat-vdso, see PR #9284).
194+
if ! arch-test armhf > /dev/null 2>&1; then
195+
display_alert "Native armhf via binfmt_elf" "post-disable verification failed (host kernel lacks COMPAT_VDSO — see extensions/arm64-compat-vdso); restoring qemu-arm and falling back to emulation" "wrn"
196+
echo 1 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null || true
197+
declare -g _native_armhf_disabled_qemu_arm=no
198+
return 1
199+
fi
200+
201+
display_alert "Native armhf via binfmt_elf" "kernel $(uname -r), aarch64 host with COMPAT_VDSO; qemu-arm disabled, kernel binfmt_elf takes over" "info"
202+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
203+
return 0
204+
}
205+
206+
# Cleanup handler: re-enable qemu-arm after build (whether success or failure).
207+
# Idempotent — writing 1 to an already-enabled entry is a no-op for binfmt_misc.
208+
function trap_handler_native_armhf_restore_qemu_arm() {
209+
# Skip only when we know we never wrote (write itself failed); pending and yes
210+
# both require restore.
211+
[[ "${_native_armhf_disabled_qemu_arm:-no}" == "no" ]] && return 0
212+
[[ -e /proc/sys/fs/binfmt_misc/qemu-arm ]] || return 0
213+
echo 1 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null || true
214+
display_alert "Native armhf via binfmt_elf" "qemu-arm re-enabled in binfmt_misc" "info"
215+
}
216+
100217
# The actual binfmt manipulations when cross-build is confirmed above.
101218
function prepare_host_binfmt_qemu_cross() {
102219
local failed_binfmt_modprobe=0
@@ -171,7 +288,7 @@ function prepare_host_binfmt_qemu_cross_arm64_host_armhf_target() {
171288
display_alert "Host can run armhf natively or emulation is correctly setup already" "no need to enable qemu-arm" "debug"
172289
else
173290
display_alert "arm64 host can't run armhf natively" "importing enabling qemu-arm" "debug"
174-
cat <<-BINFMT_ARM_MAGIC >/usr/share/binfmts/qemu-arm
291+
cat <<- BINFMT_ARM_MAGIC > /usr/share/binfmts/qemu-arm
175292
package qemu-user-static
176293
interpreter /usr/bin/qemu-arm-static
177294
magic \x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00

lib/functions/rootfs/rootfs-create.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ function create_new_rootfs_cache_via_debootstrap() {
142142

143143
[[ ! -f "${SDCARD}/bin/bash" ]] && exit_with_error "mmdebstrap did not produce /bin/bash"
144144

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

0 commit comments

Comments
 (0)