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)
PoC verification
Evidence pack
findings/poc/DF-4535 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- 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 - never terminates when the fault persists: the next loop
iteration calls uiomove() at the faulting address again, copies 0
bytes,
uio_residnever 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 -9is 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) β seeleak_sample_irq.bin(offsets 4117..141333 after the 21-byte smoke write; analysis inrun.log). - Polled mode: same, plus
kill -9cannot 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-2write()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).
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
nbytes fromsc_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_residnever 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/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 normalprintf > /dev/lpt0write still works ("NORMAL-WRITE-STILL-OK" on the wire). - fix_status: fixed.
Kernel references
- sys/dev/misc/lpt/lpt.c:761-764 (bug: uiomove ignored)
- sys/dev/misc/lpt/lpt.c:657 (polled sleep without PCATCH)
- sys/dev/misc/lpt/lpt.c:191-198 (dev_ops flags 0 -> mplock held)
- sys/kern/kern_device.c:119-156 (dev_needmplock / dev_dopen mplock wrap)
- sys/kern/sys_generic.c:530-675 (write syscall -> devfs -> d_write)
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
fixedfix.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
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/
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.
No comments yet.