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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4806 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline 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)'
↓ fix.diffper-fix-DF-4806

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.