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

ppiwrite() ignores the uiomove() return value: stale ppi_buffer bytes are handed to byte_peripheral_write() and a fully-faulting write never terminates (DF-4535 lpt twin, IEEE1284-peripheral side)

Summary

uiomove's rc is dropped and len bytes are transmitted regardless of what was copied - a faulting user buffer transmits stale prior-write content and a zero-copy fault re-sends the stale chunk indefinitely (PCATCH-killable, unlike lpt). Exact line-shape twin of runtime-verified DF-4535; binary-proven in the shipped module (uiomove rc never tested); write-path entry demonstrated live (blocks S3 wchan=ppiwrite). Stale-byte facet needs a physical 1284 master peer - honestly not_reproduced for that facet. check-and-bail fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4807 Β· 8 files
FileTypeDescriptionSize
ppiwrite_uio.c β€” 2.2 KB view raw
ppiwrite.dis β€” 2.8 KB ↓ download
run.sh β€” 164 B view raw
run.log β€” 1.1 KB view raw
build.log β€” 693 B view raw
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
verdict.json β€” 2.7 KB view raw

DF-4807 β€” ppiwrite() ignores the uiomove() return value: stale ppi_buffer bytes are handed to byte_peripheral_write() and a fully-faulting write never terminates (DF-4535 lpt twin, 1284-peripheral side)

File: sys/dev/misc/ppi/ppi.c line 478 (pass 2)

    /* negotiation done, write bytes to master host */
    while ((len = (int)szmin(uio->uio_resid, BUFSIZE)) != 0) {
        uiomove(ppi->ppi_buffer, (size_t)len, uio);     /* rc DROPPED */
        if ((error = byte_peripheral_write(ppbus,
                        ppi->ppi_buffer, len, &sent)))
            goto error;
    }

