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)
PoC verification
Evidence pack
findings/poc/DF-2304 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 exportfh_regsfwpcictl.cβ userland driverfwpci_prep.sh,build_kernel.sh,build_fix.shβ guest prep + buildsrun2304.shβ the trigger (root):fwpcictl attach dpafailrun.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/Bfix.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.
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)
- 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). - fwohci_pci.c:439
sc->fc.bdev = child-- sc is the softc ofbus(== parent in every real call: bus_generic_identify invokes BUS_ADD_CHILD(parent, parent, ...), subr_bus.c:2793). - fwohci_pci.c:442
err = device_probe_and_attach(child). - 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-353device_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. - fwohci_pci.c:447
device_delete_child(parent, child)-- the localchildstill 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 FAILfwohci0: 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 != elmbacktrace: 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_testableConfirmed 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.
No comments yet.