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

lptwrite() ignores uiomove() failure: stale kernel-heap bytes transmitted out the parallel port and a non-terminating write (unkillable, mplock-starvation system DoS in polled mode)

Summary

The uiomove return is discarded and xfercnt=n set unconditionally: for a write whose user buffer faults partway the driver transmits the full n bytes from the kernel buffer - bytes beyond what uiomove copied are stale heap - and each loop iteration re-transmits the stale chunk forever (uio_resid stops shrinking). REPRODUCED on a live QEMU lpt0 with a file-backed parallel-port wiretap: 133,120 stale bytes strobed after the valid data in IRQ mode; polled mode kill -9 ignored, 2.18M stale bytes, and the guest stopped accepting ssh (mplock starvation) until force-reset. Fix validated on the rebuilt module: EFAULT after ~1s, zero stale bytes, normal writes intact. Check-and-bail fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4535 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
lpt_uioleak.c β€” 2.4 KB view raw
polled_switch.c β€” 384 B view raw
build.sh β€” 143 B view raw
run.sh β€” 97 B view raw
run.log β€” 2.2 KB view raw
env.txt β€” 713 B view raw
leak_sample_irq.bin β€” 138.0 KB ↓ download
leak_sample_polled_head64k.bin β€” 64.0 KB ↓ download
fix_capture_patched.bin β€” 8.0 KB ↓ download
fix.diff β€” 608 B view raw
verdict.json β€” 4.2 KB view raw
bootlog-wedge-tail.txt β€” 2.0 KB view raw

DF-4535 β€” lptwrite() ignores uiomove() failure: stale-kernel-heap transmission + unkillable livelock (system-wide DoS in polled mode)

File: sys/dev/misc/lpt/lpt.c lines 761-764 (pass 2)

