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

Double-free / UAF in fwohci_pci_add_child error path: detach deletes child then caller deletes it again

Summary

In fwohci_pci_add_child if device_probe_and_attach(child) fails error handler calls fwohci_pci_detach(parent) which internally calls device_delete_child(self sc->fc.bdev) freeing the child device struct via kobj_delete. Control returns to add_child which immediately calls device_delete_child(parent child) on now-freed pointer producing UAF read on child->state potential corrupted-list TAILQ_REMOVE and kobj_delete double-free. sc->fc.bdev set to NULL after detach but local child still holds dangling pointer. sc->fc.bdev IS the same child pointer (set at :439). Currently unreachable (firewire probe/attach always return 0) but latent memory-corruption defect. [VERIFIED pass2: reproduced via forced-branch interposition - detach deletes child at :352 then add_child deletes the SAME pointer at :447, deterministic panic Bad link elm in device_delete_child on INVARIANTS, silent heap corruption on production; naturally unreachable today - identify-created child probe and attach always succeed and all allocs are M_INTWAIT, so latent memcorrupt; one-line fix-validated - drop the redundant delete]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2304 Β· 19 files
FileTypeDescriptionSize
fwpci_harness.c β€” 23.5 KB view raw
fwpci_harness.h β€” 1.8 KB view raw
fwohci_harness.c β€” 22.3 KB view raw
fwohci_harness.h β€” 3.9 KB view raw
fwpcictl.c β€” 3.6 KB view raw
fwpci_prep.sh β€” 1.9 KB view raw
build_kernel.sh β€” 358 B view raw
build_fix.sh β€” 519 B view raw
run2304.sh β€” 1.0 KB view raw
run.log β€” 2.0 KB view raw
panic.txt β€” 1.8 KB view raw
run_fixed.log β€” 2.1 KB view raw
fix.diff β€” 896 B view raw
env.txt β€” 1.4 KB view raw
build.log β€” 2.0 KB view raw
README.md β€” 4.1 KB ↓ raw
VERDICT.md β€” 6.2 KB ↓ raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.0 KB view raw

DF-2304 β€” Double-free / UAF in fwohci_pci_add_child error path

Verification pack for finding DF-2304 (sys/bus/firewire/fwohci_pci.c:446-447). Medium / memcorrupt. Claim: when device_probe_and_attach(child) fails, fwohci_pci_add_child calls fwohci_pci_detach(parent) (which deletes sc->fc.bdev == child via device_delete_child at fwohci_pci.c:352, freeing the child), and then calls device_delete_child(parent, child) AGAIN at fwohci_pci.c:447 on the freed pointer.

Verdict (short)

REPRODUCED on the verbatim-interposition harness (kernel-compiled, fwohci_pci.c TU-included unmodified): with device_probe_and_attach interposed to fail exactly at its fwohci_pci.c:442 call inside a REAL fwohci_pci_attach run, the REAL chain

