β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5773

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5773 Β· 14 files
FileTypeDescriptionSize
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:

  1. Mechanism (decisive) β€” storm_demo.c is a byte-faithful extraction of init.c's monitor/child structure (fork loop, sigaction placement after fork, handle_term doing kill(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.

  1. In-vkernel (documented, gated) β€” run5773.sh drives the real vkernel: boot /vkroot.img to the single-user shell, run /bin/reboot2 (direct reboot(2) RB_AUTOBOOT from process context) to complete kern_reboot -> cpu_reset() -> exit(EX_VKERNEL_REBOOT), which makes the monitor refork; child #2 then INHERITS handle_term with childpid == 0, and a TERM burst in its early-boot window would execute kill(0, SIGTERM). On this stock snapshot the reboot path panics first (naccess NULL deref, fault VA 0x58 β€” sibling bug, see VERDICT.md), so child #2 is never reached; reboot2.c and run5773.sh are kept as the ready-to-run rig for when that sibling is fixed (unblock_DF-5561.diff already applied here moves the panic from the pre-sync no-op to the post-sync naccess fault).

Expected output

  • storm_demo (vulnerable): runner trap fires in a tight loop (85+ hits in ~3 s), the bystander sleep is killed by the group splash, the demo pair survives a second TERM at ~100% CPU (only kill -9 works).
  • storm_demo_fixed: zero runner hits, bystander alive, demo cycles normally.

Fix

fix.diff hunk 1: only the monitor (childpid > 0) may forward.

VERDICT.md
↓ download raw

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_REBOOT else loop β†’ fork again;
  • 500-506: handle_term(int sig) { kill(childpid, sig); } β€” in the CHILD of any post-reboot iteration, childpid == 0, so kill(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):

  1. Group splash: the rc script / supervisor / pipeline that launched the vkernel (everything sharing its pgrp) receives SIGTERM.
  2. 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 (until initshutdown, 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 in naccess, untouched by this fix β€” as expected, different file).

Artifacts

  • storm_demo.c / storm_demo_fixed.c / run_storm_demo.sh β€” mechanism
  • baseline_storm.obs (85 hits, splash, wedge) β€” decisive baseline
  • run5773.sh, reboot2.c, baseline_vk5773.obs β€” in-vkernel rig
  • unblock_DF-5561.diff β€” sibling-bug unblocker applied for the attempt
  • fix.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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline 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
↓ fix.diffper-fix-DF-5773

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.