while ((n = (unsigned)szmin(BUFSIZE, uio->uio_resid)) != 0) {
        sc->sc_cp = sc->sc_inbuf;
        uiomove(sc->sc_cp, (size_t)n, uio);      /* return value DROPPED */
        sc->sc_xfercnt = n;

uiomove() copies at most n bytes and returns EFAULT when the user buffer faults. lptwrite() ignores the error and sets sc_xfercnt = n regardless, so the driver:

  1. transmits bytes beyond what was copied β€” the remainder of the 1024-byte kernel buffer sc_inbuf (stale content of the previous write; uninitialized slab memory on first use) is strobed out the parallel data port (kernel heap -> physical port info leak), and
  2. never terminates when the fault persists: the next loop iteration calls uiomove() at the faulting address again, copies 0 bytes, uio_resid never shrinks, and the loop spins forever, each pass re-transmitting the full stale 1024-byte chunk.

In polled mode the inner sleep (lpt_pushbytes, lpt.c:657) has no PCATCH, and the whole d_write runs under the kernel mplock (lpt's dev_ops flags = 0, so dev_needmplock() is true, sys/kern/kern_device.c:119-122): the livelocked write sleeps holding Giant, starving every non-MPSAFE path in the kernel β€” on the test guest this killed the ability to establish new ssh sessions (system-wide denial of service).

Reproduce (as root on the guest, lpt0 attached)

Build:

cc -O -Wall -o lpt_uioleak lpt_uioleak.c

Run (IRQ-driven port β€” default when the parallel port has an IRQ):

./lpt_uioleak 10

Polled variant (unkillable + mplock starvation):

./polled_switch      # ioctl(LPT_IRQ, 0) on /dev/lpt0.ctl; next open is polled
./lpt_uioleak 10     # then: kill -9 does NOT work; new ssh logins fail

Expected / observed (baseline, unpatched lpt.ko)

  • IRQ mode: write() never returns; process state R4 (kernel); kill -9 is required and works (PCATCH at lpt.c:796). Host-side port capture (QEMU -parallel chardev:file): after the 4096 valid 'B' bytes, 130+ extra 1024-byte blocks of stale buffer content (133,120 bytes, all 0x42) β€” see leak_sample_irq.bin (offsets 4117..141333 after the 21-byte smoke write; analysis in run.log).
  • Polled mode: same, plus kill -9 cannot terminate the writer and the system stops accepting new ssh sessions ("Connection timed out during banner exchange"); 2,183,288 stale bytes captured before the guest was force-reset (leak_sample_polled_head64k.bin, bootlog-wedge-tail.txt).

Fix validation (fix.diff applied to guest /usr/src, lpt.ko rebuilt)

  • ./lpt_uioleak 10 β†’ phase-2 write() returns -1/EFAULT after ~1 s.
  • Port capture delta = exactly 8192 bytes = 'A'4096 + 'B'4096 β€” zero stale bytes (fix_capture_patched.bin).
  • Normal write still works ("NORMAL-WRITE-STILL-OK" captured).

Environment

DragonFly 6.5-DEVELOPMENT X86_64_GENERIC #0 Thu Jul 2 06:02:54 UTC 2026, QEMU/KVM x86_64, 6 vCPU, isa-parallel (0x378, irq 7) with -chardev file backend; kernel modules ppbus.ko/lpt.ko/ppc.ko built from the guest's /usr/src/sys (see env.txt).

VERDICT.md
↓ download raw

DF-4535 β€” VERDICT

Status: reproduced (impact: dos; leak facet proven; severity Low β€” device node is 0600 root:wheel, so reaching lptwrite requires root or equivalent on the node).

Root cause (path:line)

sys/dev/misc/lpt/lpt.c:763 β€” the return value of uiomove() is discarded; sc->sc_xfercnt = n (lpt.c:764) is set unconditionally. On a faulting user buffer, uiomove copies 0 (or a partial prefix), and lptwrite:

  • transmits the full n bytes from sc_inbuf β€” bytes beyond what was copied are stale heap content from the previous write (proven) or uninitialized slab memory (first use after open);
  • loops forever because uio_resid never decreases once uiomove copies 0 bytes: n = szmin(BUFSIZE, uio->uio_resid) (lpt.c:761) stays non-zero.

Amplifier: lpt's dev_ops has flags 0 (lpt.c:191-198), so dev_needmplock() (sys/kern/kern_device.c:119) is true and all d_write/d_read/d_open/d_close run under the mplock (sys/kern/ kern_device.c:142-156). In polled mode lpt_pushbytes sleeps with Giant held (tsleep flags=0, lpt.c:657) β€” the livelocked syscall starves every non-MPSAFE kernel path.

How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC kernel + module build of ppbus/ppc/lpt from the same tree (GENERIC does not compile lpt in). QEMU -chardev file,id=par0,... -parallel chardev:par0 gives a PC-style LPT at 0x378 irq 7 whose SW emulation (QEMU hw/char/parallel.c) asserts the "printer ready" status pattern after an INIT-low control write β€” lptopen succeeds and strobed data bytes land verbatim in the host-side file = a perfect wiretap of the port.

Run 1 (IRQ mode, baseline module):

$ ./lpt_uioleak 10
[*] opened /dev/lpt0 (irq mode)
[*] phase1: wrote 4096 'A' bytes (kernel buffer now 'A')
[*] phase2: write(fd, 8192) with PROT_NONE guard page at 4096
[*] expecting syscall to NEVER return; will be killed after 10s

Process observed in state R4 (kernel) after 8 s; SIGKILL required. Port capture (host /proc//fd/11): 21-byte smoke write, then 'A'4096, 'B'4096 (valid), then 133,120 bytes of stale content (130 x 1024-byte blocks, all 0x42) β€” the buffer content from the last successfully copied chunk, re-transmitted continuously until the kill. Analysis transcript in run.log; raw bytes in leak_sample_irq.bin.

Run 2 (polled mode via ioctl(LPT_IRQ, 0) + reopen, baseline module):

kernel: lpt0: switched to polled standard mode

After ~90 s the guest stopped accepting ssh connections ("Connection timed out during banner exchange" β€” TCP accepted, sshd child starved); the console showed no panic. 2,183,288 stale bytes were strobed out before the wedge completed. kill -9 had been sent 8 s in and was ignored (no PCATCH on the polled path). Guest had to be force-reset. Evidence: bootlog-wedge-tail.txt, first 64 KB of the polled capture in leak_sample_polled_head64k.bin.

Exploit chain

Not a corruption primitive. As shipped, the node is 0600 root:wheel, so this is a root-triggerable robustness/DoS/leak defect, not an unprivileged escalation. Impact if the node permissions are relaxed (printer spoolers, lp groups): unkillable process + kernel mplock starvation (system-wide DoS in polled mode) and an unbounded kernel heap -> physical-port leak channel.

Fix validation (mandatory-class: leak/DoS with reproduced behavior)

fix.diff (also applied to the guest's /usr/src copy β€” never to the audit tree): check uiomove and bail out with the error.

  • Baseline re-produced before the fix (Run 1/Run 2 above).
  • Patched lpt.ko: write() returns -1/EFAULT after ~1 s; port capture delta exactly 8192 bytes = 'A'4096 + 'B'4096, ZERO stale bytes (fix_capture_patched.bin); a normal printf > /dev/lpt0 write still works ("NORMAL-WRITE-STILL-OK" on the wire).
  • fix_status: fixed.

Kernel references

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

fix.diff applied to the guest's /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_capture_patched.bin (8192 bytes = A*4096+B*4096, verified by script); patched-run transcript in run.log; fix.diff
↓ fix.diffper-fix-DF-4535

Confirmed kernel references

Detail

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 (decisive lines)

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

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//fd/11 on the host.

Verified 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.

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.