Skip to content

Commit 3d4c8cd

Browse files
committed
fix(rootfs/qemu-static): close 3 races flagged by codex review of 5359f27
Codex review of the previous lock-pattern commit (5359f27) found three real holes: 1. trap_handler released the owner lock BEFORE taking the control lock. Between fd close and flock(control) a fresh builder could enter setup, prune+count and see live_count == 1 (only itself), then observe the now-disabled qemu-arm and write '0' as its prior_state. Our subsequent restore would read that overwritten state and never re-enable qemu-arm — turning a coordinated handoff into permanent disablement. Fix: take control.lock first, release owner UNDER control, then count and restore. The whole transition is now atomic. 2. The post-disable arch-test failure path released the owner lock manually and then called the trap handler. The handler's first guard ([[ -n ${_native_armhf_owner_fd:-} ]] || return 0) saw the already-cleared global and exited without restore — leaving qemu-arm disabled and the state file leaking. Fix: drop control.lock and let the handler do the release+count+restore under its own control lock, matching the normal cleanup path. 3. Stale active window after process kill (SIGKILL / OOM / kill -9 between echo 0 and trap firing): kernel releases the fd so the owner lock is gone, but the state file remains. The next first-owner would observe the leftover-disabled qemu-arm and record disabled as its new prior_state — clean shutdown then never re-enables. Fix: in the first-owner branch, before saving prior_state, check for an existing state file. If found and prior was '1', reconcile qemu-arm back to enabled and remove the stale file before observing our own prior_state. Crash recovery is now self-healing on the next build. Same Edit additionally cleans up the disable-failure case: when 'echo 0 > qemu-arm' itself fails (no CAP_SYS_ADMIN), no state actually changed, so leave state file and owner lock alone — the cleanup handler will observe current==prior==1 and no-op the restore. The previous code released owner outside control and rm-ed the state file, both of which introduced the same race the trap_handler change closes elsewhere. Reported-by: codex (PR #9769 lock-impl review-helper) Assisted-by: Claude:claude-opus-4.7 Signed-off-by: Igor Velkov <325961+iav@users.noreply.github.com>
1 parent 5359f27 commit 3d4c8cd

1 file changed

Lines changed: 55 additions & 22 deletions

File tree

lib/functions/rootfs/qemu-static.sh

Lines changed: 55 additions & 22 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,9 +310,13 @@ 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"
292-
rm -f "${_NATIVE_ARMHF_PRIOR_STATE}"
313+
# qemu-arm was not changed (write failed). State file
314+
# (prior=1) and our owner lock stay; the cleanup
315+
# handler — already registered above — will observe
316+
# current==prior==1 and no-op the restore at build
317+
# exit. Don't touch them here: releasing owner outside
318+
# control would race with concurrent setup probes.
293319
exec {control_fd}>&-
294-
_native_armhf_release_owner_lock
295320
return 1
296321
fi
297322
;;
@@ -311,13 +336,13 @@ function _native_armhf_setup_binfmt_elf() {
311336
# arm64-compat-vdso, PR #9284) — qemu-arm interprets the stub.
312337
if ! arch-test armhf > /dev/null 2>&1; then
313338
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.
339+
# Drop control lock so the handler can re-acquire it. Do NOT
340+
# release the owner lock here — the handler must release it UNDER
341+
# control so live_count reflects only OTHER owners and the
342+
# state-preserving restore decision uses a consistent snapshot.
343+
# Releasing owner outside the control section was the race codex
344+
# flagged in the previous round.
317345
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.
321346
trap_handler_native_armhf_restore_qemu_arm
322347
return 1
323348
fi
@@ -344,22 +369,30 @@ function _native_armhf_release_owner_lock() {
344369
# disabled state. Restore writes the prior state observed at first-owner
345370
# time (1 / 0 / missing) — never blindly 1.
346371
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.
372+
# Skip cleanup entirely if we never acquired an owner lock.
349373
[[ -n "${_native_armhf_owner_fd:-}" ]] || return 0
350-
_native_armhf_release_owner_lock
351374

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.
375+
# Take the control lock BEFORE releasing our owner lock. The previous
376+
# revision did the reverse and codex flagged the race: between fd
377+
# close and control acquire, a fresh setup could enter, see itself as
378+
# the only live owner (live_count == 1), observe the now-disabled
379+
# qemu-arm and write that as its prior_state — so our subsequent
380+
# restore would read the overwritten state and fail to re-enable.
381+
# Holding control across release+count+restore makes the transition
382+
# atomic with respect to any concurrent setup.
354383
declare control_fd
355384
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.
385+
if ! flock "${control_fd}"; then
386+
# Lock failed — best effort. Drop our owner so we are not counted
387+
# live forever; next build will reconcile.
360388
exec {control_fd}>&-
389+
_native_armhf_release_owner_lock
361390
return 0
362-
}
391+
fi
392+
393+
# Drop our own ownership UNDER control. live_count below now reflects
394+
# only OTHER live owners.
395+
_native_armhf_release_owner_lock
363396

364397
declare live_count
365398
live_count="$(_native_armhf_live_owner_count_locked)"

0 commit comments

Comments
 (0)