Skip to content

Commit 6138bd0

Browse files
committed
fix(rootfs/qemu-static): close codex P1/P2 races + symmetrize K state + clarify ordering invariant
Addresses codex review feedback on PR #113 / armbian#9769. N-builder entry race: EX-NB → release → re-take SH split let a K-builder slip in between observation and the state-check write, silently disabling qemu-arm while K relied on it. Hold EX during state check + echo 0, then atomically downgrade EX→SH on the same fd via Linux flock(2)'s flc_lock transition. SH waiters are granted only after our SH is in place and qemu-arm is already 0. K-builder symmetric state check: after acquiring the emulation-mode SH-NB, verify state=1; if 0, concurrent N-builders have disabled qemu-arm and the killswitch cannot honor its contract — bail with a clear error rather than running a broken anchor. Joiner state-recheck: in the state=0 / EX-NB-fail path, between observing state=0 and acquiring SH the last live N-builder may release SH and run the restorer (echo 1). Recheck state after SH and bail back to emulation if state has flipped — otherwise we'd skip qemu deploy while binfmt actually routes to qemu, and chroot exec fails because the qemu binary is not in the chroot. Killswitch gated by ARCH/host: NATIVE_ARMHF_ON_ARM64=no only meaningful for armhf builds on aarch64 hosts. Moved idempotent + ARCH/host gates above killswitch evaluation so unrelated builds (amd64/arm64 targets, x86 hosts) skip qemu-arm entirely. Early call site removed: prepare_host_binfmt_qemu_cross previously called _native_armhf_setup_binfmt_elf BEFORE mmdebstrap, leaving the bootstrap phase without qemu registration on a chroot that has no libc/ld-linux yet — armhf maintainer-script exec fails. Native activation stays where it belongs: post-mmdebstrap call sites in rootfs-create.sh and rootfs-image.sh. Cleanup-ordering invariant comment was inverted. add_cleanup_handler PREPENDS, so later registrations run FIRST. The invariant survives because setup is called before mount_chroot in both call sites; the comment now states the actual mechanism and documents the failure mode if a future call site inverts the order. Assisted-by: Claude:claude-opus-4.7
1 parent 5cab79e commit 6138bd0

1 file changed

Lines changed: 85 additions & 50 deletions

File tree

lib/functions/rootfs/qemu-static.sh

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ function _native_armhf_observe_qemu_arm_state() {
177177
}
178178

