handle_term() inherited by reforked vkernel children runs kill(0, SIGTERM): process-group-wide TERM splash plus a self-re-arming, unstoppable signal storm
Summary
The monitor installs the SIGTERM forwarder AFTER fork, so every child of a LATER fork iteration (every boot after a reboot cycle) inherits it with childpid==0 - and kill(0) signals the WHOLE PROCESS GROUP while TERM is self-masked in the handler: the kill(0) re-arms a pending TERM that fires on handler return - a 1:1 self-sustaining storm freezing the booting vkernel plus collateral TERMs on the rc script/supervisor. Byte-faithful storm demo: 85x RUNNER_GOT_SIGTERM + dead bystander + wedge immune to further TERMs; first-boot children have SIG_DFL (verified). The in-vkernel chain is line-certain but the stock snapshot cannot reach child #2 - reboot(2) NULL-derefs in naccess (a third DF-5560/5561-family sibling noted). childpid>0 guard fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5773 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| storm_demo.c | β | 2.2 KB | view raw | |
| storm_demo_fixed.c | β | 1.0 KB | view raw | |
| run_storm_demo.sh | β | 1.5 KB | view raw | |
| baseline_storm.obs | β | 1.7 KB | β download | |
| fixed_storm.obs | β | 550 B | β download | |
| run5773.sh | β | 4.4 KB | view raw | |
| reboot2.c | β | 428 B | view raw | |
| baseline_vk5773.obs | β | 291 B | β download | |
| fixed_vk5773.obs | β | 291 B | β download | |
| unblock_DF-5561.diff | β | 2.8 KB | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| verdict.json | β | 5.0 KB | view raw |
DF-5773 β handle_term() in a reforked vkernel child runs kill(0, SIGTERM): process-group-wide TERM splash + self-re-arming signal storm
File: sys/platform/vkernel64/platform/init.c
Lines: 204-230 (monitor loop, sigaction placement), 500-506 (handle_term)
Severity: Medium (DoS + collateral kill of the launching process group)
Status: mechanism reproduced on-guest (faithful extraction); in-vkernel trigger line-certain but gated on a sibling stock bug (see VERDICT.md). Fix validated.
What to build / run (guest, root)
Two layers of evidence:
- Mechanism (decisive) β
storm_demo.cis a byte-faithful extraction of init.c's monitor/child structure (fork loop, sigaction placement after fork,handle_termdoingkill(childpid, sig), exit-32 reboot cycle):
cc -O -o storm_demo storm_demo.c
sh run_storm_demo.sh 3 # bystander in pgrp + TERM in gen-2 window
Vulnerable baseline (baseline_storm.obs): 85Γ RUNNER_GOT_SIGTERM,
BYSTANDER_DEAD_BY_SPLASH, DEMO_STILL_ALIVE_AFTER_SECOND_TERM_storm_wedged.
Fixed build (storm_demo_fixed.c = guard added): 0 runner hits,
BYSTANDER_ALIVE β the splash and the storm are gone.
- In-vkernel (documented, gated) β
run5773.shdrives the real vkernel: boot/vkroot.imgto the single-user shell, run/bin/reboot2(directreboot(2)RB_AUTOBOOT from process context) to completekern_reboot -> cpu_reset() -> exit(EX_VKERNEL_REBOOT), which makes the monitor refork; child #2 then INHERITShandle_termwithchildpid == 0, and a TERM burst in its early-boot window would executekill(0, SIGTERM). On this stock snapshot the reboot path panics first (naccessNULL deref, fault VA 0x58 β sibling bug, see VERDICT.md), so child #2 is never reached;reboot2.candrun5773.share kept as the ready-to-run rig for when that sibling is fixed (unblock_DF-5561.diffalready applied here moves the panic from the pre-sync no-op to the post-syncnaccessfault).
Expected output
- storm_demo (vulnerable): runner trap fires in a tight loop (85+ hits in
~3 s), the bystander
sleepis killed by the group splash, the demo pair survives a second TERM at ~100% CPU (onlykill -9works). - storm_demo_fixed: zero runner hits, bystander alive, demo cycles normally.
Fix
fix.diff hunk 1: only the monitor (childpid > 0) may forward.
DF-5773 β VERDICT
Status: reproduced (mechanism, faithful extraction of init.c:204-230/500-506, on-guest); in-vkernel trigger traced line-certain but gated on a sibling stock bug. Fix validated (harness-level + rebuilt kernel). Impact: dos (process-group collateral TERM + permanent vkernel wedge). Confidence: likely (mechanism certain; reachability needs a completed reboot cycle, which stock currently blocks one bug further up the chain).
Root cause (certain, path:line)
sys/platform/vkernel64/platform/init.c:
- 204:
while ((childpid = fork()) != 0) { - 206-211: SIG_IGN for INT/QUIT/HUP installed inside the loop, i.e. by the monitor AFTER fork();
- 217-218:
sa.sa_handler = handle_term; sigaction(SIGTERM, &sa, NULL);β likewise installed only after the fork returned; - 228-230:
WEXITSTATUS(status) != EX_VKERNEL_REBOOTelse loop β fork again; - 500-506:
handle_term(int sig) { kill(childpid, sig); }β in the CHILD of any post-reboot iteration,childpid == 0, sokill(0, SIGTERM)means "signal every process in my process group".
Consequences in a reforked child (the normal reboot lifecycle:
guest reboot(2) β kern_reboot β cpu_reset() (init.c:1474-1480,
exit(EX_VKERNEL_REBOOT)) β monitor refork):
- Group splash: the rc script / supervisor / pipeline that launched the vkernel (everything sharing its pgrp) receives SIGTERM.
- Self-re-arming storm: the child's own handler runs with SIGTERM
self-masked (no SA_NODEFER),
kill(0)posts a new pending TERM to the child itself, and on handler return it is delivered immediately β the handler re-runs βkill(0)again β a 1:1 self-sustaining loop. The vkernel never progresses past the early-boot window (untilinitshutdown, shutdown.c:82-83 at SI_BOOT2_PROC0, replaces the handler), never answers, and is immune to further TERMs. The monitor (same inherited forwarding logic,childpid > 0) joins the loop by re-forwarding every group TERM it receives.
Live proof
Mechanism β storm_demo.c (faithful extraction), vulnerable
run_storm_demo.sh 3 on the guest (bystander sleep 120 in the same pgrp,
runner traps TERM):
demo=28519 bystander=28518 window=3 FIRING TERM at gen2 window BYSTANDER_DEAD_BY_SPLASH <- kill(0) hit the whole group DEMO_ALIVE DEMO_STILL_ALIVE_AFTER_SECOND_TERM_storm_wedged # and: 85 x RUNNER_GOT_SIGTERM <- the storm, re-arming every delivery
(baseline_storm.obs; count via grep -c GOT = 85.)
Mechanism β fixed (storm_demo_fixed.c, guard if (childpid > 0))
demo=40863 bystander=40862 window=3
FIRING TERM at gen2 window
BYSTANDER_ALIVE <- no splash
DEMO_ALIVE <- TERMs harmlessly ignored in-window,
lifecycle continues (not a wedge)
# and: 0 x RUNNER_GOT_SIGTERM
In-vkernel β rig ready, sibling bug blocks the last step
run5773.sh + reboot2.c: boots /vkroot.img to single-user, invokes
reboot(2) directly from process context (bypassing reboot(8), whose
process teardown wedges on this rig). The stock kernel reaches
boot() called on cpu#0, stops system threads, syncs disks, then takes
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x58 instruction pointer = 0x2b:0x55d143 (naccess, vfs_nlookup.c) current process = 1 (sh)
and wedges in ddb β the reboot NEVER reaches cpu_reset(), so the monitor
never reforks and child #2 (the storm's stage) is unreachable on this
snapshot. With DF-5661's fix applied (unblock_DF-5561.diff: shutdown.c
gate + kern_shutdown.c closefrom hardening) the panic moves from the
pre-sync no-op to this same post-sync naccess fault β still short of
cpu_reset. This is a third, separate defect in the shutdown/reboot
family (out of init.c's scope; logged for the family):
kern_shutdown.c boot() β shutdown_cleanup_proc β ... β naccess NULL
deref at offset 0x58 on VKERNEL64.
Every code link in the chain is otherwise verified:
- the sigactions run post-fork (first-boot children have SIG_DFL β
confirmed live: TERM at 0.15 s into boot #1 = silent child death,
no splash, no storm);
- cpu_reset() exits 32 and the monitor's refork loop re-forks from a
monitor that by then HAS handle_term installed (init.c:217-218 ran in
iteration 1) β child #2 inherits it;
- in child #2, childpid == 0 β kill(0, SIGTERM) (init.c:505).
Why not higher impact
No memory-corruption primitive; the deliverable is the demonstrated
group-wide collateral kill + permanent TERM-proof wedge of the vkernel
process pair. An operator (or rc shutdown, killall TERM, a supervision
framework) signaling a booting reforked vkernel kills its own process
group and leaves an unkillable-except-KILL spinning vkernel behind.
Fix validation
fix.diff (init.c, 3 hunks β this finding is hunk 1):
if (childpid > 0) kill(childpid, sig);
- Harness: baseline (storm, splash, 85 runner hits, wedge) vs fixed (0 hits, bystander alive, normal lifecycle) β above.
- Rebuilt vkernel with the fix (+ the DF-5661 unblock): the TERM burst
during the (sibling-gated) window produces no splash and no storm
(
fixed_vk5773.obs: SLEEPER_ALIVE, no RUNNER_GOT_SIGTERM); behavior otherwise unchanged (reboot path still wedges innaccess, untouched by this fix β as expected, different file).
Artifacts
storm_demo.c/storm_demo_fixed.c/run_storm_demo.shβ mechanismbaseline_storm.obs(85 hits, splash, wedge) β decisive baselinerun5773.sh,reboot2.c,baseline_vk5773.obsβ in-vkernel rigunblock_DF-5561.diffβ sibling-bug unblocker applied for the attemptfix.diffβ the verified init.c fix (3 hunks; hunk 1 is this finding)
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedBaseline harness: storm (85 runner hits) + bystander killed + TERM-proof wedge. Patched harness (guard): 0 hits, bystander alive, normal lifecycle. Patched vkernel (rebuilt): TERM burst produces no splash and no storm (SLEEPER_ALIVE, no RUNNER_GOT_SIGTERM); the reboot path still wedges in naccess, which is a different file's defect and correctly unaffected.
baseline_storm.obs vs fixed_storm.obs; fixed_vk5773.obs; fix.diff hunk 1
Confirmed kernel references
Detail
Exploit chain
attacker with same uid as vkernel (or root) -> vkernel completes one guest reboot (kern_reboot -> cpu_reset exit 32 -> monitor refork; stock-gated by the sibling naccess bug today) -> child #2 inherits handle_term with childpid==0 -> single kill -TERM during the next boot's pre-SI_BOOT2_PROC0 window -> kill(0, SIGTERM) -> process-group splash (launching script/supervisor TERMed) + self-re-arming storm -> vkernel permanently wedged, TERM-immune, ~100% CPU; only kill -9 recovers
Evidence (decisive lines)
baseline_storm.obs (85x RUNNER_GOT_SIGTERM, BYSTANDER_DEAD_BY_SPLASH, DEMO_STILL_ALIVE_AFTER_SECOND_TERM_storm_wedged); fixed_storm.obs (0 hits, BYSTANDER_ALIVE after the guard); VERDICT.md (full trace, sibling-gate analysis); run5773.sh/reboot2.c (in-vkernel rig); fixed_vk5773.obs (fixed kernel, TERM burst: SLEEPER_ALIVE, no splash)
PoC changes
PoC authored fresh this run (no seed): storm_demo.c extracts init.c's exact monitor/child structure; run5773.sh adapts the DF-5661 fifo rig (root-mount answer must be written at t=0; guest reboot(8) wedges, so reboot2.c invokes reboot(2) directly; exec 3>fifo must happen only after the vkernel holds the read end -- fifo deadlock otherwise)
Verified recommended fix
Guard handle_term: if (childpid > 0) kill(childpid, sig); -- only the monitor forwards
Verdict
init.c installs the monitor's SIGTERM forwarder handle_term AFTER fork() (init.c:206-218), so every child forked by a LATER monitor iteration -- i.e. every vkernel boot after a cpu_reset()/EX_VKERNEL_REBOOT reboot cycle -- inherits handle_term with childpid == 0, turning init.c:505's kill(childpid, sig) into kill(0, SIGTERM): a signal to the ENTIRE process group. Because the handler runs with SIGTERM self-masked, its own kill(0) leaves a fresh TERM pending on itself that is delivered the instant the handler returns: a self-re-arming, unstoppable signal storm that freezes the reforked vkernel in its early-boot window (until shutdown.c's SI_BOOT2_PROC0 initshutdown would replace the handler) and TERMs every bystander in the launching pgrp (rc script, supervisor, pipeline). The mechanism was reproduced on the guest with a byte-faithful extraction of init.c:204-230/500-506 (storm_demo.c): 85 runner TERM deliveries in ~3s, bystander killed by the splash, demo pair wedged against a second TERM; with the one-line guard the storm and splash vanish (0 hits, bystander alive). The full in-vkernel trigger is traced line-by-line and rigged (run5773.sh + reboot2.c) but is currently gated ONE bug upstream: the stock vkernel's reboot path NULL-derefs in naccess (fault VA 0x58, post-sync, kern_shutdown boot() cleanup) before reaching cpu_reset, so no reforked child exists on this snapshot to storm. Same-uid (or root) signaling is the only prerequisite on the attacker side, and vkernel reboot cycles are routine operation.
No comments yet.