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

cardbus_detach_card ignores device_detach/device_delete_child failures and pci_freecfg's the child devinfo anyway: surviving child keeps freed ivars (UAF) while its bus resources are ripped out from under the running driver

Summary

Both error returns are dropped yet the devinfo is unconditionally kfree'd - a busy child (NIC up / open tty) survives eject/suspend with freed ivars AND its resources released + BARs zeroed: the next driver kldload walks freed dinfo (UAF read; pci_cfg_restore writes card-controlled config into reclaimed heap) while the old driver keeps using released bus space. Hw-gated (no cbb on guest); memcorrupt class. Propagate-both-errors fix (git-apply clean).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4977 Β· 8 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
trigger.md β€” 2.2 KB ↓ raw
env.txt β€” 1.4 KB view raw
build.sh β€” 106 B view raw
run.sh β€” 142 B view raw
fix.diff β€” 1.4 KB view raw
verdict.json β€” 3.5 KB view raw

DF-4977 β€” cardbus_detach_card ignores device_detach/device_delete_child failures and frees the child devinfo anyway β†’ dangling ivars UAF

Target file: sys/dev/pccard/cardbus/cardbus.c (pass 2, GLM 5.3)

TL;DR

cardbus_detach_card() (sys/dev/pccard/cardbus/cardbus.c:220-251):

236     for (tmp = 0; tmp < numdevs; tmp++) {
237         struct cardbus_devinfo *dinfo = device_get_ivars(devlist[tmp]);
238         int status = device_get_state(devlist[tmp]);
239
240         if (dinfo->pci.cfg.dev != devlist[tmp])
241             device_printf(cbdev, "devinfo dev mismatch\n");
242         if (status == DS_ATTACHED || status == DS_BUSY)
243             device_detach(devlist[tmp]);                       /* (1) error ignored */
244         cardbus_release_all_resources(cbdev, dinfo);               /* (2) rips resources from a possibly-live driver */
245         device_delete_child(cbdev, devlist[tmp]);                  /* (3) error ignored */
246         pci_freecfg((struct pci_devinfo *)dinfo);                  /* (4) unconditional kfree(dinfo) */
247     }

