puc_setup_intr publishes ihand before ihandarg: shared-IRQ event during child attach dispatches siointr with a NULL argument -> kernel NULL-deref panic (boot-time DoS)
Summary
The handler gate is stored FIRST and its argument SECOND with no barrier, while puc_intr gates on ihand alone - a shared-IRQ sibling event on another CPU between the two stores executes siointr with the still-NULL bzero'd argument, immediately dereferenced: boot-time panic. The IRQ is RF_SHAREABLE so the event can come from ANY device on the line; up to 16 attaches per card per boot. Userspace interleaving model of the exact kernel pattern run on the guest: kernel-order dispatches NULL 2/140263 (faithful) and 36901/49191 (widened) vs ZERO with the fixed order - proving the publication order is the defect. Publish-arg-first + fence + inverse-teardown fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4653 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| trigger.c | β | 6.2 KB | view raw | |
| build.sh | β | 127 B | view raw | |
| run.sh | β | 185 B | view raw | |
| build.log | β | 15 B | view raw | |
| run.log | β | 629 B | view raw | |
| env.txt | β | 820 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 4.1 KB | view raw |
DF-4653 β puc_setup_intr publishes ihand before ihandarg: shared-IRQ window dispatches the child handler with a NULL argument (kernel panic) ======================================================================
Finding file: sys/dev/misc/puc/puc.c (pass 2) Decisive lines: 691-692 (publication order), 406-409 (dispatch), 229-238 (early RF_SHAREABLE registration of puc_intr).
What the bug is
puc_pci_attach() allocates its IRQ RF_ACTIVE | RF_SHAREABLE
(puc.c:229-230) and registers puc_intr immediately
(puc.c:236-238, serializer NULL). From that instant, puc_intr is
dispatched on ANY event on that line β including events raised by
sibling devices sharing the IRQ, and including the whole child-attach
phase that follows.
Each child (sio) registers its handler during device_probe_and_attach()
(puc.c:351) via bus_setup_intr(..., INTR_MPSAFE, siointr, com, ...)
(sys/dev/serial/sio/sio.c:1236), which lands in puc_setup_intr. That
function publishes the two fields in the wrong order:
puc.c:691 sc->sc_ports[i].ihand = ihand; /* gate FIRST */ puc.c:692 sc->sc_ports[i].ihandarg = arg; /* argument SECOND */
while puc_intr gates on exactly that publication:
puc.c:407 if (sc->sc_ports[i].ihand != NULL && ...) puc.c:409 (sc->sc_ports[i].ihand)(sc->sc_ports[i].ihandarg);
An interrupt dispatched on another CPU between the two stores (handlers
are INTR_MPSAFE and unsynchronized β puc itself passes NULL serializer
and KKASSERTs child serializers are NULL at puc.c:693) observes
ihand == siointr with the still-NULL ihandarg (softc is bzero'd at
puc.c:214) and executes siointr(NULL), which immediately dereferences
its argument: sio.c:1750-1755 siointr(arg) -> siointr1((struct
com_s *)arg) -> com->... (first deref at sio.c:1797+). Result: kernel
NULL-deref panic during boot. The plain stores also carry no compiler
barriers, so the compiler may sink or swap them. (The teardown side at
puc.c:711-712 has the correct store order β gate cleared first β but is
equally barrier-less; it is dead in practice because no child driver in
the tree implements detach.)
Model / evidence
trigger.c reproduces the interleaving as a userspace model of the exact kernel store/load pattern (thread A = CPU0 in puc_setup_intr, thread B = CPU1 in puc_intr on a shared-IRQ event):
phase A β kernel store order, faithful back-to-back stores phase B β kernel store order, window widened by one sched_yield() (instrumentation; isolates the ORDER as the defect) phase C β fixed store order (argument first, gate last), same widening
B > 0 while C == 0 proves the publication order is the defect; A > 0 shows it is observable even at faithful width. run.sh builds and runs the model on the audit guest. The KERNEL manifestation (siointr(NULL) panic) is hardware-gated: reproduction needs a puc-supported multiport serial card whose sibling/shared IRQ activity (or the card itself) fires in the window during sio_attach β no such card exists on the audit guest (run.log), so the kernel verdict is not_reproduced with the ordering defect proven from source + model.
Fix
fix.diff publishes ihandarg first and ihand last (with compiler barriers), and snapshots the pair symmetrically in puc_intr / puc_teardown_intr.
DF-4653 VERDICT β puc_setup_intr publishes ihand before ihandarg
STATUS: not_reproduced (kernel manifestation hardware-gated). Ordering defect PROVEN on the guest by an interleaving model of the exact kernel store/load pattern (run.log: phase A hit 2 NULL-arg dispatches at faithful 2-store width; phase B 36,901 widened; phase C β fixed order β ZERO). IMPACT ceiling: boot-time kernel NULL-deref panic (DoS) on machines with a puc-supported multiport serial card. No privilege gain, no corruption beyond the NULL deref.
Root cause (source-certain)
puc.c stores the child handler and its argument in the wrong order:
:691 sc->sc_ports[i].ihand = ihand; /* gate published FIRST */ :692 sc->sc_ports[i].ihandarg = arg; /* argument SECOND */
puc_intr() gates on exactly that publication:
:407 if (sc->sc_ports[i].ihand != NULL && ...) :409 (sc->sc_ports[i].ihand)(sc->sc_ports[i].ihandarg);
Reachability of the window
- puc_pci_attach() registers puc_intr on the shared line BEFORE any child exists: bus_alloc_resource(SYS_RES_IRQ, RF_ACTIVE|RF_SHAREABLE) at :229-230, BUS_SETUP_INTR(..., puc_intr, sc, ..., NULL /serializer/) at :236-238.
- Children register during device_probe_and_attach() at :351 -> sio_attach -> bus_setup_intr(..., INTR_MPSAFE, siointr, com, ...) (sio.c:1236) -> puc_setup_intr.
- Handlers are INTR_MPSAFE and unsynchronized (puc asserts child serializers are NULL, puc.c:693), so puc_intr runs concurrently on another CPU β and because the IRQ resource is RF_SHAREABLE, the line can be raised by ANY sibling device sharing the interrupt, not just the puc card. A shared-IRQ sibling event landing in the 2-store window dispatches siointr with the still-NULL ihandarg (softc bzero'd at :214).
- Consequence: siointr(NULL) -> siointr1((struct com_s *)NULL) -> immediate dereference (sio.c:1750-1755, first use at :1797+) -> kernel NULL-deref panic. Boot-time DoS; window is two instructions wide, so probability per attach is small but non-zero, and there are up to 16 attaches per card (PUC_MAX_PORTS) per boot.
- The plain struct-field stores carry no compiler barriers β the compiler may also sink or swap them, widening the window.
- Teardown side (:711-712) has the correct store order (gate cleared first) but is equally barrier-less; it is dead in practice: no child driver in this tree implements detach (no device_detach / bus_teardown_intr anywhere in sys/dev/serial/sio/sio.c), so the setup-side race is the only live instance.
Evidence (what ran, where)
- trigger.c β userspace model of the exact store/load pattern (thread A = CPU0 in puc_setup_intr, thread B = CPU1 in puc_intr on a shared-IRQ event). Synchronization (barriers) only re-opens the window each round; the kernel's defect is that none exists.
-
Built and RUN ON THE DRAGONFLY GUEST (build.log / run.log):
A. kernel order, faithful 2-store window: dispatches=140263 NULL-arg dispatches=2 <- panic condition B. kernel order, widened (sched_yield between the two stores): dispatches=49191 NULL-arg dispatches=36901 C. FIXED order (argument first, gate last), same widening: dispatches=126664 NULL-arg dispatches=0
B >> 0 while C == 0 isolates the ORDER as the defect; A > 0 shows the faithful-width window is observable at all β in the kernel the interrupt arrival is asynchronous, so any IRQ landing between the two stores has exactly the modeled effect. * Kernel manifestation: hardware-gated. The audit guest has no comm-class (0x07) PCI device and no puc-matched card (run.log probe, env.txt), so puc_attach/puc_setup_intr/puc_intr never execute there.
Why not reproduced on the guest
puc.c only binds to the ~100 PCI IDs in the const pucdata.c table (multiport serial cards). QEMU exposes no such device and none can be added without new hardware emulation. The NULL-deref consequence chain (siointr -> siointr1 immediate deref of its argument) is traced line-by-line in source; the publication-order defect is proven by the guest-executed model.
Fix validation
fix.diff (git apply --check clean against the unmodified sys/ tree): publishes ihandarg first, compiler-barrier (cpu_ccfence), then ihand; teardown retracts in the inverse order with the same barrier; puc_intr additionally refuses to dispatch a NULL argument (defense in depth β every in-tree child passes a non-NULL arg). Baseline kernel behavior cannot run on the guest (no puc card), so fix_status=not_testable; the model's phase C demonstrates the fixed ordering yields zero NULL-arg dispatches under the same widening that produces 36,901 with the kernel order.
Recommended fix: publish ihandarg before ihand (+cpu_ccfence) β see fix.diff.
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
not_testableFix not validated at kernel runtime on guest: baseline cannot run (no puc card). fix.diff applies cleanly (git apply --check) against puc.c; the model's phase C demonstrates the fixed ordering yields zero NULL-argument dispatches under the same widening that produces 36901 with the kernel order.
findings/poc/DF-4653/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (store-order trace + reachability chain + dead-teardown note); trigger.c + run.log (guest-executed model: A=2/140263 NULL-arg dispatches faithful-width, B=36901/49191 widened, C=0 fixed-order β order is the defect); build.log (guest cc -Wall -Wextra -pthread clean); env.txt/run.log probe (no comm-class PCI device on guest); fix.diff (publish ihandarg first + cpu_ccfence + inverse teardown order + defensive NULL-arg gate in puc_intr; git apply --check clean)
PoC changes
No seed pack existed; trigger authored as a 3-phase pthread interleaving model (faithful / widened kernel order / widened fixed order) of the exact kernel store/load pattern, built and executed on the DragonFly guest.
Verified recommended fix
Publish ihandarg before ihand with cpu_ccfence() between (and inverse order in teardown); see fix.diff.
Verdict
puc_setup_intr() (sys/dev/misc/puc/puc.c:691-692) publishes the child interrupt handler BEFORE its argument, while puc_intr() (puc.c:407-409) gates dispatch on ihand != NULL alone. puc_pci_attach registers puc_intr on an RF_SHAREABLE line before any child exists (puc.c:229-238, serializer NULL), children register during device_probe_and_attach (:351) via sio_attach's bus_setup_intr(INTR_MPSAFE, siointr, com) (sio.c:1236), and the handlers are unsynchronized β so a shared-IRQ event dispatched on another CPU between the two stores calls siointr with the still-NULL ihandarg (softc bzero'd at puc.c:214), which immediately dereferences it (sio.c:1750-1755 -> siointr1, first deref :1797+) = kernel NULL-deref panic during boot. Plain barrier-less stores mean the compiler may also widen the window; the teardown side (:711-712) has correct order but is dead code (no in-tree child driver implements detach). The kernel manifestation is hardware-gated on the audit guest (no comm-class PCI device β run.log), so not_reproduced at kernel level; however the publication-order defect itself was PROVEN on the DragonFly guest by an interleaving model of the exact kernel store/load pattern: kernel order at faithful 2-store width dispatched the handler with NULL argument 2/140263 times, widened 36901/49191, and the FIXED order (argument first, gate last) ZERO under the same widening (run.log phases A/B/C). Ceiling: boot-time DoS panic on machines with a puc-supported multiport serial card; no privilege gain.
No comments yet.