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

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)

No comments yet.

PoC verification

Evidence pack

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

VERDICT.md
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-4653

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.