DF-4535 / verdict.json
{ "finding_id": "DF-4535", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "lptwrite() drops the uiomove() return value (lpt.c:763): a write() whose user buffer faults mid-stream makes the driver strobe out the stale 1024-byte kernel buffer sc_inbuf over and over (133,120 stale bytes captured on the wire in the IRQ-mode baseline; 2.18 MB in the polled run) while the syscall never returns. In polled mode the livelocked write sleeps holding the kernel mplock (lpt dev_ops flags=0), which starved every non-MPSAFE path on the guest: SIGKILL was ignored and new ssh sessions died at banner exchange - a system-wide DoS requiring a reset. With the node at 0600 root:wheel the trigger needs root, so severity stays Low; the leak facet (kernel heap bytes to the physical port) and the unkillable-livelock facet are both proven on the wire via the QEMU file-backed parallel port. fix.diff (check uiomove, bail with EFAULT) validated on the guest: write returns -1/EFAULT, capture is exactly the 8192 valid bytes with zero stale tail, normal writes unaffected.", "exploit_chain": "root on guest -> open /dev/lpt0 -> write(8192) from a buffer whose second page is PROT_NONE -> uiomove faults forever, ignored by lptwrite -> kernel re-transmits stale sc_inbuf content to the port indefinitely (kernel-heap -> physical-port leak) while the write syscall livelocks; polled variant (ioctl LPT_IRQ=0 then reopen) holds the mplock while sleeping -> SIGKILL ignored, new ssh logins starve, guest must be reset. No unprivileged escalation (device node 0600 root:wheel).", "evidence": "run.log (three full transcripts: IRQ baseline with byte-composition analysis, polled wedge, patched re-run); leak_sample_irq.bin (141,333 raw port bytes: 21 smoke + A*4096 + B*4096 + 133,120 stale); leak_sample_polled_head64k.bin; bootlog-wedge-tail.txt (no panic, system wedged); fix_capture_patched.bin (exactly A*4096+B*4096, zero stale); fix.diff; VERDICT.md", "kernel_refs": [ "sys/dev/misc/lpt/lpt.c:761", "sys/dev/misc/lpt/lpt.c:763", "sys/dev/misc/lpt/lpt.c:764", "sys/dev/misc/lpt/lpt.c:657", "sys/dev/misc/lpt/lpt.c:191", "sys/kern/kern_device.c:119", "sys/kern/kern_device.c:142" ], "poc_changes": "Trigger written from scratch for this finding (no seed). Polled-mode switcher (polled_switch.c, ioctl LPT_IRQ) added after the IRQ-mode run; evidence capture uses the QEMU file-backed parallel chardev read from /proc/<qemu>/fd/11 on the host.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 1500, "guest_dirty": 0, "build_cmd": "cc -O -Wall -o lpt_uioleak lpt_uioleak.c && cc -O -Wall -o polled_switch polled_switch.c", "run_cmd": "./lpt_uioleak 10 (polled variant: ./polled_switch && ./lpt_uioleak 10)", "code_hash": "909ef35d41f538d49566fabfc92a0ca035343d324192e7b7487014ab0d30e477", "notes": "Baseline runs were on the first guest boot; the guest was force-reset after the polled-mode mplock wedge, so the baseline /root logs were wiped - the decisive outputs (byte-composition analyses, ps states, ssh failure text, boot.log tail) are preserved verbatim in run.log and the raw capture binaries. GENERIC does not compile lpt/ppc; modules were built from the guests unmodified /usr/src/sys (boot #1) for the baseline and from the patched tree for the fix run.", "recommended_fix": "Check the uiomove() return value in lptwrite() and return the error (see fix.diff); separately consider PCATCH on the polled tsleep and a D_MPSAFE audit.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to the guests /usr/src/sys/dev/misc/lpt/lpt.c, lpt.ko rebuilt and installed, guest rebooted into it: the exact baseline PoC now returns write()=-1/EFAULT after ~1s, the port capture contains exactly the 8192 valid bytes with ZERO stale bytes, and a normal write still works. Baseline (unpatched) behavior was re-produced in the same session before the fix.", "fix_evidence": "fix_capture_patched.bin (8192 bytes = A*4096+B*4096, verified by script); patched-run transcript in run.log; fix.diff" } |