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

Missing DRIVER_MODULE(iicbus, pcf): the iicbus child never attaches, so pcf->iicbus has a NULL softc forever and the first chip interrupt panics the kernel via iicbus_intr()->sc->owner

Summary

The child is added on the pcf bus but the iicbus driver is registered only on iicbb - newbus probes children only against the parent bus's devclass, so the child stays DS_NOTPRESENT with a NULL softc permanently; pcfintr (registered at attach, ENI enabled since probe) calls iicbus_intr on 8 paths which derefs sc->owner on the NULL softc. KLD-rig-verified on the guest: exact production attach path (io 0x320 irq 5) -> 'iicbus0 state=0 softc=0' (the bug observed) -> the exact :475 sink call -> Fatal trap 12 at iicbus_intr+0x18 va 0x8; on silicon the IRQ line substitutes. DRIVER_MODULE + NULL-guard fix validated on rebuilt module (iicbus0 attaches, sink returns cleanly, guest alive). The driver is also functionally dead (no /dev/iicN).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5490 Β· 12 files
FileTypeDescriptionSize
README.md β€” 4.3 KB ↓ raw
VERDICT.md β€” 5.7 KB ↓ raw
df5490_rig.c β€” 2.7 KB view raw
df5490_probe.c β€” 884 B view raw
build.sh β€” 201 B view raw
run.sh β€” 212 B view raw
run.log.baseline_panic_full β€” 14.1 KB ↓ download
panic.txt β€” 1016 B view raw
run.fixed_serial.log β€” 462 B view raw
fix.diff β€” 1.1 KB view raw
env.txt β€” 350 B view raw
verdict.json β€” 5.4 KB view raw

DF-5490 β€” pcf: iicbus child never attaches (missing DRIVER_MODULE(iicbus, pcf)) β†’ NULL-softc panic in pcfintr

What

pcf_attach() (sys/bus/iicbus/pcf/pcf.c:200) adds an "iicbus" child to the pcf bus, but no DRIVER_MODULE(iicbus, pcf, ...) exists anywhere β€” the iicbus driver is registered only on the iicbb bus (sys/bus/iicbus/iicbus.c:194). DragonFly newbus probes a child only against drivers registered in the parent bus's devclass (sys/kern/subr_bus.c:1414-1484 device_probe_child), so the child never attaches: it stays state=0 (DS_NOTPRESENT) with softc == NULL forever. pcfintr() β€” the live IRQ handler registered by pcf_attach() (pcf.c:193-195) β€” calls iicbus_intr(pcf->iicbus, ...) on 8 paths (pcf.c:412, 430, 435, 445, 447, 453, 468, 475); iicbus_intr() dereferences device_get_softc(bus)->owner (iiconf.c:47-50) β†’ NULL softc β†’ page fault β†’ kernel panic. ENI (chip interrupt enable) is permanently set by pcf_rst_card() (pcf.c:537), pcf_stop() (pcf.c:300) and pcfintr's own error path (pcf.c:490), so on real PCF8584 hardware in the default (non-polled) configuration the FIRST interrupt after attach panics the machine deterministically. The driver is also functionally dead: with no iicbus attachment there is no /dev/iicN and no in-tree caller can reach pcf_write/pcf_read/pcf_start.

Build (guest)

scp df5490_rig.c dfbsd:/root/
mkdir -p /usr/src/sys/bus/iicbus/df5490 && cp /root/df5490_rig.c $_/
printf 'KMOD=   df5490_rig\nSRCS=   df5490_rig.c\nSRCS+=  bus_if.h device_if.h iicbus_if.h isa_if.h\n.include <bsd.kmod.mk>\n' > /usr/src/sys/bus/iicbus/df5490/Makefile
cd /usr/src/sys/bus/iicbus/df5490 && make

Run (baseline β€” expect panic)

kldload iicbus && kldload pcf
kldload /usr/src/sys/bus/iicbus/df5490/df5490_rig.ko

The rig attaches pcf0 through the exact newbus API isahint uses (BUS_ADD_CHILD + bus_set_resource + device_probe_and_attach β€” cf. sys/bus/isa/isahint.c:53-91), because the kernel's hinted-children enumeration only reads the compiled-in devtab (subr_bus.c:2401 resource_query_string), so kenv/loader hints cannot add an isa child at runtime. After attach it dumps the child state and then invokes the exact pcfintr sink call (iicbus_intr(pcf->iicbus, INTR_RECEIVE, &data), pcf.c:475).