This is the exact DF-4535 shape (lpt.c:763, runtime-verified): a faulting user buffer leaves the tail of the 512-byte kernel ppi_buffer stale (previous write's data; probe-time zeros on first use), and byte_peripheral_write transmits the full len bytes to the IEEE1284 master host. A fully-faulting uiomove (0 bytes copied) leaves uio_resid unchanged, so the loop re-sends the stale chunk forever β€” though unlike lpt's polled mode, this loop's exits are the wire handshake timeouts in byte_peripheral_write (ppb_poll_bus PPB_FOREVER waits on the remote master, PCATCH-interruptible) rather than an unkillable Giant-held livelock.

Reproduce boundary (honest status: not_reproduced for the leak facet)

The transmit loop (ppi.c:477) is only reachable after the local host enters IEEE1284 peripheral mode β€” PPB_PERIPHERAL_IDLE β€” which requires a remote master host to complete the 1284 negotiation handshake (ppi.c:450-471). The QEMU guest's parallel port is an SPP-style emulation with no 1284 master peer, so on this guest:

  • write() demonstrably enters the path and blocks in the negotiation wait: process state S3, wchan ppiwrite (tsleep ppi.c:459, PCATCH), SIGKILL terminates it (run.log).
  • The stale-byte transmission itself needs physical peer hardware.

What IS proven beyond the source line:

  • the module binary drops the check β€” objdump -dr (ppiwrite.dis): callq uiomove at 0x58e/0x59e, return in %eax never tested, the very next call is byte_peripheral_write at 0x5a0 whose return IS tested (test %eax,%eax; jne at 0x5a8);
  • the identical code shape was runtime-verified for lpt (DF-4535: 133,120 stale bytes strobed out the port in IRQ mode on this same guest).

Build / run (root, ppi.ko with PERIPH_1284 loaded)

cc -O -Wall -o ppiwrite_uio ppiwrite_uio.c
./ppiwrite_uio &      # blocks in the 1284 negotiation wait
ps ax -o pid,stat,wchan,comm | grep ppiw
kill -9 <pid>         # PCATCH: terminated

Impact

Low: device node 0600 root:wheel + options PERIPH_1284 (LINT64-only) + a physical 1284 master peer. Stale kernel-buffer bytes (not kernel heap at large β€” ppi_buffer is only ever filled by uiomove/ppb_1284_read) leave the machine over the parallel cable; a faulting write spins the retransmit loop (bounded by wire timeouts, killable). Defense-in-depth: check uiomove and bail.

VERDICT.md
↓ download raw

DF-4807 β€” VERDICT

Status: not_reproduced (the stale-byte transmission facet is hardware-gated on this guest: it requires an IEEE1284 master peer to complete the peripheral-side negotiation before ppiwrite's data loop is reachable). reproduced=0, impact=none as demonstrable on this guest; the code defect is certain, and its runtime consequence is proven by the line-identical, runtime-verified twin DF-4535 (lpt).

Root cause (path:line)

sys/dev/misc/ppi/ppi.c:478 β€” uiomove(ppi->ppi_buffer, len, uio); return value discarded; byte_peripheral_write(ppbus, ppi->ppi_buffer, len, &sent) (ppi.c:479-480) transmits len bytes regardless of how many uiomove actually copied. On EFAULT: stale ppi_buffer content (prior write's bytes) is transmitted to the 1284 master; if uiomove copies 0 bytes, uio_resid never shrinks and the loop re-sends the stale chunk indefinitely (bounded only by the wire handshake timeouts / PCATCH). Same defect class as DF-4535 (sys/dev/misc/lpt/lpt.c:763, reproduced live: 133,120 stale bytes out the port).

What was verified live (ppi.ko built with PERIPH_1284, pristine

sources, guest 6.5-DEVELOPMENT X86_64_GENERIC, ppc0 0x378 irq7)

  1. Path entry + block: write(fd, faulting_buf, 8192) blocks in the kernel at ppi.c:459 β€” ps: S3 wchan=ppiwrite; SIGKILL terminates (PCATCH sleep, unlike lpt's polled-mode unkillable loop). The data loop at ppi.c:477 requires PPB_PERIPHERAL_IDLE, which requires a remote 1284 master host β€” absent on QEMU (SPP emulation only).
  2. Binary proof of the dropped check (ppiwrite.dis, objdump -dr of the loaded module): 58e: callq ... R_X86_64_PLT32 uiomove-0x4 593: mov %r15,%rsi <- no test of %eax (uiomove rc) ... 5a0: callq ... R_X86_64_PLT32 byte_peripheral_write-0x4 5a5: mov %eax,%r15d 5a8: test %eax,%eax <- THIS rc is checked
  3. Sibling verification cross-reference: DF-4535 evidence pack (same guest, same port) demonstrates the identical dropped-uiomove shape transmitting stale kernel bytes out the parallel port in lpt.

Exploit chain

None. Privileged-only (0600 root:wheel), PERIPH_1284-gated, peer-hardware-gated; impact ceiling is stale-buffer disclosure over the parallel cable plus a killable retransmit spin. Fix is defensive.

Fix (not hardware-validated on this guest β€” trivial check-and-bail)

    while ((len = (int)szmin(uio->uio_resid, BUFSIZE)) != 0) {
-       uiomove(ppi->ppi_buffer, (size_t)len, uio);
+       if ((error = uiomove(ppi->ppi_buffer, (size_t)len, uio)))
+           goto error;
        if ((error = byte_peripheral_write(ppbus,
                        ppi->ppi_buffer, len, &sent)))
            goto error;

(Validating this end-to-end requires a 1284 master peer; the fix is the same check-and-bail validated for DF-4535.)

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

End-to-end fix validation requires IEEE1284 master-peer hardware (absent on the QEMU guest); the proposed check-and-bail is identical to the fix validated for DF-4535.

VERDICT.md 'Fix' section
per-fix-DF-4807

Confirmed kernel references

Detail

Evidence (decisive lines)

ppiwrite.dis (objdump -dr of the loaded module: uiomove rc dropped); run.log (write() blocks at ppi.c:459 S3/wchan=ppiwrite, SIGKILL_TERMINATED); README.md cross-reference to the DF-4535 evidence pack

PoC changes

PoC authored fresh (no seed); note signal(3) installs SA_RESTART on DF so a caught signal restarts the blocked write - the block/kill demo must use SIGKILL.

Verified recommended fix

Check the uiomove() return at ppi.c:478 and goto error on failure (same check-and-bail as the validated DF-4535 fix).

Verdict

ppiwrite() discards the uiomove() return (ppi.c:478) and hands the full len bytes of the kernel ppi_buffer to byte_peripheral_write regardless of how many were copied - the DF-4535 (lpt.c:763) twin on the 1284-peripheral side, proven in the module binary (objdump: uiomove call at 0x58e with %eax never tested before byte_peripheral_write at 0x5a0, whose rc IS tested at 0x5a8). The stale-byte transmission facet could not be exercised on the QEMU guest: reaching the data loop requires PPB_PERIPHERAL_IDLE, i.e. a remote IEEE1284 master host to complete the peripheral negotiation, and the SPP emulation provides none - the write demonstrably enters the driver and blocks in the negotiation wait (S3, wchan=ppiwrite, SIGKILL-killable via PCATCH; run.log). Confidence certain: the dropped check is in the shipped binary and the identical code shape was runtime-verified transmitting 133,120 stale bytes for lpt (DF-4535) on this same guest/port.