Skip to content

Commit 52faab6

Browse files
committed
fix(rootfs/qemu-static): close 2 more races flagged by codex review of 3d4c8cd
Codex re-review of fix2 (commit 3d4c8cd) confirmed the original three races are properly closed but identified two new ones, both producing the same class of runtime failure: a parallel builder ends up running chroot exec against globally-enabled qemu-arm with no qemu binary inside its chroot. 1. Disable-failure path left a live owner. When 'echo 0 > qemu-arm' itself fails (no CAP_SYS_ADMIN), fix2 chose to leave the owner lock and the prior-state file in place, on the theory that the cleanup handler would no-op the restore at exit. But the owner stays counted as live for the entire build duration, so a concurrent builder enters its own setup, sees live_count > 1, takes the join branch, skips qemu binary deployment, and fails at chroot exec time. Fix: a failed disable now performs a full abort under the held control lock — rm state file, release owner, drop control, return 1. The aborted setup leaves no observable native window for anyone to join. 2. Join path did not verify the actual kernel state. fix2 trusted that live_count > 1 implies qemu-arm is disabled. But an external agent (admin running 'update-binfmts --enable qemu-arm', a stray service) can flip it back on during the active window. Our pre-flight arch-test then passes as a false positive — qemu interprets the ARM stub — and we skip qemu binary deployment, hitting the same runtime failure as case 1. Fix: in the join branch, observe the current qemu-arm state explicitly. If it is enabled, this is not a normal join — abort to qemu fallback with a warning naming the external mutator. This is a one-shot invariant check at join time, not a periodic re-verify (which codex previously argued against). Reported-by: codex (PR #9769 lock-impl re-review) Assisted-by: Claude:claude-opus-4.7 Signed-off-by: Igor Velkov <325961+iav@users.noreply.github.com>
1 parent 5359f27 commit 52faab6

1 file changed

Lines changed: 77 additions & 25 deletions

File tree

lib/functions/rootfs/qemu-static.sh

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,31 @@ function _native_armhf_setup_binfmt_elf() {
267267
live_count="$(_native_armhf_live_owner_count_locked)"
268268

269269
if [[ "${live_count}" -eq 1 ]]; then
270-
# We are the only live owner — first-owner. Record current qemu-arm
271-
# state so cleanup can restore it state-preserving (see codex review:
272-
# never blindly write 1 — if prior was disabled, leave disabled; if
273-
# prior was missing, don't try to recreate the handler).
270+
# We are the only live owner — first-owner. Before recording our
271+
# prior_state, reconcile any stale state file from a previous
272+
# active window that ended without running cleanup (e.g. SIGKILL /
273+
# OOM kill / `kill -9` of the build between the post-disable echo 0
274+
# and the trap firing). Without this branch the crashed window's
275+
# left-behind disabled state would be observed as our new
276+
# prior_state, and our own clean shutdown would never re-enable
277+
# qemu-arm — turning a single crash into a permanent disablement.
278+
if [[ -f "${_NATIVE_ARMHF_PRIOR_STATE}" ]]; then
279+
declare stale_prior
280+
stale_prior="$(cat "${_NATIVE_ARMHF_PRIOR_STATE}" 2> /dev/null || echo missing)"
281+
if [[ "${stale_prior}" == "1" && -e /proc/sys/fs/binfmt_misc/qemu-arm ]]; then
282+
declare crashed_observed
283+
crashed_observed="$(_native_armhf_observe_qemu_arm_state)"
284+
if [[ "${crashed_observed}" != "1" ]]; then
285+
echo 1 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null || true
286+
display_alert "Native armhf via binfmt_elf" "stale active window detected (previous build crashed pre-cleanup, prior=${stale_prior}); qemu-arm reconciled to enabled before our first-owner observation" "wrn"
287+
fi
288+
fi
289+
rm -f "${_NATIVE_ARMHF_PRIOR_STATE}"
290+
fi
291+
292+
# Record current qemu-arm state so cleanup can restore it state-
293+
# preserving (codex: never blindly write 1 — if prior was disabled,
294+
# leave disabled; if prior was missing, don't recreate the handler).
274295
declare prior_state
275296
prior_state="$(_native_armhf_observe_qemu_arm_state)"
276297
echo "${prior_state}" > "${_NATIVE_ARMHF_PRIOR_STATE}"
@@ -289,18 +310,41 @@ function _native_armhf_setup_binfmt_elf() {
289310
1)
290311
if ! echo 0 > /proc/sys/fs/binfmt_misc/qemu-arm 2> /dev/null; then
291312
display_alert "Native armhf via binfmt_elf" "could not disable qemu-arm (no CAP_SYS_ADMIN?); falling back to qemu-arm-static emulation" "wrn"
313+
# Full abort: failed disable must NOT leave us as a live
314+
# native-mode owner. Otherwise a concurrent builder sees
315+
# live_count > 1, takes the join branch, skips qemu
316+
# binary deployment, and runs its chroot against still-
317+
# enabled qemu-arm without the binary inside — runtime
318+
# failure (codex review of fix2). Cleanup is performed
319+
# UNDER control so prune+count by other builders sees
320+
# the abort atomically.
292321
rm -f "${_NATIVE_ARMHF_PRIOR_STATE}"
293-
exec {control_fd}>&-
294322
_native_armhf_release_owner_lock
323+
exec {control_fd}>&-
295324
return 1
296325
fi
297326
;;
298327
esac
299328
else
300-
# Another live owner already disabled qemu-arm. Just join — no state
301-
# to save (the first owner's state file is authoritative), no
302-
# disable to perform. Cleanup is still registered so this owner's
303-
# release decrements live_count back toward zero.
329+
# Another live owner already disabled qemu-arm — or so we think.
330+
# Verify the kernel state matches: if qemu-arm is observably
331+
# enabled here despite a live first owner, an external agent
332+
# (admin / `update-binfmts --enable qemu-arm`) flipped it back on
333+
# during the active window. Our pre-flight arch-test then passes
334+
# as a false positive (qemu interprets the stub) and we'd skip
335+
# the qemu binary deployment, then run chroot exec against
336+
# globally-enabled qemu-arm with no binary inside the chroot —
337+
# runtime failure (codex review of fix2).
338+
declare observed_join
339+
observed_join="$(_native_armhf_observe_qemu_arm_state)"
340+
if [[ "${observed_join}" == "1" ]]; then
341+
display_alert "Native armhf via binfmt_elf" "${live_count} live owner(s) but qemu-arm is observably enabled (external agent re-enabled it); aborting join, falling back to qemu-arm-static emulation" "wrn"
342+
_native_armhf_release_owner_lock
343+
exec {control_fd}>&-
344+
return 1
345+
fi
346+
# Cleanup is still registered so this owner's release decrements
347+
# live_count back toward zero.
304348
add_cleanup_handler trap_handler_native_armhf_restore_qemu_arm
305349
display_alert "Native armhf via binfmt_elf" "joining ${live_count} existing native-armhf owner(s); shared qemu-arm state preserved" "info"
306350
fi
@@ -311,13 +355,13 @@ function _native_armhf_setup_binfmt_elf() {
311355
# arm64-compat-vdso, PR #9284) — qemu-arm interprets the stub.
312356
if ! arch-test armhf > /dev/null 2>&1; then
313357
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"
314-
# Restore is via cleanup handler when owner_lock releases. Mark
315-
# ourselves as not-owner-of-native-mode so the cleanup invocation
316-
# that runs immediately on this return path triggers restore.
358+
# Drop control lock so the handler can re-acquire it. Do NOT
359+
# release the owner lock here — the handler must release it UNDER
360+
# control so live_count reflects only OTHER owners and the
361+
# state-preserving restore decision uses a consistent snapshot.
362+
# Releasing owner outside the control section was the race codex
363+
# flagged in the previous round.
317364
exec {control_fd}>&-
318-
_native_armhf_release_owner_lock
319-
# Manually invoke the same restore path so the failed setup leaves
320-
# binfmt_misc as we found it; subsequent owners (if any) re-evaluate.
321365
trap_handler_native_armhf_restore_qemu_arm
322366
return 1
323367
fi
@@ -344,22 +388,30 @@ function _native_armhf_release_owner_lock() {
344388
# disabled state. Restore writes the prior state observed at first-owner
345389
# time (1 / 0 / missing) — never blindly 1.
346390
function trap_handler_native_armhf_restore_qemu_arm() {
347-
# Drop our own ownership first so the live-count below reflects only
348-
# OTHER live owners. Skip cleanup entirely if we never acquired.
391+
# Skip cleanup entirely if we never acquired an owner lock.
349392
[[ -n "${_native_armhf_owner_fd:-}" ]] || return 0
350-
_native_armhf_release_owner_lock
351393

352-
# Take the control lock to make prune+count+restore atomic with respect
353-
# to any new builder racing to become an owner right now.
394+
# Take the control lock BEFORE releasing our owner lock. The previous
395+
# revision did the reverse and codex flagged the race: between fd
396+
# close and control acquire, a fresh setup could enter, see itself as
397+
# the only live owner (live_count == 1), observe the now-disabled
398+
# qemu-arm and write that as its prior_state — so our subsequent
399+
# restore would read the overwritten state and fail to re-enable.
400+
# Holding control across release+count+restore makes the transition
401+
# atomic with respect to any concurrent setup.
354402
declare control_fd
355403
exec {control_fd}> "${_NATIVE_ARMHF_CONTROL_LOCK}"
356-
flock "${control_fd}" || {
357-
# Lock failed — best effort cleanup without restore. Worst case is
358-
# qemu-arm stays in whatever state it is now; next build will
359-
# reconcile.
404+
if ! flock "${control_fd}"; then
405+
# Lock failed — best effort. Drop our owner so we are not counted
406+
# live forever; next build will reconcile.
360407
exec {control_fd}>&-
408+
_native_armhf_release_owner_lock
361409
return 0
362-
}
410+
fi
411+
412+
# Drop our own ownership UNDER control. live_count below now reflects
413+
# only OTHER live owners.
414+
_native_armhf_release_owner_lock
363415

364416
declare live_count
365417
live_count="$(_native_armhf_live_owner_count_locked)"

0 commit comments

Comments
 (0)