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

fwohci_pci_detach() swallows child-detach failure, force-deletes the firewire child and returns unconditional success β€” orphaned attached child + live callouts over unmapped BAR/freed DMA rings once a firewire-bus child vetoes detach (latent panic/UAF, lying kldunload)

Summary

Detach discards bus_generic_detach's return, force-deletes the child and hardcodes 0: if a firewire-bus child ever vetoes (none in-tree today - all children return 0), the parent still unregisters the IRQ, unmaps the BAR, frees the DMA rings and reports success - leaving an attached orphan whose self-re-arming watchdog callout writes MMIO through a null bus handle (page-0 panic) while sbp's queued xfers reference freed rings; device_delete_child re-runs firewire_detach on partially-torn state. CardBus surprise removal reachable. Latent API-contract hazard inherited verbatim from FreeBSD 13.4 (same swallow). Propagate-and-bail fix mirrors the suspend path.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5528 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 6.1 KB ↓ raw
check_guest.sh β€” 871 B view raw
build.sh β€” 211 B view raw
run.sh β€” 177 B view raw
run.log β€” 935 B view raw
env.txt β€” 167 B view raw
fix.diff β€” 743 B view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.4 KB view raw

DF-5528 β€” fwohci_pci_detach() swallows child-detach failure, force-deletes the firewire child, and returns unconditional success

File: sys/bus/firewire/fwohci_pci.c (pass 2, GLM 5.3 re-audit) Class: driver lifecycle / error-handling (CWE-754, latent CWE-672 downstream) Severity: Low β€” latent trigger, requires FireWire OHCI hardware + privileged unload (or CardBus surprise removal)

TL;DR

fwohci_pci_detach() (sys/bus/firewire/fwohci_pci.c:341-386) discards the return value of bus_generic_detach(self) (line 350), unconditionally force-deletes the firewire child device (lines 351-354), and always return 0 (line 385). If any device on the fwohci→firewire→{sbp,fwe,dcons_crom} chain ever vetoes detach (returns EBUSY), the parent nonetheless releases the IRQ, unmaps the BAR and frees the OHCI DMA rings while the firewire child remains DS_ATTACHED and orphaned with live callouts — a use-after-free / page-0-MMIO-write panic chain, and kldunload reports success with live references left behind.

Latent today: in this tree no firewire-bus child can currently veto (sbp_detach sys/dev/disk/sbp/sbp.c:2016-2058, fwe_detach sys/dev/netif/fwe/if_fwe.c:284-295 and dcons_crom_detach sys/dev/misc/dcons/dcons_crom.c:190-207 all return 0 unconditionally; fwdev_destroydev sys/bus/firewire/fwdev.c:809-817 likewise). The defect is a structural trap, not a live regression β€” hence Low/untested.

How to run / what "reproducing" would mean

  1. Requires a machine with a real FireWire OHCI PCI/CardBus controller (PCI class 0c/10/10) β€” there is no such device in the QEMU guest (see run.log: pciconf -l shows only i440fx/virtio devices).
  2. Build the fwohci + firewire + sbp KLDs, attach storage over FireWire so an sbp target exists, then make sbp_detach() return EBUSY (upstream FreeBSD versions of sbp had such guards; this tree does not β€” add one to test).
  3. kldunload if_fwohci.ko β†’ observe: "detached" reported, unload completes, firewire watchdog callout (firewire.c:375-376, stopped only inside the never-completed firewire_detach at firewire.c:468-471) fires and executes OWRITE() through the zeroed sc->bst/sc->bsh (fwohci_pci.c:378-379) β†’ kernel page-0 write β†’ panic.
  4. With the fix (fix.diff), the unload fails with the child's error code and nothing is torn down.

run.sh documents the hardware-gating check performed on the audit guest.

Artifacts

  • lifecycle_trace.md β€” line-cited callgraph of both the veto and non-veto paths
  • check_guest.sh / run.sh + run.log β€” proof the audit guest has no OHCI device
  • fix.diff β€” git-apply-able fix (propagate bus_generic_detach error; never force-delete a child that vetoed; do not report unconditional success)
  • verdict.json β€” machine verdict (status untested, hardware-gated)
VERDICT.md
↓ download raw

DF-5528 β€” full narrative and verdict

Finding: fwohci_pci_detach() discards bus_generic_detach()'s error, force-deletes the firewire child regardless of its detach state, and returns 0 unconditionally. sys/bus/firewire/fwohci_pci.c:341-386 (specifically :350, :351-354, :385).