If the child driver's device_detach at (1) fails (returns EBUSY β€” e.g. the device is open/busy), DragonFly's device_delete_child() returns the error and leaves the child device in the tree (sys/kern/subr_bus.c:1284-1293: if ((error = device_detach(child)) != 0) return(error); β€” the kobj_delete/TAILQ_REMOVE happen only after a successful detach). But the loop then:

  • releases every bus resource of the still-live driver and zeroes its BARs (cardbus.c:300-311, rle->res = NULL while the driver's softc still holds the struct resource *), and
  • pci_freecfg(dinfo) β†’ kfree(dinfo, M_DEVBUF) (sys/bus/pci/pci.c:2139-2156), while child->ivars == dinfo still.

Result: a live device whose every later interaction dereferences freed memory, plus already-released MMIO/IO windows still mapped by the running driver (cross-device bus-space aliasing).

Concrete trigger (requires CardBus hardware)

  1. Insert a CardBus card whose driver keeps the device busy across detach β€” classic candidates compiled into this kernel (see env.txt): cardbus/fxp, cardbus/if_re, cardbus/if_dc, pccard/sio (serial: an open /dev/ttydX keeps it busy without root on many setups; an IFF_UP NIC detach returns EBUSY in several DF drivers).
  2. Eject the card (or suspend the machine: cardbus_suspend β†’ cardbus_detach_card, cardbus.c:121-126; or re-insert another card: cardbus_attach_card calls cardbus_detach_card first, cardbus.c:168).
  3. device_detach(child) returns EBUSY β†’ child survives with freed ivars.
  4. Any later touch of the surviving child is a UAF: e.g. kldload a new cardbus driver β†’ cardbus_driver_added β†’ dinfo = device_get_ivars(dev) + pci_print_verbose(&dinfo->pci) (cardbus.c:281-282, 287) reads freed heap; any bus method on the child (read_ivar/probe/allocation) follows child->ivars.

Why "untested" on this run

The audit QEMU guest (DragonFly 6.5-DEVELOPMENT x86_64, see env.txt) has no CardBus bridge β€” pciconf -l shows only i440FX/virtio devices, no cbb. The cardbus driver set is compiled into X86_64_GENERIC (kldstat -v shows cbb/cardbus) but with zero cbb parent devices there is no cardbus bus instance, so cardbus_detach_card can never execute. The defect is proven by code reading (cardbus.c:236-247 + subr_bus.c:1284-1310 + pci.c:2139-2156); dynamic reproduction needs physical CardBus hardware (card eject / suspend with a busy card).

Fix

See fix.diff β€” abort the per-child teardown (keep child + dinfo alive, propagate the error) when either device_detach or device_delete_child fails, and only free dinfo after the child is actually gone.

VERDICT.md
↓ download raw

VERDICT β€” DF-4977

Status: untested (restricted enum; the natural-language classification is "not testable on this guest: no CardBus hardware"). Reproduced: 0.

What the defect is (certain, by code reading)

cardbus_detach_card() at sys/dev/pccard/cardbus/cardbus.c:236-247:

  1. device_detach(devlist[tmp]) (line 243) β€” return value ignored.
  2. cardbus_release_all_resources(cbdev, dinfo) (line 244) β€” releases every resource the (possibly still-attached) child driver holds and zeroes its BARs (cardbus.c:300-311; rle->res = NULL at :304, pci_write_config(..., 0, 4) at :309).
  3. device_delete_child(cbdev, devlist[tmp]) (line 245) β€” return value ignored. DragonFly semantics: device_delete_child calls device_detach(child) again and returns the error without removing the child when detach fails (sys/kern/subr_bus.c:1284-1293): c 1292 if ((error = device_detach(child)) != 0) 1293 return(error); i.e. on a busy child the device stays in cbdev's child list with child->ivars == dinfo intact.
  4. pci_freecfg((struct pci_devinfo *)dinfo) (line 246) β€” unconditional kfree(dinfo, M_DEVBUF) (sys/bus/pci/pci.c:2139-2156).

Net effect when a child's detach fails (EBUSY from an open device / IFF_UP NIC):

  • Use-after-free of dinfo: the surviving child's ivars point at freed memory. Next dereference: cardbus_driver_added on any later driver kldload (cardbus.c:281-287: dinfo = device_get_ivars(dev), pci_print_verbose(&dinfo->pci), pci_cfg_restore(dinfo->pci.cfg.dev, &dinfo->pci)), or any bus method (read_ivar/write_ivar/resource ops) on the surviving child. The freed cardbus_devinfo (~0x200+ bytes, M_DEVBUF zone) is attacker-reclaimable via the usual slab grooming if the attacker can drive kernel allocations between the free and the reuse β€” on a machine with the hardware this is a plausible privilege- escalation primitive (ivars confusion / config-space restore of a forged pcicfgregs), but that chain was not developed here because the primitive could not even be triggered on the audit guest (no hardware).
  • Resource lifetime violation: the still-running child driver keeps struct resource * pointers whose bus space was released at step 2 and whose BARs were zeroed β€” the windows can be re-allocated to another device while the old driver keeps doing bus_space_write to them β†’ cross-device MMIO aliasing (hardware-level memory corruption, device-dependent).

Trigger requirements (why untested)

  • A cbb (CardBus bridge) controller with a card inserted β€” the audit QEMU guest has none (env.txt: pciconf -l lists only i440FX/virtio; kldstat -v shows the cardbus drivers compiled in but idle). QEMU's default machine cannot surface a cbb device, and DF has no userland way to synthesize a PCI bridge.
  • A child driver whose device_detach returns non-zero while attached (busy device). Candidates in this kernel: cardbus/if_re, cardbus/if_dc, cardbus/if_fxp (detach commonly EBUSY while interface up / bpf attached), pccard/sio (open tty).
  • The card event itself: physical eject, re-insert of another card (cardbus_attach_card at :168 calls cardbus_detach_card first), or suspend (cardbus_suspend at :121-126). Root can reach the same path via kldunload-style driver detach, but then it is already privileged.

Severity is filed as Low per the audit rubric ("requires specific hardware"), though the defect class is memcorrupt (UAF + resource lifetime) and would rate higher on hardware that exposes it.

Honest classification mapping

  • Guest reachable but the target device/feature is absent β†’ per the restricted enum for this run the status is recorded as untested (impact none, no dynamic evidence). Attempts: 0 (nothing executable without the hardware; the "PoC" is the trigger analysis in README.md + trigger.md).

Fix validation

Not possible on this guest (same hardware gate); fix.diff is authored against the read-only sys/ tree from line-accurate reading, never applied there. fix_status: not_testable.

Key citations

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.

trigger.md
↓ download raw

DF-4977 β€” trigger sequence (hardware-gated; not executable on the audit guest)

No userland program can drive this path on a machine without a CardBus controller: the entry points are card events (CSC interrupts β†’ card_attach_card/card_detach_card on the cbb driver) or privileged driver detach. The faithful reproduction is a physical sequence:

Preconditions

  • Machine with a cbb CardBus bridge (TI/Ricoh/O2Micro PCI-PCI bridge, hdrtype 2), DragonFly kernel with cardbus (built into X86_64_GENERIC β€” confirmed via kldstat -v on the audit guest, see env.txt).
  • A CardBus card whose driver fails device_detach while busy. Example with the compiled-in if_re/fxp/dc families:
# as root: bring the cardbus NIC up so its detach will refuse
ifconfig re0 up        # or: keep a bpf reader attached: tcpdump -i re0 >/dev/null &
# OR with a serial card (pccard/sio):
stty -f /dev/ttyd0.open       # keep dial-out open as any user of group dialer

Fire

# physical: eject the card   -> cbb CSC interrupt -> CARD_DETACH_CARD(cardbus)
#                             -> cardbus_detach_card() -> device_detach()==EBUSY
#                             -> resources released + dinfo freed, child survives
# equivalent software path (root): acpiconf -s 1   (cardbus_suspend -> same function)

Observe (kernel debugger or instrumented build)

kgdb> p *(struct cardbus_devinfo *)<child->ivars>   # freed memory read
  • pciconf -l after the botched detach still lists the child (device_delete_child failed) β€” its ivars are freed.
  • kldload <any cardbus driver> β†’ cardbus_driver_added() β†’ pci_print_verbose(&dinfo->pci) walks the freed devinfo (cardbus.c:281-287) β€” UAF read; pci_cfg_restore writes restored BAR values into dinfo->pci.cfg β€” UAF write of card-controlled config values into reclaimed heap.
  • The still-live driver continues to touch its released resources β†’ bus-space aliasing.

Why not run

The audit QEMU guest has no cbb device (env.txt pciconf -l), and neither QEMU's i440FX machine nor DF userland offers a way to synthesize a CardBus bridge, so the sequence above cannot be executed there. Nothing was built or run; attempts = 0.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix validation impossible on the audit guest for the same reason the PoC is untested (no CardBus controller; the path cannot execute). fix.diff was authored against the read-only sys/ tree from line-accurate reading and never applied there.

findings/poc/DF-4977/fix.diff
↓ fix.diffper-fix-DF-4977

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4977/VERDICT.md (code-path proof), README.md/trigger.md (physical trigger), env.txt (guest pciconf/kldstat proof of no cardbus hardware), fix.diff (authored, not applied to sys/)

PoC changes

No seeded PoC existed; the pack was authored from scratch as a hardware-gated trigger analysis (trigger.md). Nothing was built or run (attempts 0) because the guest lacks any cbb/CardBus controller.

Verified recommended fix

Abort per-child teardown when device_detach or device_delete_child fails: keep the child and its dinfo alive and propagate the error; only pci_freecfg after the child is actually deleted (see fix.diff).

Verdict

cardbus_detach_card (cardbus.c:236-247) ignores the return of device_detach (line 243) and device_delete_child (line 245) yet unconditionally pci_freecfg's the child devinfo (line 246). DF's device_delete_child returns the detach error and leaves the child in the tree (subr_bus.c:1284-1293), so a busy child (detach EBUSY) survives with child->ivars pointing at freed memory, while cardbus_release_all_resources (line 244) has already ripped its bus resources and zeroed its BARs out from under the running driver. Next touch of the surviving child (e.g. cardbus_driver_added at cardbus.c:281-287 after any driver kldload: pci_print_verbose(&dinfo->pci) read, pci_cfg_restore write) is a UAF. Untested because the trigger needs a CardBus bridge and a card event (eject/re-insert/suspend) with a busy cardbus child; the audit QEMU guest has no cbb controller at all (env.txt), so the code path cannot execute there. Defect proven by code reading; exploit chain not developed (hardware gate), hence impact recorded as none-demonstrated with the class being UAF/memcorrupt.