Expected (observed) β€” see panic.txt / run.log.baseline_panic_full

df5490: added pcf child pcf0, probing+attaching
pcf0: <PCF8584 I2C bus controller> at port 0x320-0x321 irq 5 on isa1
interrupt uses mplock: pcf0
df5490: device_probe_and_attach -> 0
df5490: pcf0=pcf0 state=3 children=1
df5490: child[0]=iicbus0 state=0 softc=0 devclass=iicbus     <-- never attached, NULL softc
df5490: calling iicbus_intr(child0, INTR_RECEIVE, &c) == pcfintr pcf.c:475
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x8
Stopped at      iicbus_intr+0x18:       movq    0x8(%rax),%rbx   <-- sc->owner @ offset 8 of NULL softc

Run (fix validation β€” expect NO panic)

cd /usr/src && patch -p1 < fix.diff          # adds DRIVER_MODULE(iicbus, pcf, ...) + pcfintr guard
cd sys/bus/iicbus/pcf && make && cp pcf.ko /root/pcf_fixed.ko
kldload iicbus && kldload iic && kldload /root/pcf_fixed.ko
kldload /usr/src/sys/bus/iicbus/df5490/df5490_rig.ko

Expected (observed) β€” see run.fixed_serial.log

iicbus0: <Philips I2C bus> on pcf0 addr 0xaa
df5490: child[0]=iicbus0 state=3 softc=0xfffff8004f0e7478 devclass=iicbus
df5490: iicbus_intr RETURNED (no fault)

Guest stays up; the previously-panicking sink call is a clean no-op (owner == NULL).

Notes

  • The only hardware-substituted step is IRQ delivery: on real PCF8584 silicon the chip's interrupt (enabled since probe via ENI, pcf.c:171β†’537) calls the registered pcfintr handler, which makes the identical iicbus_intr(pcf->iicbus, ...) call that the rig makes. The device state (pcf0 attached, irq registered, child unattached with NULL softc) is the genuine production state, created through production newbus APIs.
  • Cross-observations for future audits (other files): (1) even with the fix, iicbus0's own wildcard children (ic/iic/iicsmb) did not attach/materialize /dev/iic0 β€” iicbus.c/iic.c child-handling question; (2) kldunload iicbus succeeded while iicbus0 was attached (module unload not pinned to attached bus children).
VERDICT.md
↓ download raw

DF-5490 β€” VERDICT

Status: REPRODUCED (impact: panic / kernel NULL-pointer DoS, hardware-gated delivery). Fix validated live: baseline panics 1/1, patched module 1/1 clean.

Root cause (path:line)

  1. pcf_attach() adds an iicbus child: pcf->iicbus = device_add_child(pcfdev, "iicbus", -1) β€” sys/bus/iicbus/pcf/pcf.c:200.
  2. newbus can only probe that child against drivers registered on the pcf bus β€” device_probe_child() iterates dev->devclass->drivers of the parent bus (sys/kern/subr_bus.c:1414-1484). The iicbus driver is registered ONLY on the iicbb bus: DRIVER_MODULE(iicbus, iicbb, ...) β€” sys/bus/iicbus/iicbus.c:194. No DRIVER_MODULE(iicbus, pcf, ...) exists anywhere in the tree (verified: grep -rn 'DRIVER_MODULE(iicbus' sys/ returns exactly one hit). Sibling bridges prove the requirement: DRIVER_MODULE(smbus, ichsmb, ...) at sys/bus/smbus/ichsmb/ichsmb.c:696 and the five-line bridge enumeration at sys/bus/iicbus/iicbb.c:441-445.
  3. Therefore the child never attaches: state=0 (DS_NOTPRESENT), device_get_softc(pcf->iicbus) == NULL β€” proven live: rig printed child[0]=iicbus0 state=0 softc=0 devclass=iicbus (panic.txt).
  4. pcfintr() is a live registered interrupt handler (pcf.c:193-195; serial log: interrupt uses mplock: pcf0). All 8 of its event paths call iicbus_intr(pcf->iicbus, ...) (pcf.c:412, 430, 435, 445, 447, 453, 468, 475). iicbus_intr() does sc = device_get_softc(bus); if (sc->owner) β€” sys/bus/iicbus/iiconf.c:47-50 β†’ read at offset 8 of NULL β†’ Fatal trap 12, fault virtual address = 0x8, Stopped at iicbus_intr+0x18: movq 0x8(%rax),%rbx (panic.txt).
  5. On hardware: ENI (interrupt enable) is permanently set β€” pcf_rst_card() pcf.c:537 (runs from probe, pcf.c:171), pcf_stop() pcf.c:300, pcfintr error path pcf.c:490. The PCF8584 asserts INT when PIN clears (any completed byte transfer, master or slave). Default configuration is interrupt mode (IIC_POLLED unset in the hint flags). First interrupt after attach β‡’ panic. Not a narrow race: every interrupt-bearing pcf deployment hits it.
  6. Secondary effect: with the child dead there is no /dev/iicN, and pcf_start/pcf_write/pcf_read have no in-tree caller (iic.c ioctls need the iic child; iicsmb needs the iicsmb child; pcfclock is ppbus-based). The driver is functionally dead in-tree β€” and the dead code still panics via pcfintr.

