# DF-4806 — VERDICT

**Status: reproduced** (impact: panic; severity Low as shipped — see
gating below). **Fix validated** on the guest: baseline (armed) panics,
patched (fix.diff) survives the identical trigger with interrupts
delivered.

## Root cause (path:line)

Two interacting defects in `sys/dev/misc/ppi/ppi.c`:

1. **ppi.c:160-161** — `bus_alloc_legacy_irq_resource(..., RF_ACTIVE)`:
   non-shareable legacy IRQ allocation. The ppbus parent ppc always holds
   the parallel IRQ shareably first (`sys/dev/misc/ppc/ppc.c:1882-1883`,
   attached before its ppbus children), and DF's rman refuses to share a
   shareable region with a non-shareable (or ACTIVE-at-reserve shareable)
   request (`sys/kern/subr_rman.c:338-341` "must match exactly by both
   size and sharing type"). Verified live on the guest: with `RF_ACTIVE`
   and with `RF_ACTIVE|RF_SHAREABLE` the allocation returns NULL
   (`PPIDIAG: attach intr_resource=0`, diagnostic run); with plain
   `RF_SHAREABLE` (lpt's exact pattern, `sys/dev/misc/lpt/lpt.c:385-386`)
   it returns a valid resource. Consequence as shipped: the PERIPH_1284
   interrupt path is dead — no handler is ever registered, 1284
   peripheral mode cannot work.

2. **ppi.c:268** — `BUS_SETUP_INTR(..., ppiintr, dev, ...)` passes
   `dev = ap->a_head.a_dev`, the **cdev_t** (ppi.c:245), as the interrupt
   handler argument. `ppiintr` (ppi.c:187-189) casts its arg to
   `device_t` and immediately does `device_get_parent(ppidev)` +
   `ppi_disable_intr(ppidev)` -> `ppb_rctr(bus)` -> macro
   `PPBUS_IO(device_get_parent(dev), ...)` (`sys/bus/ppbus/ppbio.h:74-75`).
   On amd64 `struct bsd_device.parent` sits at offset 0x28
   (`sys/sys/bus_private.h:104-109`); at offset 0x28 of `struct cdev`
   lies `link.tqe_prev`, the cdev TAILQ linkage (`sys/sys/conf.h:67-80`).
   The handler therefore chases kernel-list pointers as if they were bus
   devices and dereferences the result at interrupt level. lpt passes the
   correct device (`lpt.c:755`).

Because (1) keeps `intr_resource == NULL`, (2) is latent as shipped. Fix
(1) alone — the obvious minimal repair to make 1284 mode functional — and
(2) fires on the first interrupt with `/dev/ppi0` open.

## How it was reproduced (guest: DragonFly 6.5-DEVELOPMENT
X86_64_GENERIC + module build of ppbus/ppc/ppi from the same pristine
/usr/src; QEMU PC-style LPT at 0x378 irq 7, source md5-identical to the
audit tree)

Modules built with `PERIPH_1284` (in-tree only in LINT64; `COPTS+=
-DPERIPH_1284`, stock sources otherwise). Load order ppbus -> ppi -> ppc
is required: `bus_generic_identify` no-ops once the parent is attached
(`sys/kern/subr_bus.c:2789-2794`), so ppi must be in the driver list
before ppbus0 attaches (this is automatic in a static kernel config).

- **stock** (`RF_ACTIVE`): ppi0 attaches, `/dev/ppi0` = `crw------- root
  wheel`; trigger's 8 data strobes each raise irq 7 (`vmstat -i` ppc0=8)
  but no handler is registered — guest healthy (`stock_run.log`).
- **armed** (arming.diff = `RF_ACTIVE` -> `RF_SHAREABLE` ONLY): handler
  registration succeeds ("interrupt uses mplock: ppi0"), trigger opens
  /dev/ppi0, first strobe => **Fatal trap 12, fault VA 0x0, `Stopped at
  ppiintr+0x41: movq (%rax),%rsi`**, current process Idle / pri CRIT, all
  CPUs stopped, guest dead (`panic.txt`; corroborating instrumented run
  in `panic-diag.txt` shows `ppiintr entered, arg=<kmem-heap ptr>` — the
  cdev — immediately before the fault).
- **fixed** (fix.diff = RF_SHAREABLE + `ppiintr, ppidev`): identical
  trigger — 8 strobes, handler registered and running, guest healthy,
  `vmstat -i` ppc0=8 (`fixrun.log`).

Trigger = `open("/dev/ppi0")` (registers the handler), `open("/dev/io")`
(iopl), control 0x1c, data byte, strobe pulse — the exact pattern QEMU's
emulated printer ACKs (same IRQ path DF-4535 exercised for lpt).

## Exploit chain

None to uid=0. The primitive is a supervisor-mode READ fault through
kernel-list pointers (panic at interrupt level). The dereferenced values
are not attacker-controlled (cdev TAILQ linkage contents), so this is a
reliable local panic (DoS), not a corruption write primitive. Primary
gating as shipped: `options PERIPH_1284` (LINT64-only) + `/dev/ppi0`
0600 root:wheel + allocation fix. Severity Low accordingly; would be
Medium on a distribution that ships PERIPH_1284 and relaxes node perms.

## Fix (validated)

`fix.diff` (both parts, mirrors lpt):
1. allocate with `RF_SHAREABLE` (drop RF_ACTIVE — RF_ACTIVE|RF_SHAREABLE
   fails activation against the live shareable region, verified live);
2. pass `ppidev`, not `dev`, as the handler argument.
Optionally also guard `irq > 0` like lpt.c:382 and check the
BUS_SETUP_INTR return (registration failure is currently silent).

Baseline vs patched: panic (armed) vs healthy + interrupts serviced
(fixed) under the identical trigger — fix_status=fixed.

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