ppi: interrupt handler registered with a cdev_t argument but consumed as device_t - wild-pointer deref and instant kernel panic on the first parallel-port IRQ (currently defused by a sibling bug: the legacy IRQ allocation can never succeed, leaving 1284 peripheral mode dead)
Summary
ppiopen passes the cdev_t as the ppiintr handler arg while ppiintr casts to device_t - device_get_parent reads the cdev TAILQ linkage -> supervisor read fault at IRQ level. As shipped it is defused by a second bug: the non-shareable legacy IRQ alloc always fails against ppc's shareable holding, so the 1284 path is dead code. Fixing the allocation alone (the lpt-identical one-liner any maintainer would write) arms the panic: REPRODUCED live on guest (Fatal trap 12 at ppiintr+0x41, interrupt context, guest dead); RF_SHAREABLE + pass-ppidev fix validated (survives 8 strobes, handler servicing irq7).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4806 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ppi_intr_panic.c | β | 2.8 KB | view raw | |
| modbuild.sh | β | 2.4 KB | view raw | |
| arming.diff | β | 278 B | view raw | |
| fix.diff | β | 559 B | view raw | |
| run.sh | β | 167 B | view raw | |
| stock_run.log | β | 568 B | view raw | |
| panic.txt | β | 2.6 KB | view raw | |
| panic-diag.txt | β | 1.9 KB | view raw | |
| fixrun.log | β | 967 B | view raw | |
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4806 β ppi(4): interrupt handler registered with a cdev_t argument, treated as device_t β wild-pointer deref / instant kernel panic at first parallel-port IRQ (currently defused by a second bug: the IRQ resource allocation can never succeed)
File: sys/dev/misc/ppi/ppi.c lines 160-161, 266-269 vs 187-189 (pass 2)
The two interacting bugs
Bug A (ppi.c:160-161) β the PERIPH_1284 interrupt resource is allocated non-shareable:
ppi->intr_resource = bus_alloc_legacy_irq_resource(dev, &zero, irq,
RF_ACTIVE);
The ppbus parent (ppc, sys/dev/misc/ppc/ppc.c:1882-1883) already holds the
parallel-port IRQ as RF_SHAREABLE. DragonFly's rman requires matching share
type to share (sys/kern/subr_rman.c:341, "a candidate region must match
exactly by both size and sharing type"), and RF_ACTIVE|RF_SHAREABLE
allocations also fail activation against the live shareable region β so on a
stock system the allocation always fails: intr_resource == NULL, the
handler is never registered, and the whole 1284-peripheral interrupt path is
dead code. (Sibling driver lpt allocates with plain RF_SHAREABLE,
sys/dev/misc/lpt/lpt.c:385-386 β that is the minimal, correct fix and it
works: verified live.)
Bug B (ppi.c:268 vs 187) β the registration passes the wrong pointer:
/* ppiopen(), line 245: dev = ap->a_head.a_dev -- a cdev_t */
BUS_SETUP_INTR(device_get_parent(ppidev), ppidev,
ppi->intr_resource, 0,
ppiintr, dev, /* <-- cdev_t as handler arg */
&ppi->intr_cookie, NULL, NULL);
static void
ppiintr(void *arg)
{
device_t ppidev = (device_t)arg; /* <-- expects device_t */
device_t ppbus = device_get_parent(ppidev); /* cdev+0x28 = link.tqe_prev */
...
ppi_disable_intr(ppidev); /* -> ppb_rctr(bogus) -> PPBUS_IO(deref) */
lpt passes the correct lptdev (lpt.c:755). ppi passes the character
device. On amd64, struct bsd_device's parent field (offset 0x28,
sys/sys/bus_private.h:109) lands on struct cdev's link.tqe_prev
(offset 0x28, sys/sys/conf.h:67+) β the cdev TAILQ linkage β so the first
use of the "bus" in the interrupt handler chases a bogus pointer chain
(ppb_rctr -> PPBUS_IO(device_get_parent(bogus))) and faults at interrupt
level.
Interaction: bug A defuses bug B today; fixing A alone (the obvious
"make 1284 mode work" one-liner) arms B and every parallel-port interrupt
with /dev/ppi0 open panics the kernel. Both fixes must land together.
Reproduce (root on the QEMU guest with ppc0 at 0x378 irq 7)
# 1. build modules (ppbus with PERIPH_1284 via COPTS, ppc, ppi) and trigger sh modbuild.sh stock # or: armed | fixed (see arming.diff / fix.diff) cc -O -Wall -o ppi_intr_panic ppi_intr_panic.c # 2. load ORDER MATTERS: ppi must register before ppbus0 attaches # (bus_generic_identify no-ops on an attached parent) kldload ppbus/ppbus.ko && kldload ppi/ppi.ko && kldload ppc/ppc.ko # 3. fire ./ppi_intr_panic
Expected per variant (all three were run and captured):
| variant | ppi.c state | result |
|---|---|---|
| stock | RF_ACTIVE (as shipped) | 8 strobes, no handler registered (intr_resource NULL), guest healthy β see stock_run.log |
| armed | RF_SHAREABLE (arming.diff, lpt-identical) | Fatal trap 12 at ppiintr+0x41: movq (%rax),%rsi, fault VA 0x0, interrupt context, all CPUs stopped β see panic.txt |
| fixed | RF_SHAREABLE + ppiintr, ppidev (fix.diff) |
8 strobes survived, handler registered+running (vmstat -i ppc0=8), guest healthy β see fixrun.log |
The trigger needs /dev/ppi0 (0600 root:wheel, verified) and /dev/io;
each data strobe makes QEMU's emulated printer ACK => irq 7 delivery
(the same mechanism DF-4535 used for lpt).
Impact
Instantaneous, unrecoverable kernel panic (local DoS) from open() + one
parallel-port interrupt, on any kernel built with options PERIPH_1284
(LINT64; not in X86_64_GENERIC/GENERIC) once the IRQ allocation is
functional. The fault is a supervisor READ through uncontrolled pointers β
panic-class, not a practical write primitive. Device node is 0600
root:wheel, so the direct trigger requires root/operator-granted access;
on a config where the node is group-writable (devfs.conf relaxations for
printer groups) it is reachable by any local member.
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:
-
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: withRF_ACTIVEand withRF_ACTIVE|RF_SHAREABLEthe allocation returns NULL (PPIDIAG: attach intr_resource=0, diagnostic run); with plainRF_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. -
ppi.c:268 β
BUS_SETUP_INTR(..., ppiintr, dev, ...)passesdev = ap->a_head.a_dev, the cdev_t (ppi.c:245), as the interrupt handler argument.ppiintr(ppi.c:187-189) casts its arg todevice_tand immediately doesdevice_get_parent(ppidev)+ppi_disable_intr(ppidev)->ppb_rctr(bus)-> macroPPBUS_IO(device_get_parent(dev), ...)(sys/bus/ppbus/ppbio.h:74-75). On amd64struct bsd_device.parentsits at offset 0x28 (sys/sys/bus_private.h:104-109); at offset 0x28 ofstruct cdevlieslink.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 -ippc0=8) but no handler is registered β guest healthy (stock_run.log). - armed (arming.diff =
RF_ACTIVE->RF_SHAREABLEONLY): 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 inpanic-diag.txtshowsppiintr 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 -ippc0=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.
Fix verification
fixedBaseline armed module (stock ppi.c + RF_SHAREABLE only): Fatal trap 12 in ppiintr+0x41 on the first parallel-port strobe with /dev/ppi0 open. Patched module (fix.diff: RF_SHAREABLE + ppiintr arg = ppidev): identical trigger runs to completion (8 strobes), handler registered ('interrupt uses mplock: ppi0') and servicing irq7 (vmstat -i ppc0=8), guest healthy.
fixrun.log (patched run output + vmstat), panic.txt (baseline panic), VERDICT.md section 'Fix (validated)'
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (clean armed run: 'Stopped at ppiintr+0x41: movq (%rax),%rsi', Fatal trap 12, interrupt context); panic-diag.txt (instrumented run: 'ppiintr entered, arg=0xfffff80116a12e00' immediately before the fault); stock_run.log (stock RF_ACTIVE: no handler registered, 8 strobes survived); fixrun.log (fix.diff: 8 strobes survived, handler registered 'interrupt uses mplock: ppi0', vmstat -i ppc0=8); arming.diff/fix.diff; modbuild.sh + ppi_intr_panic.c
PoC changes
PoC authored fresh (no seed). Fixed module-build issues found on the way: kmod CFLAGS is clobbered by sys/conf/kmod.mk:98 (must use COPTS for -DPERIPH_1284), ppc needs isa_if.h/opt_ppc.h, ppi must kldload BEFORE ppc so bus_generic_identify runs pre-attach, and the arming must be plain RF_SHAREABLE (RF_ACTIVE|RF_SHAREABLE still fails - verified).
Verified recommended fix
Allocate the legacy IRQ with RF_SHAREABLE (mirroring lpt.c:385) AND pass ppidev (device_t), not dev (cdev_t), as the ppiintr handler argument; also guard irq>0 and check BUS_SETUP_INTR's return.
Verdict
ppiopen() registers ppiintr with the cdev_t as the handler argument while ppiintr treats it as a device_t (ppi.c:268 vs 187); the first dereference chain (device_get_parent at struct-device offset 0x28 lands on the cdev TAILQ linkage, then ppb_rctr/PPBUS_IO derefs) faults at interrupt level - Fatal trap 12 at ppiintr+0x41, fault VA 0x0, all CPUs stopped. As shipped this is defused by a second bug (ppi.c:160-161 allocates the legacy IRQ non-shareably, which always fails against ppc's shareable holding), so the 1284 peripheral interrupt path is dead; fixing that allocation alone (the lpt-identical RF_SHAREABLE one-liner, arming.diff) arms the panic, which reproduced live on the QEMU guest ppi0/ppc0 at 0x378 irq7. fix.diff (RF_SHAREABLE + ppidev argument) validated: baseline armed module panics on the first strobe, patched module survives the identical trigger with the handler registered and servicing interrupts.
No comments yet.