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

sili_os_stop_port() waits for the port helper thread with no bound β€” a wedged helper thread turns kldunload/shutdown into a permanent kernel hang

Summary

Unbounded while(ap->ap_thread) poll with no escalation - a helper wedged on dead silicon or a held ap_lock hangs the kldunload/shutdown path itself (system-level, permanent). RUNTIME-REPRODUCED on guest via the kernel's own sili_port_alloc/thread + a parked helper: driver's own 'Waiting for thread to terminate' spun >=90s; 60s-bound + leak-not-hang fix A/B-validated on rebuilt kernel (returns t=60s, normal stops unaffected). Mirror to ahci_dragonfly.c recommended.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5044 Β· 8 files
FileTypeDescriptionSize
run.log run-log E1 control + E2 wedge (watchdog 5/10/15s spin) + E3, baseline kernel 1.8 KB view raw
run.2.log run-log norelease baseline: stopper still spinning after 90s (forever) 2.1 KB view raw
run.3.log run-log norelease on fix.patched kernel: returns at 60s bound, 'thread did not terminate; leaking port' 2.1 KB view raw
run.4.log run-log normal E1/E2/E3 on patched kernel (fix regression check) 10.8 KB view raw
fix_build.log build-log quickkernel with fix.diff applied (rc=0) 622.9 KB ↓ download
fix.diff suggested-fix bounded wait + leak-not-hang (sili_dragonfly.c/sili.c/sili_attach.c/sili.h) 3.8 KB view raw
trigger.c poc-source the driver harness 11.1 KB view raw
VERDICT.md verdict full narrative incl. the post-observation harness incident 4.9 KB ↓ raw
VERDICT.md verdict full narrative incl. the post-observation harness incident
↓ download raw

DF-5044 β€” VERDICT

Verdict: REPRODUCED (runtime, stock-kernel sili functions, no hardware) Class: unbounded wait / DoS amplifier (CWE-835) Impact ceiling: dos (permanent hang of kldunload/shutdown path with the module/controller resources pinned; requires a wedged port helper thread β€” hardware-misbehaviour-conditioned, root-triggered) Confidence: certain

Reproduction

The guest has no SiI 3124/3132 card, but sili is compiled into X86_64_GENERIC, so the OS glue under test is the running kernel's own code. The module (DF-4964 technique) constructs a softc whose BAR windows point at a real pmap_mapdev_uncacheable RAM mapping and calls the kernel's own sili_port_alloc() twice β€” REAL helper threads (sili_port_thread) start, run their probe against the fake MMIO ("No device detected") and park.

  • E1 (control) β€” sili_os_stop_port(port0) signals STOP and returns promptly (run.log).
  • E2 β€” module holds ap->ap_lock, signals AP_SIGF_TIMEOUT; the helper thread wakes and blocks inside sili_port_thread_core() β†’ sili_os_lock_port() (sili.c:1601). A stopper kthread calls sili_os_stop_port(port1): the driver prints its own pci0.1: Waiting for thread to terminate (sili_dragonfly.c:215-216) and spins in while (ap->ap_thread) sili_os_sleep(100) (sili_dragonfly.c:217-218) β€” watchdog observes 5/10/15 s of continuous spinning; only the module's wedge release lets it finish (run.log).
  • norelease (baseline kernel) β€” wedge held forever by a sacrificial kthread: stopper still spinning after 90 s (18Γ—5 s observations, run.2.log) β€” i.e. forever: nothing in the kernel will ever release it. In the real kldunload/shutdown scenario the waiter is the unload path itself (linker/module locks held) β†’ permanent system-level hang.

Mechanism (source-confirmed)

sili_os_stop_port() (sili_dragonfly.c:208-224) has no bound, no escalation and no force-kill: once the helper thread is wedged anywhere inside the driver core (a port stuck in reset loops against dead silicon, a CAM-completion path holding ap_lock, a stuck sili_pwait retry), the detach path (sili_port_free β†’ sili_os_stop_port, sili.c:674) can never complete and kldunload sili/shutdown hangs forever. The identical pattern exists in the ahci sibling (ahci_os_stop_port) this file was derived from.

Fix validation (full runtime A/B, patched kernel)

fix.diff bounds the wait at 60 s and converts the hang into a leak-not-hang: sili_os_stop_port() returns ETIMEDOUT, sili_port_free() skips freeing that port (keeps the memory the thread references valid), and sili_pci_detach() keeps the controller resources and returns EBUSY so kldunload fails cleanly instead of hanging.