1. The code

341: fwohci_pci_detach(device_t self)
342: {
...
346:    crit_enter();
347:    if (sc->bsr)
348:        fwohci_stop(sc, self);
349:
350:    bus_generic_detach(self);                      /* <-- return discarded */
351:    if (sc->fc.bdev) {
352:        device_delete_child(self, sc->fc.bdev); /* <-- unconditional     */
353:        sc->fc.bdev = NULL;
354:    }
...
361:    if (sc->irq_res) { ... bus_teardown_intr ... }   /* IRQ handler gone  */
370:    if (sc->irq_res) { ... bus_release_resource ... }
375:    if (sc->bsr)   { ... bus_release_resource ...    /* BAR unmapped      */
378:        sc->bst = 0; sc->bsh = 0; }
382:    fwohci_detach(sc, self);                        /* DMA rings freed    */
383:    crit_exit();
385:    return 0;                                        /* <-- always success */

2. What newbus actually does (verified against sys/kern/subr_bus.c)

  • bus_generic_detach (subr_bus.c) walks children calling device_detach; device_detach (subr_bus.c:2128-2154) returns the child error and leaves the child DS_ATTACHED on failure.
  • device_delete_child (subr_bus.c:1284-1310) first calls device_detach(child) again (line 1292) β€” so a vetoing child re-runs its whole detach a second time on partially-torn-down state (fwdev_destroydev already ran once inside the first firewire_detach, firewire.c:462). If it vetoes again, device_delete_child returns before TAILQ_REMOVE/kobj_delete (subr_bus.c:1296-1306) β€” the child stays attached and linked into the parent's tree, but sc->fc.bdev was already NULLed by fwohci_pci.c:353.
  • The parent then completes the teardown: IRQ handler unregistered (:362), IRQ released (:371), BAR released and bst/bsh zeroed (:375-379), DMA rings freed by fwohci_detach β†’ fwohci_db_free (fwohci.c:764-785, fwohci.c:1158-1182), and reports success (:385). kldunload therefore completes.

3. Consequence chain once any grandchild vetoes

Firewire-bus device tree: fwohci (pci/cardbus) β†’ firewire β†’ {sbp, fwe, dcons_crom}.

  1. Live callouts on torn-down softc: firewire_attach starts the self-re-arming watchdog callout_reset(&sc->fc->timeout_callout, hz, firewire_watchdog, fc) (firewire.c:402-403, re-arm at firewire.c:375-376). Those callout_stops live at the END of firewire_detach (firewire.c:468-471) β€” code that a vetoing detach never reaches. After the parent's teardown the callout fires with bst == 0 && bsh == 0 (fwohci_pci.c:378-379); any OWRITE in the watchdog / timeout path (e.g. fwohci_start_atq via fc->atq->start, fwohci.c:629) writes through a null bus handle β†’ page-0 kernel write β†’ panic.
  2. DMA rings freed under live users: sbp keeps queued fw_xfers on the (already fwohci_stoped, fwohci_pci.c:348) controller; fwohci_detach (fwohci.c:773-777) frees the arr/at descriptor rings and buffers underneath them β€” post-free descriptor consumption / UAF.
  3. Orphaned attached child: still DS_ATTACHED, parented to a detached device; its ivars point at &sc->fc whose backing resources are gone; subsequent module unload of firewire UAFs on the freed softc.
  4. Lying success: kldunload/devctl detach report success with live references.

4. Why "latent" β€” the honesty section

In the CURRENT tree no firewire-bus child can veto: - sbp_detach sys/dev/disk/sbp/sbp.c:2016-2058 β†’ always return 0 - fwe_detach sys/dev/netif/fwe/if_fwe.c:284-295 β†’ always return 0 - dcons_crom_detach sys/dev/misc/dcons/dcons_crom.c:190-207 β†’ always return 0 - fwdev_destroydev sys/bus/firewire/fwdev.c:809-817 β†’ always return 0 so firewire_detach (firewire.c:453-487) cannot currently fail, and the normal detach path is clean (successful device_detach sets DS_NOTPRESENT, so the device_delete_child at :352 re-detaches nothing).

The trap arms the moment any child gains an EBUSY guard (upstream FreeBSD sbp carried such guards historically). The defect was inherited verbatim from FreeBSD β€” verified against freebsd-src release/13.4.0 sys/dev/firewire/fwohci_pci.c whose detach also discards the error β€” so this is NOT a DF-only divergence, and it is filed as robustness/hardening with a real, line-proven panic chain behind it.

Adjacent verified-but-harmless differences vs FreeBSD 13.4 (recorded, not filed): - DF calls bus_teardown_intr(self, sc->irq_res, sc->ih) with sc->ih == NULL in the bus_setup_intr-failure path (fwohci_pci.c:361-368); FreeBSD 13.4 guards with if (sc->ih). Traced harmless: pci_teardown_intr (sys/bus/pci/pci.c:3384+) masks INTx then nexus_teardown_intr (nexus.c:537-544) returns -1 for a NULL cookie β†’ error message only.

5. Verification status

  • status: untested / hardware-gated. Reproduction requires a physical FireWire OHCI controller (PCI 0c/10/10) plus a patched sbp (or any vetoing child). The audit QEMU guest has none β€” see run.log: pciconf -l lists only i440fx/virtio functions, dmesg contains no fwohci/firewire lines, no FW KLDs loaded. A KLD harness of the DF-5302 class (compile-the-core) cannot express this bug: the defect is in the newbus parent/child detach protocol, which only executes against a real PCI attachment.
  • Guest left untouched (read-only commands; guest_dirty=0).

6. Fix

fix.diff in this directory: capture and propagate the bus_generic_detach error before touching the child or any resource, mirroring the argument structure of the suspend path (fwohci_pci.c:395-397) which already propagates. Fix is source-level sound; no runtime fix validation was possible (no hardware), hence fix_status=not_testable.

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.diff (git apply --check OK against sys/bus/firewire/fwohci_pci.c) captures err = bus_generic_detach(self) and returns it after crit_exit() before any teardown, exactly matching the working suspend-path error handling; not runtime-validated because the baseline itself is hardware-gated (no OHCI controller on the guest, no vetoing child in the tree).

fix.diff hunk @@ -342,12 +342,24 @@; git apply --check output 'GIT APPLY CHECK: OK'; sys/ verified untouched (git status --short sys/ empty)
↓ fix.diffper-fix-DF-5528

Confirmed kernel references

Detail

Evidence (decisive lines)

read VERDICT.md sections 1-3 for the line-cited veto-path trace; run.log lines '=== PCI device classes ===' (no 0c10 device) and '=== dmesg fwohci/firewire === NONE'; fix.diff applies cleanly (git apply --check OK)

PoC changes

no seed PoC existed; the pack carries a source-level lifecycle trace plus a guest hardware-gating check (check_guest.sh) instead of a runnable trigger

Verified recommended fix

Propagate bus_generic_detach()'s error out of fwohci_pci_detach() before releasing any resource or deleting the child (fix.diff); mirror in the suspend path's already-correct pattern at fwohci_pci.c:395-397.

Verdict

fwohci_pci_detach() (sys/bus/firewire/fwohci_pci.c:341-386) discards bus_generic_detach()'s error at :350, force-deletes the firewire child at :351-354 regardless of its detach state, and returns unconditional success at :385; if any fwohci->firewire->{sbp,fwe,dcons_crom} child ever vetoes detach, the parent nonetheless unregisters the IRQ, unmaps the BAR (zeroing bst/bsh), and frees the OHCI DMA rings underneath a still-attached, orphaned firewire child whose self-re-arming watchdog callout (firewire.c:402-403, only stopped at firewire.c:468-471 inside the never-completed firewire_detach) then writes MMIO through a null bus handle -> page-0 kernel write/panic, plus UAF on the freed descriptor rings and a lying kldunload success. The defect is certain at the code level (traced through subr_bus.c:1284-1310 and :2128-2154) but LATENT in this tree: sbp_detach (sbp.c:2016-2058), fwe_detach (if_fwe.c:284-295), dcons_crom_detach (dcons_crom.c:190-207) and fwdev_destroydev (fwdev.c:809-817) all return 0 unconditionally today, so firewire_detach cannot currently fail and the normal path is clean. Untested because the trigger requires physical FireWire OHCI hardware (PCI 0c/10/10) plus a vetoing child: the audit QEMU guest has no such device (run.log: pciconf -l shows only i440fx/virtio, zero class=0c10 functions, no fwohci/firewire dmesg lines, no FW KLDs) and a compile-the-core KLD harness cannot express a newbus parent/child detach-protocol bug. Inherited verbatim from FreeBSD (verified against freebsd-src release/13.4.0 sys/dev/firewire/fwohci_pci.c, whose detach also swallows the error).