179179
function _native_armhf_setup_binfmt_elf() {
180+
# Idempotent: callers in rootfs-create.sh and rootfs-image.sh invoke this
181+
# from both the cache-miss and cache-hit paths.
182+
[[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]] && return 0
183+
184+
# Gate by ARCH/host first: native armhf binfmt_elf only applies to armhf
185+
# builds on aarch64 hosts. Unrelated builds (amd64/arm64 targets, x86 host)
186+
# must not touch qemu-arm at all — neither to disable nor to anchor an
187+
# emulation hold via NATIVE_ARMHF_ON_ARM64=no. Killswitch evaluation lives
188+
# below this gate.
189+
[[ "${ARCH}" == "armhf" ]] || return 1
190+
[[ "$(arch)" == "aarch64" ]] || return 1
191+
180192
declare killswitch=no
181193
case "${NATIVE_ARMHF_ON_ARM64:-auto}" in
182194
no | never | disabled) killswitch=yes ;;
@@ -190,21 +202,26 @@ function _native_armhf_setup_binfmt_elf() {
190202
if [[ -e /proc/sys/fs/binfmt_misc/qemu-arm ]] &&
191203
{ exec {_native_armhf_emul_lock_fd}< /proc/sys/fs/binfmt_misc/qemu-arm; } 2> /dev/null &&
192204
flock -s -n "${_native_armhf_emul_lock_fd}"; then
193-
add_cleanup_handler trap_handler_native_armhf_release_emul_lock
194-
display_alert "Native armhf via binfmt_elf" "killswitch active; emulation-mode SH-lock acquired (blocks concurrent native-armhf switchover)" "info"
205+
# K-builder invariant: qemu-arm must be enabled. If state=0 here,
206+
# concurrent N-builders have already disabled it — the emulation
207+
# anchor would be a lie and the K-builder's chroot would fail to
208+
# exec qemu-arm-static. Bail rather than silently coexist.
209+
if [[ "$(_native_armhf_observe_qemu_arm_state)" == "1" ]]; then
210+
add_cleanup_handler trap_handler_native_armhf_release_emul_lock
211+
display_alert "Native armhf via binfmt_elf" "killswitch active; emulation-mode SH-lock acquired (blocks concurrent native-armhf switchover)" "info"
212+
else
213+
exec {_native_armhf_emul_lock_fd}>&-
214+
unset _native_armhf_emul_lock_fd
215+
display_alert "Native armhf via binfmt_elf" "killswitch requested but qemu-arm already disabled by concurrent native-armhf builders" "err"
216+
exit_with_error "cannot honor NATIVE_ARMHF_ON_ARM64=no: concurrent native-armhf builders have disabled qemu-arm. Wait for them to finish or run on a separate host."
217+
fi
195218
else
196219
[[ -n "${_native_armhf_emul_lock_fd:-}" ]] && exec {_native_armhf_emul_lock_fd}>&-
197220
unset _native_armhf_emul_lock_fd
198221
fi
199222
return 1
200223
fi
201224

202-
# Idempotent: callers in rootfs-create.sh and rootfs-image.sh invoke this
203-
# from both the cache-miss and cache-hit paths.
204-
[[ "${ARMBIAN_NATIVE_ARMHF_VIA_BINFMT_ELF:-no}" == "yes" ]] && return 0
205-
[[ "${ARCH}" == "armhf" ]] || return 1
206-
[[ "$(arch)" == "aarch64" ]] || return 1
207-
208225
# Pre-flight is unreliable when qemu-arm is enabled (it interprets the
209226
# arch-test stub); the authoritative check is post-disable below.
210227
if ! arch-test armhf > /dev/null 2>&1; then
@@ -228,32 +245,56 @@ function _native_armhf_setup_binfmt_elf() {
228245
return 1
229246
fi
230247

231-
# EX-NB probe BEFORE acquiring our own SH (otherwise our own SH would
232-
# block the probe — flock counts per-OFD, our two fds on the same file
233-
# would interfere). EX-NB success means zero other SH-holders. Failure
234-
# with state="1" identifies a killswitch K-builder holding the
235-
# emulation-mode anchor — switching qemu-arm off would corrupt their
236-
# chroot exec routing. Failure with state="0" is a peer N-builder in
237-
# joiner-territory.
248+
# Take EX-NB first to determine whether we are alone or joining. While
249+
# we hold EX, no SH/EX can enter, so state observation and the disable
250+
# write are atomic w.r.t. concurrent K- or N-builders. Then downgrade
251+
# EX → SH on the same fd; Linux flock(2) performs the transition under
252+
# the inode's flc_lock, granting pending SH waiters only after our SH
253+
# is in place — by which time qemu-arm is already 0 and any joiner
254+
# sees joiner-territory (state=0).
255+
#
256+
# EX-NB failure means someone else holds SH. State distinguishes:
257+
# state=1 → killswitch K-builder holds the emulation-mode anchor;
258+
# switching qemu-arm off would corrupt its chroot exec.
259+
# state=0 → peer N-builder; we are a joiner, take SH without writing.
238260
if flock -x -n "${_native_armhf_lock_fd}"; then
239-
flock -u "${_native_armhf_lock_fd}"
261+
if [[ "$(_native_armhf_observe_qemu_arm_state)" == "1" ]]; then
262+
if ! echo 0 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null; then
263+
display_alert "Native armhf via binfmt_elf" "could not disable qemu-arm (no CAP_SYS_ADMIN?); falling back to qemu-arm-static emulation" "wrn"
264+
exec {_native_armhf_lock_fd}>&-
265+
unset _native_armhf_lock_fd
266+
return 1
267+
fi
268+
fi
269+
if ! flock -s "${_native_armhf_lock_fd}"; then
270+
display_alert "Native armhf via binfmt_elf" "could not downgrade EX→SH on binfmt_misc/qemu-arm; falling back to qemu emulation" "wrn"
271+
exec {_native_armhf_lock_fd}>&-
272+
unset _native_armhf_lock_fd
273+
return 1
274+
fi
240275
elif [[ "$(_native_armhf_observe_qemu_arm_state)" == "1" ]]; then
241276
exec {_native_armhf_lock_fd}>&-
242277
unset _native_armhf_lock_fd
243278
display_alert "Native armhf via binfmt_elf" "concurrent build holds emulation-mode lock (NATIVE_ARMHF_ON_ARM64=no)" "err"
244279
exit_with_error "cannot enable native armhf: concurrent build with NATIVE_ARMHF_ON_ARM64=no holds emulation lock. Wait for it to finish or run on a separate host."
245-
fi
246-
247-
if ! flock -s -w 30 "${_native_armhf_lock_fd}"; then
248-
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"
249-
exec {_native_armhf_lock_fd}>&-
250-
unset _native_armhf_lock_fd
251-
return 1
252-
fi
253-
254-
if [[ "$(_native_armhf_observe_qemu_arm_state)" == "1" ]]; then
255-
if ! echo 0 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null; then
256-
display_alert "Native armhf via binfmt_elf" "could not disable qemu-arm (no CAP_SYS_ADMIN?); falling back to qemu-arm-static emulation" "wrn"
280+
else
281+
if ! flock -s -w 30 "${_native_armhf_lock_fd}"; then
282+
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"
283+
exec {_native_armhf_lock_fd}>&-
284+
unset _native_armhf_lock_fd
285+
return 1
286+
fi
287+
# Joiner state-recheck. Between observing state=0 above and acquiring
288+
# SH here, the last live N-builder may have released its SH and run
289+
# trap_handler_native_armhf_restore_qemu_arm, taking EX-NB and writing
290+
# qemu-arm back to "1". Our blocking SH then gets granted on an empty
291+
# lock — state=1 now. Continuing as a native joiner would skip the
292+
# qemu-arm-static deploy while binfmt actually routes to qemu, and
293+
# chroot exec fails because the qemu binary isn't in the chroot.
294+
# Release SH and fall back to the qemu emulation path; the caller
295+
# (prepare_host_binfmt_qemu_cross) will register qemu-arm normally.
296+
if [[ "$(_native_armhf_observe_qemu_arm_state)" != "0" ]]; then
297+
display_alert "Native armhf via binfmt_elf" "joiner lost race to last-out restorer; qemu-arm re-enabled, falling back to emulation" "wrn"
257298
exec {_native_armhf_lock_fd}>&-
258299
unset _native_armhf_lock_fd
259300
return 1
@@ -290,9 +331,16 @@ function trap_handler_native_armhf_release_emul_lock() {
290331
# the build's subshells (umount / SDCARD / MOUNT teardown). BSD flock is per-
291332
# OFD, so a forked subshell inheriting our SH-fd shares the same lock entry —
292333
# the LOCK_EX-NB probe below would falsely block on the inherited fd of a
293-
# still-alive child. add_cleanup_handler runs in registration order; the
294-
# umount handlers register first, so by the time we run, the docker container
295-
# is dead and its child-tree with it. Verified empirically (SIGINT mid-chroot).
334+
# still-alive child. add_cleanup_handler PREPENDS to the handler list and
335+
# run_cleanup_handlers iterates it in order, so later-registered handlers
336+
# run first. Our setup is invoked before mount_chroot in both call sites
337+
# (rootfs-create.sh and rootfs-image.sh), so mount_chroot's umount handlers
338+
# are registered after ours, sit at the head of the list, and execute before
339+
# us — by the time we run, the chroot is unmounted and inheriting subshells
340+
# have exited. Verified empirically (SIGINT mid-chroot). If a future call
341+
# site registers our setup AFTER mount_chroot, this invariant inverts and
342+
# the EX-NB probe will spuriously fail; the documented escape hatches are
343+
# POSIX F_SETLK on a helper fd or explicit descendant-kill.
296344
function trap_handler_native_armhf_restore_qemu_arm() {
297345
[[ -n "${_native_armhf_lock_fd:-}" ]] || return 0
298346
exec {_native_armhf_lock_fd}>&-
@@ -369,25 +417,12 @@ function prepare_host_binfmt_qemu_cross() {
369417
continue
370418
fi
371419

372-
# Early native-armhf claim. On aarch64 host targeting armhf, try to
373-
# become or join the native-armhf-via-binfmt_elf owner BEFORE the
374-
# Apple-Silicon special branch below. The latter mutates global kernel
375-
# binfmt_misc state via update-binfmts, which races against another
376-
# concurrent build that holds qemu-arm in its disabled state. Joining
377-
# (or becoming first) keeps qemu-arm disabled coherently and lets
378-
# /usr/share/binfmts/qemu-arm absence in this container be a non-issue.
379-
if [[ "${host_arch}" == "aarch64" && "${wanted_arch}" == "arm" && "${ARCH}" == "armhf" ]]; then
380-
if _native_armhf_setup_binfmt_elf; then
381-
display_alert "binfmt qemu-arm" "skipped: native armhf via binfmt_elf is active" "cachehit"
382-
continue
383-
fi
384-
# qemu-arm disabled means another builder native-owns it; route
385-
# through the guard so we fail fast instead of clobbering.
386-
if [[ "$(_native_armhf_observe_qemu_arm_state)" == "0" ]]; then
387-
prepare_host_binfmt_qemu_cross_arm64_host_armhf_target
388-
continue
389-
fi
390-
fi
420+
# Native armhf activation deferred. _native_armhf_setup_binfmt_elf is
421+
# called AFTER mmdebstrap (rootfs-create.sh / rootfs-image.sh) — calling
422+
# it here, before the chroot has libc/ld-linux-armhf.so.3, would leave
423+
# bootstrap with no qemu registration and no native interpreter inside
424+
# the chroot, breaking armhf maintainer-script execution. Let prepare_host
425+
# register qemu-arm normally; the post-mmdebstrap call switches to native.
391426

392427
if [[ ! -e "/proc/sys/fs/binfmt_misc/qemu-${wanted_arch}" || ! -e "/usr/share/binfmts/qemu-${wanted_arch}" ]]; then
393428
display_alert "Updating binfmts" "update-binfmts --enable qemu-${wanted_arch}" "debug"

0 commit comments

Comments
 (0)