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)
PoC verification
Evidence pack
findings/poc/DF-5044 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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, signalsAP_SIGF_TIMEOUT; the helper thread wakes and blocks insidesili_port_thread_core() β sili_os_lock_port()(sili.c:1601). A stopper kthread callssili_os_stop_port(port1): the driver prints its ownpci0.1: Waiting for thread to terminate(sili_dragonfly.c:215-216) and spins inwhile (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 printspci0.1: thread did not terminate; leaking port to avoid unload hangandsili_os_stop_portRETURNS; 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
fixedBaseline 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)
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.
No comments yet.