Patched kernel 6.5-DEVELOPMENT #1 Sat Sep 26 10:36:11 2026 (fix_build.log: quickkernel rc=0, -Werror):

  • norelease mode (run.3.log): stopper spins exactly 60 s (12Γ—5 s observations), then the fixed kernel code itself prints pci0.1: thread did not terminate; leaking port to avoid unload hang and sili_os_stop_port RETURNS; guest stays up, module unloads.
  • normal mode (run.4.log): E1/E2/E3 all behave as before β€” the fix does not slow or break the normal stop path (E2's stopper returns promptly after the wedge release, as on baseline).

Baseline (run.2.log) vs patched (run.3.log) on otherwise identical guests is the A/B pair.

Post-observation incident (harness artifact, not the finding)

After the first patched norelease observation completed, the runner mistakenly kldunloaded the module while the intentionally-leaked wedge kthread still referenced module memory; that thread later executed unmapped module text (IP 0xffffffff826000da, module range) and panicked the guest (Fatal trap 12, serial console). All decisive evidence had already been written; the guest was reset to the clean-source snapshot afterwards. The incident is unrelated to the finding's mechanism (it demonstrates only that one must not unload a module that deliberately leaks threads β€” which is exactly the situation the fixed driver leaves the real kernel in, minus the unload, since the real module refuses unload via EBUSY).

Gating precondition

sili(4) controller present + a port helper thread wedged in the driver core (dead/misbehaving silicon, stuck CAM path), triggered by kldunload sili or system shutdown. Root-triggered DoS (availability), no escalation path β€” hence Low severity.

Exploit chain

Not applicable (hang class; the trigger requires privileged unload/shutdown plus a wedged device).

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 kernel #0: wedge-forever stopper spins indefinitely (90s observation window exhausted, 'STILL SPINNING' 18x). Patched kernel #1 (quickkernel with fix.diff, -Werror, rc=0; fix_build.log): same test returns at the 60s bound with the fix's own message 'pci0.1: thread did not terminate; leaking port to avoid unload hang'; module unloads; guest healthy. Normal-mode regression check (run.4.log): E1 normal stop still returns immediately, E2 unwinds after wedge release exactly as baseline - fix affects only the wedged case.

run.3.log (patched wedge-forever: 12x5s then bound+message+RETURNED); run.2.log (baseline wedge-forever: 18x5s STILL SPINNING); run.4.log (patched normal mode); fix_build.log (kernel build rc=0)
↓ fix.diffper-fix-DF-5044

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (E2: driver's 'pci0.1: Waiting for thread to terminate' + watchdog 5/10/15s spin, clean unwind after wedge release); run.2.log (baseline wedge-forever: 18x5s 'STILL SPINNING' = spin-forever); run.3.log (patched kernel wedge-forever: 12x5s spin then 'pci0.1: thread did not terminate; leaking port to avoid unload hang' + stopper RETURNED); run.4.log (patched kernel normal mode: fix changes nothing for healthy threads); fix_build.log (quickkernel rc=0); VERDICT.md

PoC changes

Self-authored from the DF-4964 technique: real sili_port_alloc() against a fake BAR window with real DMA tags (lowaddr must be BUS_SPACE_MAXADDR like the real driver - DF-4964's own tag args caused NOSGET), two real helper threads, wedge via lockmgr(ap_lock)+AP_SIGF_TIMEOUT (thread blocks in sili_port_thread_core -> sili_os_lock_port at sili.c:1601), stopper/watchdog kthreads, and a wedge-forever norelease mode for the baseline-vs-fix A/B.

Verified recommended fix

Bound the wait (60s), then leak-not-hang: sili_os_stop_port returns ETIMEDOUT, sili_port_free skips freeing the stuck port, sili_pci_detach keeps controller resources and returns EBUSY so kldunload fails cleanly (see fix.diff; mirrors should be applied to ahci)

Verdict

sili_os_stop_port() (sili_dragonfly.c:208-224) waits for the per-port helper thread with no bound: 'while (ap->ap_thread) sili_os_sleep(100)'. A helper thread wedged anywhere in the driver core (stuck reset/pwait loops against dead silicon, blocked behind a held ap_lock) therefore turns kldunload sili / system shutdown into a permanent kernel hang, since the waiter is the unload path itself. Reproduced at runtime on the guest WITHOUT hardware: sili is compiled into X86_64_GENERIC, so the module built REAL ports via the kernel's own sili_port_alloc() (DF-4964 fake-MMIO technique), started REAL sili_port_thread helpers, parked one behind a held ap_lock, and called the kernel's own sili_os_stop_port() - the driver printed its own 'Waiting for thread to terminate' and spun for the entire observation window (>=15s bounded-proof run; 90s+ in wedge-forever mode, i.e. forever). Full fix A/B: fix.diff bounds the wait at 60s and converts the hang into leak-not-hang (stop_port returns ETIMEDOUT, port_free skips the port, sili_pci_detach keeps controller resources and returns EBUSY). On the rebuilt patched kernel (#1 Sep 26 10:36) the same wedge-forever test showed the fixed code's own 'pci0.1: thread did not terminate; leaking port to avoid unload hang' at t=60s, stopper returned, guest healthy, module unloaded; normal stops (E1/E2/E3) unaffected. Root-triggered availability bug (unload/shutdown), hardware-misbehaviour-conditioned - Low severity, no escalation path.