fwohci_pci_attach -> fwohci_init (real, emulated OHCI regs+PHY)
-> bus_generic_probe -> firewire identify (bus_generic_identify)
-> BUS_ADD_CHILD -> fwohci_pci_add_child (the verbatim function)
-> device_add_child("firewire")   [REAL new-bus child]
-> [interposed] device_probe_and_attach FAIL (ENXIO)
-> fwohci_pci_detach(parent)      [device_delete_child #1, from :352]
-> device_delete_child(parent, child)   #2, from :447 -- FREED POINTER

panics the INVARIANTS kernel: panic: Bad link elm 0xfffff8011769c6a0 prev->next != elm inside the second device_delete_child, backtrace fwohci_pci_add_child+0xfd <- bus_generic_identify <- bus_generic_probe. Both deletes were logged by the pure logging wrapper with the same freed child pointer (see run.log / panic.txt).

Reachability is honestly latent: the error branch requires device_probe_and_attach of the firewire child to fail, but the child is created BY the firewire driver's own identify method (so its probe always matches: firewire_probe, firewire.c:313-317, returns 0 unconditionally), and firewire_attach (firewire.c:383-416) has no failure return. All allocation steps below it use M_INTWAIT (cannot fail). No unprivileged or remote input reaches this branch. The defect is real memory corruption in the code, demonstrated mechanically; it is NOT currently reachable through natural inputs, hence no privesc.

Rig

fwpci_harness.c (this pack) #includes the unmodified sys/bus/firewire/fwohci_pci.c; interposes exactly three call sites (device_probe_and_attach, bus_dma_tag_create, and a pure logging wrapper on device_delete_child); fabricates the PCI externalities with a real kobj bus class (bus/pci methods: read_ivar, alloc/release_resource, setup/teardown_intr, read/write_config). The fake fwohci device shell carries the real "fwohci" devclass (from the verbatim DRIVER_MODULE) and a kobj class whose bus_add_child method IS the verbatim fwohci_pci_add_child, so the stock bus_generic_probe inside the stock attach dispatches the real identify chain. OHCI externalities (register file + PHY emulation) come from the DF-2300 fwohci_harness.c rig, also installed. Root-only control device /dev/fwpcictl0.

Files

  • fwpci_harness.c/h β€” kernel rig (verbatim TU + interposition + PCI fabrications)
  • fwohci_harness.c/h β€” the DF-2300 rig, extended to export fh_regs
  • fwpcictl.c β€” userland driver
  • fwpci_prep.sh, build_kernel.sh, build_fix.sh β€” guest prep + builds
  • run2304.sh β€” the trigger (root): fwpcictl attach dpafail
  • run.log β€” full trigger output (note: ssh session dies at the panic)
  • panic.txt β€” serial-console panic + backtrace (the decisive evidence)
  • run_fixed.log β€” fixed-kernel A/B
  • fix.diff β€” combined 3-hunk fix (hunk 3 removes the :447 delete)
  • env.txt, build.log, manifest.json, verdict.json

Reproduce

# on the host, from this pack:
scp -F dfbsd-qemu/config * dfbsd:/root/fwo/
vm.sh run_root 'sh /root/fwo/fwpci_prep.sh && sh /root/fwo/build_kernel.sh'
vm.sh down && vm.sh up
vm.sh run_root 'sh /root/fwo/run2304.sh'   # guest panics; capture with vm.sh log

Expected: console shows both delete log lines (fwohci_pci.c:352 then :447, same child pointer), then panic: Bad link elm ... prev->next != elm.

Fixed kernel: vm.sh run_root 'sh /root/fwo/build_fix.sh', reboot, re-run run2304.sh: attach returns 0, exactly ONE delete (line 354 post-patch), no panic, resources balanced.

VERDICT.md
↓ download raw

DF-2304 VERDICT β€” double-free / UAF in fwohci_pci_add_child error path

  • finding: DF-2304, fwohci_pci.c:446-447 (fwohci_pci_add_child, :428-461)
  • status: reproduced (forced-branch mechanical reproduction of the exact double-delete; INVARIANTS kernel panics inside the second device_delete_child on the freed child)
  • impact: panic (INVARIANTS catch of the stale-pointer TAILQ_REMOVE; on a non-INVARIANTS kernel the same code path performs the corrupted-list writes and reaches the kobj_delete/kfree double free -- corruption class)
  • confidence: certain (for the defect itself and its behavior when the branch executes); the branch itself is NOT reachable via natural inputs today (see below)

Line-accurate control flow (stock tree)

  1. fwohci_pci.c:435 child = device_add_child(parent, "firewire", -1) -- REAL new-bus child (subr_bus.c make_device: kmalloc'd bsd_device, on parent->children and the global bus_data_devices list).
  2. fwohci_pci.c:439 sc->fc.bdev = child -- sc is the softc of bus (== parent in every real call: bus_generic_identify invokes BUS_ADD_CHILD(parent, parent, ...), subr_bus.c:2793).
  3. fwohci_pci.c:442 err = device_probe_and_attach(child).
  4. On err: fwohci_pci.c:446 fwohci_pci_detach(parent): * :350 bus_generic_detach(parent) returns EBUSY at attach time (parent state DS_ALIVE, subr_bus.c:2872-2873) -- child not detached there, but the child was never attached either (probe_and_attach failed), so its state is DS_NOTPRESENT; * :351-353 device_delete_child(parent, sc->fc.bdev) -- deletes and FREES the child: device_detach returns 0 (DS_NOTPRESENT), devclass_delete_device clears the devclass slot and NULLs child->devclass, TAILQ_REMOVE from parent->children and from bus_data_devices, kobj_delete((kobj_t)child, M_BUS) -> kfree (subr_bus.c:1284-1310). sc->fc.bdev = NULL afterwards.
  5. fwohci_pci.c:447 device_delete_child(parent, child) -- the local child still holds the now-dangling pointer (sc->fc.bdev was NULLed by the detach, but the caller kept its own copy). The second device_delete_child on the freed chunk: * reads freed child->state (device_detach, subr_bus.c:2134-2136); * skips devclass_delete_device only because the first delete NULLed child->devclass (otherwise the subr_bus.c:1159-1163 consistency panic "inconsistent device class" would fire); * TAILQ_REMOVE(&parent->children, child, link) with the stale child->link pointers -> writes through freed memory; * TAILQ_REMOVE(&bus_data_devices, child, devlink) -> global device list corruption; * kobj_delete/kfree of the already-freed chunk -> double free.

Mechanical reproduction (this run)

  • Rig: fwpci_harness.c TU-includes fwohci_pci.c verbatim; the fake fwohci device carries the real fwohci devclass; the REAL fwohci_pci_attach ran the REAL fwohci_init (emulated OHCI regs + PHY: version 1.0, 32 iso channels) and then the REAL bus_generic_probe -> bus_generic_identify -> BUS_ADD_CHILD -> fwohci_pci_add_child. device_probe_and_attach was interposed to return ENXIO at exactly the fwohci_pci.c:442 call.
  • Observed (run.log + panic.txt):
  • FWP: device_probe_and_attach(0xfffff8011769c6a0) INTERPOSED FAIL
  • fwohci0: probe_and_attach failed with err=6 (stock :444 print)
  • delete #1: FROM fwohci_pci.c:352 child=0xfffff8011769c6a0 -> rc=0 (freed), followed by the detach teardown/release sequence
  • delete #2: FROM fwohci_pci.c:447 child=0xfffff8011769c6a0 (SAME freed pointer)
  • panic: Bad link elm 0xfffff8011769c6a0 prev->next != elm backtrace: device_delete_child <- fwp_device_delete_child <- fwohci_pci_add_child+0xfd <- bus_generic_identify <- bus_generic_probe. 553 MB crash dump written. The INVARIANTS TAILQ consistency check fired on the stale link pointer before the double kfree was reached; on a production (non-INVARIANTS) kernel both stale-pointer writes and the kobj_delete/kfree of the freed chunk execute silently.

Reachability honesty (why this is latent, not privesc)

  • The only caller of fwohci_pci_add_child is the BUS_ADD_CHILD dispatch from bus_generic_identify (subr_bus.c:2793), which the firewire driver registers as its identify method on the fwohci bus devclass (firewire.c:106, DRIVER_MODULE(firewire, fwohci) at firewire.c:2207). It runs at device attach time (boot or kldload); no unprivileged or remote input influences it.
  • The created child is named "firewire" BY that same driver, so device_probe_child always finds firewire_probe, which returns 0 unconditionally (firewire.c:313-317). device_doattach then runs firewire_attach, which has no failure return (firewire.c:383-416). The remaining failure sources are M_INTWAIT allocations inside make_device/devclass_add_device, which cannot fail. Conclusion: with the in-tree firewire driver, the error branch cannot be entered through natural inputs; the finding stays Medium/latent-memcorrupt. If a future change to firewire.c introduced an attach failure, this branch becomes an immediate boot-time kernel heap corruption.
  • No user->root route exists for this bug in the current tree: the trigger requires an impossible probe/attach failure at configuration time. NOT privesc-class (contrast DF-2300, which is operator-gated register programming on live hardware).

Fix validation (A/B)

fix.diff hunk 3 removes device_delete_child(parent, child); at :447 (fwohci_pci_detach at :446 already deleted sc->fc.bdev, which IS child, and NULLed it). On the fixed kernel (run_fixed.log): * same forced-branch run: attach returns 0, NO panic, exactly ONE device_delete_child logged (fwohci_pci.c:354 -- the :352 line shifted by +2 lines from the two earlier hunks of the combined fix), resources balanced (MEM 1/1, IRQ 1/1, teardown 1, live handlers 0), guest stable. Baseline vs fixed: panic + double delete vs clean single delete.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt: the two logged deletes share child=0xfffff8011769c6a0 (FROM fwohci_pci.c:352 then FROM fwohci_pci.c:447) followed by panic Bad link elm and the fwohci_pci_add_child backtrace. run.log: full trigger output ending at the panic (ssh died). run_fixed.log: fixed kernel, same forced branch, exactly one delete (line 354 post-patch), no panic, resources balanced.

PoC changes

No seeded PoC existed; the rig was built fresh for this batch, extending the DF-2300 verbatim-interposition design to fwohci_pci.c (second TU: fwohci_pci.c TU-included with three call-site interposition macros; fabricated kobj PCI-bus parent + fake fwohci device shell carrying the real fwohci devclass; emulated OHCI regs/PHY reused from fwohci_harness.c). Fixed during bring-up: OHCI_VERSION value (mver bits 16-23 must be 1-9), bootverbose extern (already declared), sys/device.h include, BSD sed -i syntax, and softc resource-pointer preservation between rig runs.

Verified recommended fix

Remove the device_delete_child(parent, child) call at fwohci_pci.c:447; fwohci_pci_detach at :446 already deletes sc->fc.bdev (the same child) and NULLs it.

Verdict

Forced-branch mechanical reproduction on the verbatim-interposition harness: the real fwohci_pci_attach chain (bus_generic_probe -> firewire identify -> BUS_ADD_CHILD -> the verbatim fwohci_pci_add_child -> real device_add_child) ran with device_probe_and_attach interposed to fail exactly at fwohci_pci.c:442. The stock error path called fwohci_pci_detach(parent), whose device_delete_child at fwohci_pci.c:352 freed the child (kobj_delete/kfree, subr_bus.c:1306), and add_child then called device_delete_child(parent, child) again at fwohci_pci.c:447 on the same freed pointer. The INVARIANTS kernel panicked inside the second delete: panic Bad link elm prev->next != elm, backtrace fwohci_pci_add_child+0xfd <- bus_generic_identify <- bus_generic_probe, 553 MB dump. On a non-INVARIANTS kernel the stale-pointer TAILQ_REMOVEs (parent children list and the global bus_data_devices list) and the kobj_delete/kfree double free execute silently. REACHABILITY IS HONESTLY LATENT: the branch requires device_probe_and_attach of the firewire child to fail, but the child is created by the firewire drivers own identify method, firewire_probe returns 0 unconditionally (firewire.c:313-317), firewire_attach has no failure return (firewire.c:383-416), and the underlying allocations are M_INTWAIT. The branch runs at configuration time only (boot/kldload); no unprivileged or remote input reaches it. Not privesc-class today; it is a real latent heap double-free that fires if firewire attach ever grows a failure path.