Skip to content

Commit c4a1f8b

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 (success path): 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. K-builder SH-NB → blocking SH-with-timeout (failure path): the old non-blocking flock -s -n would fail when an N-builder briefly held EX (probe / disable / EX→SH downgrade window). Falling through to qemu emulation without holding the SH anchor let the peer N complete its transition with qemu-arm=0, breaking K's chroot exec routing. Now block up to 30s; if SH lands, re-check state — anchor on state=1, exit_with_error on state=0. Sub-millisecond peer transitions resolve well within the window; persistent contention falls back without anchor (preserves old behavior for degenerate cases). 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 c4a1f8b

1 file changed

Lines changed: 105 additions & 59 deletions

File tree

lib/functions/rootfs/qemu-static.sh

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -177,34 +177,62 @@ 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 ;;
183195
esac
184196

185-
# Killswitch path: still take SH-lock on qemu-arm so concurrent
186-
# native-armhf builders detect us via EX-NB probe and refuse to switch
187-
# qemu-arm off. Without this anchor an N-builder arriving mid-K-chroot
188-
# would echo 0 and silently break K's qemu-arm-static routing.
197+
# Killswitch path: take SH-lock on qemu-arm so concurrent native-armhf
198+
# builders detect us via EX-NB probe and refuse to switch qemu-arm off.
199+
# Without this anchor an N-builder arriving mid-K-chroot would echo 0 and
200+
# silently break K's qemu-arm-static routing.
189201
if [[ "${killswitch}" == "yes" ]]; then
190-
if [[ -e /proc/sys/fs/binfmt_misc/qemu-arm ]] &&
191-
{ exec {_native_armhf_emul_lock_fd}< /proc/sys/fs/binfmt_misc/qemu-arm; } 2> /dev/null &&
192-
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"
195-
else
196-
[[ -n "${_native_armhf_emul_lock_fd:-}" ]] && exec {_native_armhf_emul_lock_fd}>&-
202+
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-arm ]]; then
203+
display_alert "Native armhf via binfmt_elf" "killswitch requested but qemu-arm not registered; cannot anchor emulation" "wrn"
204+
return 1
205+
fi
206+
if ! { exec {_native_armhf_emul_lock_fd}< /proc/sys/fs/binfmt_misc/qemu-arm; } 2> /dev/null; then
207+
display_alert "Native armhf via binfmt_elf" "cannot open binfmt_misc/qemu-arm; killswitch cannot anchor" "wrn"
208+
return 1
209+
fi
210+
# Blocking SH with timeout: an N-builder's EX hold (probe / state-write
211+
# / EX→SH downgrade window) is sub-millisecond, but we may collide. A
212+
# non-blocking SH would fall through to emulation without an anchor and
213+
# let the peer complete its EX→SH transition with qemu-arm=0, breaking
214+
# our killswitch chroot exec routing later. Wait briefly for the peer's
215+
# transition to settle.
216+
if ! flock -s -w 30 "${_native_armhf_emul_lock_fd}"; then
217+
exec {_native_armhf_emul_lock_fd}>&-
197218
unset _native_armhf_emul_lock_fd
219+
display_alert "Native armhf via binfmt_elf" "could not acquire emulation SH-lock within 30s; concurrent native-armhf transition stuck?" "wrn"
220+
return 1
221+
fi
222+
# Post-SH state check: the peer's transition may have completed before
223+
# our SH waiters were granted. If qemu-arm is 0, peer is now native and
224+
# the killswitch contract cannot be honored.
225+
if [[ "$(_native_armhf_observe_qemu_arm_state)" != "1" ]]; then
226+
exec {_native_armhf_emul_lock_fd}>&-
227+
unset _native_armhf_emul_lock_fd
228+
display_alert "Native armhf via binfmt_elf" "killswitch requested but qemu-arm already disabled by concurrent native-armhf builders" "err"
229+
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."
198230
fi
231+
add_cleanup_handler trap_handler_native_armhf_release_emul_lock
232+
display_alert "Native armhf via binfmt_elf" "killswitch active; emulation-mode SH-lock acquired (blocks concurrent native-armhf switchover)" "info"
199233
return 1
200234
fi
201235

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-
208236
# Pre-flight is unreliable when qemu-arm is enabled (it interprets the
209237
# arch-test stub); the authoritative check is post-disable below.
210238
if ! arch-test armhf > /dev/null 2>&1; then
@@ -228,32 +256,56 @@ function _native_armhf_setup_binfmt_elf() {
228256
return 1
229257
fi
230258

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.
259+
# Take EX-NB first to determine whether we are alone or joining. While
260+
# we hold EX, no SH/EX can enter, so state observation and the disable
261+
# write are atomic w.r.t. concurrent K- or N-builders. Then downgrade
262+
# EX → SH on the same fd; Linux flock(2) performs the transition under
263+
# the inode's flc_lock, granting pending SH waiters only after our SH
264+
# is in place — by which time qemu-arm is already 0 and any joiner
265+
# sees joiner-territory (state=0).
266+
#
267+
# EX-NB failure means someone else holds SH. State distinguishes:
268+
# state=1 → killswitch K-builder holds the emulation-mode anchor;
269+
# switching qemu-arm off would corrupt its chroot exec.
270+
# state=0 → peer N-builder; we are a joiner, take SH without writing.
238271
if flock -x -n "${_native_armhf_lock_fd}"; then
239-
flock -u "${_native_armhf_lock_fd}"
272+
if [[ "$(_native_armhf_observe_qemu_arm_state)" == "1" ]]; then
273+
if ! echo 0 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null; then
274+
display_alert "Native armhf via binfmt_elf" "could not disable qemu-arm (no CAP_SYS_ADMIN?); falling back to qemu-arm-static emulation" "wrn"
275+
exec {_native_armhf_lock_fd}>&-
276+
unset _native_armhf_lock_fd
277+
return 1
278+
fi
279+
fi
280+
if ! flock -s "${_native_armhf_lock_fd}"; then
281+
display_alert "Native armhf via binfmt_elf" "could not downgrade EX→SH on binfmt_misc/qemu-arm; falling back to qemu emulation" "wrn"
282+
exec {_native_armhf_lock_fd}>&-
283+
unset _native_armhf_lock_fd
284+
return 1
285+
fi
240286
elif [[ "$(_native_armhf_observe_qemu_arm_state)" == "1" ]]; then
241287
exec {_native_armhf_lock_fd}>&-
242288
unset _native_armhf_lock_fd
243289
display_alert "Native armhf via binfmt_elf" "concurrent build holds emulation-mode lock (NATIVE_ARMHF_ON_ARM64=no)" "err"
244290
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"
291+
else
292+
if ! flock -s -w 30 "${_native_armhf_lock_fd}"; then
293+
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"
294+
exec {_native_armhf_lock_fd}>&-
295+
unset _native_armhf_lock_fd
296+
return 1
297+
fi
298+
# Joiner state-recheck. Between observing state=0 above and acquiring
299+
# SH here, the last live N-builder may have released its SH and run
300+
# trap_handler_native_armhf_restore_qemu_arm, taking EX-NB and writing
301+
# qemu-arm back to "1". Our blocking SH then gets granted on an empty
302+
# lock — state=1 now. Continuing as a native joiner would skip the
303+
# qemu-arm-static deploy while binfmt actually routes to qemu, and
304+
# chroot exec fails because the qemu binary isn't in the chroot.
305+
# Release SH and fall back to the qemu emulation path; the caller
306+
# (prepare_host_binfmt_qemu_cross) will register qemu-arm normally.
307+
if [[ "$(_native_armhf_observe_qemu_arm_state)" != "0" ]]; then
308+
display_alert "Native armhf via binfmt_elf" "joiner lost race to last-out restorer; qemu-arm re-enabled, falling back to emulation" "wrn"
257309
exec {_native_armhf_lock_fd}>&-
258310
unset _native_armhf_lock_fd
259311
return 1
@@ -290,9 +342,16 @@ function trap_handler_native_armhf_release_emul_lock() {
290342
# the build's subshells (umount / SDCARD / MOUNT teardown). BSD flock is per-
291343
# OFD, so a forked subshell inheriting our SH-fd shares the same lock entry —
292344
# 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).
345+
# still-alive child. add_cleanup_handler PREPENDS to the handler list and
346+
# run_cleanup_handlers iterates it in order, so later-registered handlers
347+
# run first. Our setup is invoked before mount_chroot in both call sites
348+
# (rootfs-create.sh and rootfs-image.sh), so mount_chroot's umount handlers
349+
# are registered after ours, sit at the head of the list, and execute before
350+
# us — by the time we run, the chroot is unmounted and inheriting subshells
351+
# have exited. Verified empirically (SIGINT mid-chroot). If a future call
352+
# site registers our setup AFTER mount_chroot, this invariant inverts and
353+
# the EX-NB probe will spuriously fail; the documented escape hatches are
354+
# POSIX F_SETLK on a helper fd or explicit descendant-kill.
296355
function trap_handler_native_armhf_restore_qemu_arm() {
297356
[[ -n "${_native_armhf_lock_fd:-}" ]] || return 0
298357
exec {_native_armhf_lock_fd}>&-
@@ -369,25 +428,12 @@ function prepare_host_binfmt_qemu_cross() {
369428
continue
370429
fi
371430

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
431+
# Native armhf activation deferred. _native_armhf_setup_binfmt_elf is
432+
# called AFTER mmdebstrap (rootfs-create.sh / rootfs-image.sh) — calling
433+
# it here, before the chroot has libc/ld-linux-armhf.so.3, would leave
434+
# bootstrap with no qemu registration and no native interpreter inside
435+
# the chroot, breaking armhf maintainer-script execution. Let prepare_host
436+
# register qemu-arm normally; the post-mmdebstrap call switches to native.
391437

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

0 commit comments

Comments
 (0)