⬢ DragonFlyBSD Kernel Audit
DF-4535 / verdict.json
← back to finding ↓ download raw
{
  "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"
}