Unprivileged unkillable D-state sleep: pcfclock_open() requests the ppbus with bare PPB_WAIT (= PPB_WAIT|PPB_NOINTR) on world-readable /dev/pcfclock0
Summary
PPB_NOINTR==0 so bare PPB_WAIT selects the non-PCATCH infinite tsleep on ppbreq - any user can open the 0444 device and hold the ppbus, wedging every blocking opener unkillably (even root cannot reap) plus a ppbus outage for lpt/ppi/vpo. Every other ppbus consumer passes PPB_WAIT|PPB_INTR. FULLY VERIFIED on guest: custom PCFTEST kernel + isa-parallel, reproduced 2/2 as uid 1001 (D0 ppbreq survives kill -9), one-line fix validated on rebuilt kernel (immediate reap). vpoio.c:616 same bare-PPB_WAIT (routed).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5029 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| pcf_dos.c | β | 2.4 KB | view raw | |
| run_test.sh | β | 1.5 KB | view raw | |
| build.sh | β | 222 B | view raw | |
| build.log | β | 122 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| run.2.log | β | 1.0 KB | view raw | |
| fix_run.log | β | 967 B | view raw | |
| fix_build.log | β | 660 B | view raw | |
| env.txt | β | 1.0 KB | view raw | |
| fix.diff | β | 342 B | view raw | |
| verdict.json | β | 4.2 KB | view raw |
Fix verification
fixedfix.diff (PPB_WAIT|PPB_INTR) applied to guest /usr/src, kernel PCFTEST rebuilt and installed; identical test as the same unprivileged user now shows kill -9 reaping the blocked victim immediately while the holder still owns the bus; EWOULDBLOCK non-blocking and EIO read semantics unchanged. Bad behavior eliminated.
fix_run.log ('RESULT: victim died on kill -9 (bug NOT reproduced)' on kernel #1 05:45:18) vs run.log/run.2.log ('SURVIVED kill -9' on kernel #0 05:30:21); fix.diff applies cleanly to pristine sys/ (patch --dry-run verified); fixed kernel install completed 05:48:46 UTC (fix_build.log).
Confirmed kernel references
Detail
Exploit chain
unprivileged open(/dev/pcfclock0) [holder] -> takes permanent ppbus ownership -> victim blocking-open sleeps in non-PCATCH tsleep 'ppbreq' -> kill -9 ineffective (pending only) -> repeat to accumulate unkillable D-state processes; also stalls lpt0/ppi0/vpo0 consumers for the whole time.
Evidence (decisive lines)
run.log and run.2.log: 'RESULT: victim pid 847 SURVIVED kill -9 (unkillable D-state sleep)' + ps row '847 D0 ppbreq ./pcf_dos victim' + 'victim_nb: open returned fd=-1 errno=35' + 'post-release: victim reaped once the ppbus was released'. fix_run.log (kernel PCFTEST #1 with fix.diff): 'RESULT: victim died on kill -9'. env.txt: pcfclock0 cr--r--r-- root:wheel attach lines.
PoC changes
n/a - trigger written fresh for this finding (no seed). Guest environment had to be constructed: stock X86_64_GENERIC lacks pcfclock, so a custom kernel (PCFTEST = GENERIC + ppc0/ppbus/lpt/ppi/pcfclock) was built on the guest and QEMU was started with -device isa-parallel to attach a parallel port. First generated fix.diff had an unbalanced paren (caught by the guest kernel build, fixed and re-validated).
Verified recommended fix
Pass PPB_WAIT | PPB_INTR (like ppi.c/pps.c/if_plip.c) instead of bare PPB_WAIT in pcfclock_open().
Verdict
Unprivileged local user can create unkillable D-state processes at will: pcfclock_open() (sys/dev/misc/pcfclock/pcfclock.c:152-153) requests the ppbus with bare PPB_WAIT, which equals PPB_WAIT|PPB_NOINTR (sys/bus/ppbus/ppbconf.h:125-128), so ppb_request_bus() (sys/bus/ppbus/ppbconf.c:474-476) tsleeps on 'ppbreq' with no PCATCH and no timeout while any owner holds the bus. Because /dev/pcfclock0 is 0444 root:wheel (pcfclock.c:133-134), any user can hold the bus forever, and every other process that blocking-opens the device sleeps unkillably until the holder releases; SIGKILL stays pending. Reproduced 2/2 on a custom-kernel guest with a QEMU ISA parallel port: victim STAT D0, wchan ppbreq, survived kill -9 while held, reaped only after release; O_NONBLOCK control gave EWOULDBLOCK. Not memory corruption - DoS only, hardware-gated (parallel port + kernel with pcfclock), hence Low severity.
No comments yet.