# DF-4534 — lptclose() frees lpt buffers while the lptout callout and the IRQ handler can still execute

File: `sys/dev/misc/lpt/lpt.c` (pass 2)

`lptclose()`:

* clears `OPEN` (lpt.c:582), then — after the optional drain loop —
  `callout_stop(&sc->sc_callout)` (lpt.c:596) and immediately
  `kfree(sc->sc_inbuf/sc_statbuf)` (lpt.c:598-599);
* only afterwards calls `lpt_release_ppbus()` (lpt.c:605), which is
  what unregisters the interrupt handler (BUS_TEARDOWN_INTR in
  sys/bus/ppbus/ppbconf.c ppb_release_bus()).

Two lifetime windows result:

1. **callout window** — `callout_stop()` does not wait for a callout
   that is executing on another CPU.  `lptout` re-arms itself
   (lpt.c:416-420; at 1-tick intervals due to the DF-1680 clamp typo)
   and calls `lptintr()` → `lpt_intr()`, which dereferences
   `sc->sc_cp` (lpt.c:856) when `sc_xfercnt > 0`.  The callout path
   runs *without* the mplock, so it is not serialized against
   lptclose.
2. **IRQ window** — between the kfree (lpt.c:598-599) and the teardown
   inside `ppb_release_bus` (lpt.c:605), the registered handler is
   still live.  With an interrupted write pending (`INTERRUPTED` set,
   lpt.c:585 skips the drain loop), `sc_xfercnt > 0` and QEMU's
   parallel emulation sustains an IRQ per strobed byte, so a firing
   `lpt_intr` in that window reads `*sc->sc_cp++` from the **freed**
   1024-byte M_LPT chunk and strobes the byte out the port
   (use-after-free read; 1 byte per firing).

Consequence: use-after-free read of freed heap memory, byte streamed
to the parallel port.  No crash by itself; it becomes a true
cross-zone info leak if the chunk is recycled into another kmalloc
zone inside the ~1-2 µs window.  Root-only (device 0600 root:wheel).

## Reproduce attempt (bounded; result: not reproduced)

```
cc -O -Wall -o close_lifetime close_lifetime.c
./close_lifetime 300        # 300 x (open; fork writer; SIGUSR1; wait; close)
```
Each cycle creates the precondition: write interrupted with
`sc_xfercnt > 0`, `INTERRUPTED` set, callout armed at 1 tick, IRQ
chain strobing; close then skips the drain and hits the kfree
windows.  300 cycles completed with **no panic and no dmesg anomaly**
— consistent with the quantified window (~5e-6 per cycle for an
in-flight `lpt_intr` to be exactly between its status spin and the
`sc_cp` deref during the ~µs kfree window; the UAF read itself is
silent because the freed M_LPT chunk is not recycled that fast).

The finding is retained on structural evidence; the runtime hammer is
inconclusive by nature (silent UAF read, no crash signature).
`fix.diff` (callout_drain + frees moved after the intr teardown) was
authored against the tree; fix validation is not applicable because
the bug does not produce an observable failure to compare.
