# DF-4534 — VERDICT

**Status: not_reproduced** (bounded hammer; the race is structurally
real but produces no externally observable failure signature — the
UAF read is silent — so a runtime reproduction cannot be demonstrated
in bounded time).

## Root cause (path:line)

* `lptclose()` frees `sc_inbuf`/`sc_statbuf` at
  `sys/dev/misc/lpt/lpt.c:598-599` **before** the interrupt handler is
  torn down: the teardown (`BUS_TEARDOWN_INTR`) happens inside
  `ppb_release_bus()` (`sys/bus/ppbus/ppbconf.c`, ppb_release_bus),
  which lptclose only calls at `lpt.c:605`.
* `callout_stop()` at `lpt.c:596` does not wait for a callout that is
  currently executing on another CPU; `lptout`
  (`lpt.c:406-437`) re-arms itself at 1-tick intervals (DF-1680) and
  invokes `lptintr` → `lpt_intr`, which dereferences `sc->sc_cp` at
  `lpt.c:856` whenever `sc_xfercnt > 0`.  The callout path takes no
  mplock (only `crit_enter`, `lpt.c:889`), so it races lptclose freely.
* The write/close syscall pair cannot overlap (the write syscall holds
  a file reference, so `d_close` runs only after write returns), but
  the callout and hardware-IRQ contexts are not reference-counted:
  they are exactly the contexts that can be inside `lpt_intr` when
  the kfree executes.  (The hardware IRQ itself is serialized against
  close by the mplock — dmesg "interrupt uses mplock: lpt0" — but the
  *callout* path is not, and the IRQ-vs-kfree window (a) above closes
  only at `lpt.c:605`.)

Precondition chain (all reproducible): interrupt-driven port, write
interrupted by a signal with `sc_xfercnt > 0` (`lpt.c:585` then skips
the drain loop), close → immediate `callout_stop` + `kfree`.

## What was run

`close_lifetime 300` on the guest (IRQ-driven lpt0 under QEMU): each
cycle opens the device, forks a writer of 16 MB, interrupts it with
SIGUSR1 after 60 ms (write returns 1024 = one drained chunk + pending
chunk, `INTERRUPTED` set), waits, then closes → the kfree windows are
entered with the callout armed at 1 tick and the byte-strobe IRQ
chain live.  Result: 300/300 cycles clean — no panic, no dmesg
anomaly, `vmstat -m lpt` count returned to 0.

Window math (why silent): for the UAF deref to land in the freed
chunk, an in-flight `lpt_intr` must sit between its status spin and
the `*sc->sc_cp++` during the ~0.1-1 µs between `callout_stop` /
kfree and the teardown.  `lptout` fires every 10 ms and its body is a
few µs → ~5e-6 hit probability per cycle; a hit reads one byte of the
still-intact freed M_LPT chunk (nothing else allocates from that zone
concurrently) and strobes it to the port — invisible from outside.
Escalation to an observable leak requires the chunk to be recycled
into a different zone within the same ~µs window.

## Fix

`fix.diff`: stop + `callout_drain()` the callout, move both kfree's
behind `lpt_release_ppbus()` (i.e. after `BUS_TEARDOWN_INTR`), NULL
the pointers, guard against the BYPASS path freeing never-allocated
buffers.  Validated as git-apply-clean against the tree; runtime fix
validation not applicable (no observable baseline failure).

## Kernel references

- sys/dev/misc/lpt/lpt.c:596-599 (callout_stop + kfree before teardown)
- sys/dev/misc/lpt/lpt.c:605 (release = intr teardown, too late)
- sys/dev/misc/lpt/lpt.c:406-437 (lptout re-arm; calls lptintr)
- sys/dev/misc/lpt/lpt.c:856 (`ppb_wdtr(ppbus, *sc->sc_cp++)`)
- sys/bus/ppbus/ppbconf.c (ppb_release_bus: BUS_TEARDOWN_INTR)

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