Reproduction (this run)

QEMU has no PCF8584 model. The rig reproduces the exact production state and the exact sink through production APIs:

  • pcf0 attached on isa1 @ io 0x320-0x321, irq 5, interrupt mode, via the same BUS_ADD_CHILD/bus_set_resource/device_probe_and_attach sequence isahint.c:53-91 performs (kenv/loader hints cannot add isa children because resource_query_string() enumerates only the compiled-in devtab β€” subr_bus.c:2401-2410; both /boot/device.hints (not parsed by the DF loader) and loader.conf hint.pcf.0.* kenv variables were tried and were ignored by the enumeration).
  • child state dumped: iicbus0 state=0 softc=0 β€” the bug, observed.
  • the exact call pcfintr makes β€” iicbus_intr(child, INTR_RECEIVE, &c) (pcf.c:475) β€” fired: page fault at 0x8, kernel panic, DDB (panic.txt, run.log.baseline_panic_full). 1/1 runs.

The only substituted step is who invokes the call (rig instead of the IRQ line); the pointer value, device state, and fault are identical to what the registered pcfintr handler produces on silicon.

Fix validation

fix.diff (against read-only sys/; applied only to the guest's /usr/src copy): 1. #include "../iicbus.h" + DRIVER_MODULE(iicbus, pcf, iicbus_driver, iicbus_devclass, NULL, NULL); — registers the iicbus driver on the pcf bus (mirrors ichsmb.c:696). 2. Defensive guard at pcfintr entry: return if pcf->iicbus == NULL || device_get_softc(pcf->iicbus) == NULL (covers the setup_intr→add_child window in pcf_attach:193-200 and any future registration gap).

Patched pcf.ko rebuilt in-guest (-Werror clean), loaded, rig re-run: - iicbus0: <Philips I2C bus> on pcf0 addr 0xaa β€” child ATTACHES (the previously-dead pcf_print_child() pcf.c:208-219 even prints now). - child[0]=iicbus0 state=3 softc=0xfffff8004f0e7478 β€” softc allocated. - iicbus_intr RETURNED (no fault) β€” the previously-panicking sink is a clean no-op (owner == NULL). Guest alive, no panic (run.fixed_serial.log). 1/1 baseline panic vs 1/1 patched clean.

Severity reasoning

No unprivileged-user reachability: the driver attaches only on PCF8584 hardware at a hinted ISA port; the panic trigger is the chip's own interrupt (or any bus event). Impact ceiling = deterministic kernel panic of the machine (DoS) + driver non-function. Per rubric ("narrow trigger, requires specific hardware") β†’ Low, confidence certain (code path proven, state and sink reproduced live).

Attempts

  1. /boot/device.hints (not parsed by DF loader) β€” no attach.
  2. loader.conf hint.pcf.0.* kenv vars β€” hints present in kenv but devtab-only enumeration (subr_bus.c:2401) ignores them β€” no attach.
  3. Rig v1 with bus_set_resource(..., cpuid=-1) for IRQ β€” hit unrelated nexus invalid cpuid: -1 KASSERT (nexus_alloc_resource); rig bug, fixed by passing machintr_legacy_intr_cpuid(irq) exactly as isahint.c:70-72 does.
  4. Rig v2 β€” full success: baseline panic captured; fixed module validated.

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 stock pcf.ko: 1/1 rig runs panic (Fatal trap 12, VA 0x8, iicbus_intr+0x18). Patched pcf.ko (fix.diff applied to guest /usr/src, module rebuilt, same rig): 1/1 clean -- iicbus0 attaches ('on pcf0 addr 0xaa'), child softc valid, iicbus_intr returns without fault, guest stays up.

run.fixed_serial.log; run.log.baseline_panic_full (before/after); fix.diff
↓ fix.diffper-fix-DF-5490

Confirmed kernel references

Detail

Exploit chain

PCF8584 hardware at a hinted ISA port + default interrupt mode: boot/kldload attaches pcf0 and registers pcfintr for the ISA IRQ; ENI has been enabled since probe (pcf.c:171->pcf_rst_card:537); any completed byte transfer (master or slave event) clears PIN and asserts INT; pcfintr runs, status has PIN clear, slave-receiver branch reaches iicbus_intr(pcf->iicbus, INTR_RECEIVE, &data) at pcf.c:475 -> iiconf.c:47-50 device_get_softc(child)==NULL -> sc->owner read at NULL+8 -> Fatal trap 12 -> panic. Every interrupt-bearing pcf deployment panics on first bus event; no attacker privilege needed beyond owning the hardware/bus.

Evidence (decisive lines)

panic.txt (decisive: 'child[0]=iicbus0 state=0 softc=0' then 'Fatal trap 12 ... fault virtual address = 0x8 ... Stopped at iicbus_intr+0x18: movq 0x8(%rax),%rbx'); run.log.baseline_panic_full (full untrimmed serial console of the baseline run incl. attach lines); run.fixed_serial.log (patched run: iicbus0 attaches, 'RETURNED (no fault)'); fix.diff; df5490_rig.c (production-API attach rig + sink)

PoC changes

no seed PoC existed (pass-2 discovery); wrote df5490_rig.c from scratch: isahint-style BUS_ADD_CHILD/bus_set_resource/device_probe_and_attach attach (kenv hints unusable: resource_query_string enumerates only the compiled devtab, subr_bus.c:2401), then state dump + exact pcfintr sink. Rig v1 passed cpuid=-1 for the IRQ resource and hit an unrelated nexus KASSERT; fixed by passing machintr_legacy_intr_cpuid(irq) exactly as isahint.c:70-72 does.

Verified recommended fix

Add '#include "../iicbus.h"' and 'DRIVER_MODULE(iicbus, pcf, iicbus_driver, iicbus_devclass, NULL, NULL);' to pcf.c (mirror ichsmb.c:696), plus an early 'if (pcf->iicbus == NULL || device_get_softc(pcf->iicbus) == NULL) return;' guard in pcfintr() -- see fix.diff (validated live).

Verdict

REPRODUCED: no DRIVER_MODULE(iicbus, pcf, ...) exists (only iicbus.c:194 on iicbb; cf. the bridge-enumeration pattern iicbb.c:441-445 and DRIVER_MODULE(smbus, ichsmb) ichsmb.c:696), so the iicbus child added at pcf.c:200 never attaches -- device_probe_child (subr_bus.c:1414-1484) only probes drivers registered on the parent (pcf) bus. Live proof on the QEMU guest with pcf0 attached via the isahint production API path: child dumped as 'iicbus0 state=0 softc=0' (DS_NOTPRESENT, NULL softc), and the exact pcfintr sink call iicbus_intr(pcf->iicbus, INTR_RECEIVE, &data) (pcf.c:475, one of 8 pcfintr call sites) page-faults reading sc->owner (offset 8) of the NULL softc: 'Fatal trap 12, fault virtual address = 0x8, Stopped at iicbus_intr+0x18: movq 0x8(%rax),%rbx' -> kernel panic, DDB (1/1 runs). On real PCF8584 silicon in default interrupt mode pcfintr is the registered handler ('interrupt uses mplock: pcf0') and ENI is permanently enabled since probe (pcf.c:171->537, also pcf.c:300/490), so the FIRST chip interrupt after attach panics deterministically; only the IRQ delivery was substituted by the rig because QEMU has no PCF8584 model. Driver is also functionally dead (no /dev/iicN; pcf_write/pcf_read have no in-tree caller). FIX VALIDATED live: fix.diff adds DRIVER_MODULE(iicbus, pcf, ...) + a pcfintr NULL-guard; patched pcf.ko -> 'iicbus0: on pcf0 addr 0xaa', child state=3 softc=0xfffff8004f0e7478, sink call 'RETURNED (no fault)', guest alive (1/1). Hardware-gated trigger => severity Low.