Skip to content

Commit eab2ab1

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, not running in container) 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. 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. 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 eab2ab1

3 files changed

Lines changed: 108 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: 97 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,83 @@ 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 inside a container.
142+
# Out-of-scope cases stay completely silent — no alerts, no syscalls past the guards.
143+
[[ "${ARCH}" == "armhf" ]] || return 1
144+
[[ "$(arch)" == "aarch64" ]] || return 1
145+
[[ "${ARMBIAN_RUNNING_IN_CONTAINER}" == "yes" ]] || return 1
146+
147+
# In-scope from here on: armhf-on-aarch64 in a container, killswitch off — log the
148+
# capability check result so the user sees whether the acceleration kicked in.
149+
150+
# Pre-flight: 32-bit ARM execution must actually work on this host. Fails when the
151+
# kernel lacks CONFIG_COMPAT/COMPAT_VDSO (extensions/arm64-compat-vdso, see PR #9284),
152+
# or when the CPU itself lacks 32-bit user mode (Apple Silicon, ARMv9-only cores).
153+
if ! arch-test armhf > /dev/null 2>&1; then
154+
display_alert "Native armhf via binfmt_elf" "32-bit ARM execution not supported on this kernel/host; falling back to qemu-arm-static emulation" "info"
155+
return 1
156+
fi
157+
158+
# Need a registered, enabled qemu-arm handler to disable; otherwise the kernel will
159+
# already fall through to binfmt_elf — mark active and report the no-op outcome.
160+
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-arm ]]; then
161+
display_alert "Native armhf via binfmt_elf" "qemu-arm not registered in binfmt_misc; native armhf already in effect" "info"
162+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
163+
return 0
164+
fi
165+
166+
if ! head -1 /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null | grep -q '^enabled'; then
167+
display_alert "Native armhf via binfmt_elf" "qemu-arm already disabled in binfmt_misc; native armhf already in effect" "info"
168+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
169+
return 0
170+
fi
171+
172+
# Disable qemu-arm in the (shared) binfmt_misc; the kernel will fall through to
173+
# binfmt_elf for 32-bit ARM ELF and execute natively via CONFIG_COMPAT.
174+
if ! echo 0 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null; then
175+
display_alert "Native armhf via binfmt_elf" "could not disable qemu-arm (no CAP_SYS_ADMIN?); falling back to qemu-arm-static emulation" "wrn"
176+
return 1
177+
fi
178+
179+
# Register cleanup so qemu-arm gets re-enabled on build exit (success or fail).
180+
add_cleanup_handler trap_handler_native_armhf_restore_qemu_arm
181+
182+
display_alert "Native armhf via binfmt_elf" "kernel $(uname -r), aarch64 host with COMPAT_VDSO; qemu-arm disabled, kernel binfmt_elf takes over" "info"
183+
declare -g ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF=yes
184+
return 0
185+
}
186+
187+
# Cleanup handler: re-enable qemu-arm after build (whether success or failure).
188+
function trap_handler_native_armhf_restore_qemu_arm() {
189+
[[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]] || return 0
190+
[[ -e /proc/sys/fs/binfmt_misc/qemu-arm ]] || return 0
191+
echo 1 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null || true
192+
display_alert "Native armhf via binfmt_elf" "qemu-arm re-enabled in binfmt_misc" "info"
193+
}
194+
100195
# The actual binfmt manipulations when cross-build is confirmed above.
101196
function prepare_host_binfmt_qemu_cross() {
102197
local failed_binfmt_modprobe=0
@@ -171,7 +266,7 @@ function prepare_host_binfmt_qemu_cross_arm64_host_armhf_target() {
171266
display_alert "Host can run armhf natively or emulation is correctly setup already" "no need to enable qemu-arm" "debug"
172267
else
173268
display_alert "arm64 host can't run armhf natively" "importing enabling qemu-arm" "debug"
174-
cat <<-BINFMT_ARM_MAGIC >/usr/share/binfmts/qemu-arm
269+
cat <<- BINFMT_ARM_MAGIC > /usr/share/binfmts/qemu-arm
175270
package qemu-user-static
176271
interpreter /usr/bin/qemu-arm-static
177272
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)