Skip to content

Exit vs futex block race reproducer update of sys exit#76

Draft
andreykarpenko-qc wants to merge 3 commits into
masterfrom
exit_vs_futex_block_race_reproducer_update_of_sys_exit
Draft

Exit vs futex block race reproducer update of sys exit#76
andreykarpenko-qc wants to merge 3 commits into
masterfrom
exit_vs_futex_block_race_reproducer_update_of_sys_exit

Conversation

@andreykarpenko-qc

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions github-actions Bot added the untested Mark untested PRs label Jul 12, 2026
@andreykarpenko-qc andreykarpenko-qc force-pushed the exit_vs_futex_block_race_reproducer_update_of_sys_exit branch from eaac1ba to 7567250 Compare July 12, 2026 15:08
Add an h2 test that reproduces the exit() (whole-VM teardown) vs
futex-block race that the kernel_thread_stop_reap_blocked branch fixes,
made sweepable via a runtime spin parameter.

The race and why the fix is correct
-----------------------------------
Thread A calls exit(0) -> H2K_vm_stop -> vm_stop_locked: grabs the BKL,
sets vmblock->exiting = 1, reaps every non-RUNNING peer, then IPIs the
RUNNING peers (CLUSTER_RESCHED_INT) to self-reap.  Thread B is a RUNNING
peer racing into H2K_futex_wait, where it grabs the BKL, removes itself
from the runlist and commits to H2K_STATUS_BLOCKED.

A's reap scan runs while B is still RUNNING, so reap_one_locked skips B
(the RUNNING case is a no-op).  B only commits to BLOCKED after A drops
the BKL.  Two mechanisms -- both new on this branch -- make that safe:

  1. A's H2K_vmblock_finalize_if_done_locked early-returns while
     num_cpus != 0.  B's un-reaped slot keeps num_cpus == 1, so the
     vmblock is NOT freed out from under B while B is still about to
     dereference it.  (Confirmed under lldb: at A's finalize the vmblock
     shows num_cpus=1, status=0, exiting=1, and the freeing branch is
     skipped.)
  2. B is then reaped via the vmblock->exiting gate in resched():
     reached either when B voluntarily blocks (H2K_dosched -> resched)
     or via A's Pass-2 CLUSTER_RESCHED_INT.  resched sees exiting,
     self-reaps B and clears its context; finalize then completes
     teardown once num_cpus reaches 0.

Without this branch's teardown machinery, exit() had no whole-VM reaper
at all (master has no H2K_vm_stop, no vmblock->exiting, no
reap_one_locked, and resched unconditionally requeues).  A peer that
blocked on a futex during another thread's exit was stranded as a
permanently BLOCKED context on a VM that was supposed to be gone -- a
leaked / missed-wakeup thread -- and freeing the vmblock while B was
mid-commit would have been a use-after-free.  The num_cpus guard in (1)
is exactly what closes that hazard.

Runtime spin parameter
-----------------------
A signed argv[1] selects which racing thread absorbs an artificial spin
delay, sliding one side's timing across the other's teardown so the
reproducer can land inside the race window:

  argv[1] >= 0 : delay the BLOCKER by |argv[1]| spins (exiter tears down
                 immediately) so B commits to the futex wait late.
  argv[1] <  0 : delay the EXITER by |argv[1]| spins so teardown lands
                 after B is already BLOCKED.

Default is 0 spins on both sides so the plain harness invocation still
runs; scripts/Makefile.inc.test now forwards ${GUEST_ARGS} to the guest
so a test's Makefile (or the command line) can pin the delay.  On
v81/opt the tightest race -- A and B acquiring the BKL within one spin
iteration of each other -- sits at blocker_spins == 653 (bisected under
lldb: +652 the blocker wins, +653 the exiter wins); the large value is
because the exiter's puts("TEST PASSED") gives B a multi-thousand-cycle
head start.

scripts/testlist.v61: enable exit_vs_futex_block (v81 already had it).

Signed-off-by: Andrey Karpenko <andreyk@qti.qualcomm.com>
The default __h2_thread_stop_hook__ called pthread_exit, a top-of-stack API,
from the lowest syscall layer -- a layering inversion that pulled the pthread
runtime into every hosted link.  Switch the default hook to the pure-h2 final
exit primitive h2_thread_stop_trap(), matching what pthread_exit itself does on
its last-thread path, and drop the pthread include.

Also remove the (already-disabled) if (0 == status) guard so the hook fires on
every exit.  A nonzero exit status still reaches the harness via vmblock->status
(set by both h2_vmkill and H2K_thread_stop_withlock, read back by the booter),
so the ANGEL(SYS_EXIT) fall-through is not needed for failure reporting.

Signed-off-by: Andrey Karpenko <andreyk@qti.qualcomm.com>
…elay

The suite invoked the reproducer with GUEST_ARGS=0 (no artificial delay).
Set it to 653, the v81/opt spin count that delays the blocker just enough
for the exiter's teardown scan to catch it still RUNNING -- landing the
run inside the exit-vs-futex race window rather than relying on incidental
timing. Verified: blocker_spins=653, TEST PASSED, VM 2 status 0x0, exit 0.

Signed-off-by: Andrey Karpenko <andreyk@qti.qualcomm.com>
@andreykarpenko-qc andreykarpenko-qc force-pushed the exit_vs_futex_block_race_reproducer_update_of_sys_exit branch from 7567250 to a0eb3d1 Compare July 12, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

untested Mark untested PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant