TBI-mode nge(4) deletes itself: sc->nge_miibus = dev passed to device_delete_child(dev, dev) β unbounded nge_detach recursion on detach, live-device kobj_delete UAF on attach failure
Summary
When no PHY is found and NGE_CFG_TBI_EN is set, attach points the miibus handle at the device itself; nge_detach then calls device_delete_child(dev, dev). On normal detach the nested device_detach re-enters nge_detach recursively forever (double bus_teardown_intr, double ether_ifdetach, double contigfree of the 3.3MB jumbo arena) - guaranteed panic. On the attach-fail path the nested detach unlinks the LIVE device from the PCI bus children mid-attach and kobj_delete()s it - newbus UAF for the rest of device_attach. TBI (fiber) boards + root trigger. Self-reference-guard fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4114 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| presence_probe.sh | β | 425 B | view raw | |
| env.txt | β | 347 B | view raw | |
| fix.diff | β | 600 B | view raw | |
| manifest.json | β | 798 B | view raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-4114 β nge(4) TBI mode: sc->nge_miibus = dev self-reference makes nge_detach delete itself β recursion panic / live-device free (UAF)
What this is
When no MII PHY is found and the TBI (ten-bit/fiber) interface is
enabled, nge_attach points the miibus handle at the device itself:
} else if (CSR_READ_4(sc, NGE_CFG) & NGE_CFG_TBI_EN) { /* if_nge.c:860 */
sc->nge_tbi = 1;
sc->nge_miibus = dev; /* if_nge.c:864 - SELF */
nge_detach then hands that handle to newbus as if it were a child:
if (sc->nge_miibus)
device_delete_child(dev, sc->nge_miibus); /* if_nge.c:941-942 = (dev, dev) */
device_delete_child(parent, child) (sys/kern/subr_bus.c:1284-1310)
unconditionally does:
if ((error = device_detach(child)) != 0) /* subr_bus.c:1292 */
return(error);
while ((grandchild = TAILQ_FIRST(&child->children))) /* :1296 */
device_delete_child(child, grandchild);
if (child->devclass)
devclass_delete_device(child->devclass, child); /* :1302-1303 */
TAILQ_REMOVE(&dev->children, child, link); /* :1304 - child NOT a member of dev->children */
TAILQ_REMOVE(&bus_data_devices, child, devlink); /* :1305 */
kobj_delete((kobj_t)child, M_BUS); /* :1306 - frees the device */
Two distinct blow-ups:
A. Normal detach (kldunload if_nge / devctl) β unbounded recursion.
nge_detach is invoked from device_detach while dev->state is
still DS_ATTACHED (state is only set to DS_NOTPRESENT at
subr_bus.c:2149, after DEVICE_DETACH returns). The nested
device_delete_child(dev, dev) β device_detach(dev) sees DS_ATTACHED
(subr_bus.c:2136-2137) β calls DEVICE_DETACH(dev) β nge_detach
again β same device_delete_child(dev, dev) β β¦ Each level
re-runs the attached block: second bus_teardown_intr on the stale
cookie, second ether_ifdetach/if_detach on an already-detached
ifnet, double contigfree of nge_ldata and the 3.3 MB jumbo arena β
list corruption / double-free panics within a few levels, or kernel
stack exhaustion if it somehow gets further. Panic either way.
B. Attach-failure path β live device freed (UAF). nge_attach
calls nge_detach(dev) directly on its own goto fail (if_nge.c:921)
β e.g. bus_setup_intr failure (if_nge.c:913-916) after the TBI branch
was taken. There dev->state is DS_ATTACHING, so the nested
device_detach(dev) returns 0 immediately (subr_bus.c:2136-2137 "state
!= DS_ATTACHED β return(0)"), the grandchild loop is empty, and
execution reaches TAILQ_REMOVE(&dev->children, dev, link) (subr_bus.c:1304)
β removing the device from a list it is not a member of, which
(tqe links being per-entry) silently unlinks dev from its parent
PCI bus's children list mid-attach β followed by
kobj_delete((kobj_t)dev) (:1306) freeing the live device_t.
device_attach then keeps using the freed device (state store,
sysctl_fini, printf) β use-after-free write/read in newbus on a
root-triggered path.
Threat model
Requires (a) a TBI-mode DP83820 board (fiber/1000baseSX; no external
PHY) β NGE_CFG_TBI_EN β and (b) root (kldunload/devctl/driver
re-probe, or an IRQ-allocation failure at boot for path B). Privileged
+ specific hardware β Low severity, but a genuine newbus-corrupting
logic bug (and DFly-specific: the TBI branch is a DragonFly addition).
PoC status β untested (hardware absent)
Guest has no DP83820 of any kind (presence_probe.sh); the TBI branch cannot be entered. Source-level certainty: every step above is unconditional on that path.
Fix
Never delete ourselves β guard sc->nge_miibus != dev (fix.diff).
DF-4114 β VERDICT
Finding: In TBI mode nge_attach sets sc->nge_miibus = dev
(sys/dev/netif/nge/if_nge.c:864); nge_detach then calls
device_delete_child(dev, sc->nge_miibus) = device_delete_child(dev,
dev) (if_nge.c:941-942), which newbus does not support.
Status: untested. Hardware-gated: requires a TBI-mode DP83820
(NGE_CFG_TBI_EN, no external PHY) plus root (detach path) or an
IRQ-allocation failure at boot (attach-fail path). The audit guest has
no NS DP83820 at all (presence_probe.sh: NGE_ABSENT), so neither branch
can be entered. Source-level certainty β traced step by step below.
Path A β normal detach (kldunload if_nge, devctl): unbounded
recursion β panic.
1. kldunload β device_detach(dev) with dev->state == DS_ATTACHED
(sys/kern/subr_bus.c:2136).
2. DEVICE_DETACH(dev) β nge_detach(dev); state is still
DS_ATTACHED (it becomes DS_NOTPRESENT only at subr_bus.c:2149, after
the method returns).
3. nge_detach reaches if_nge.c:941-942 β device_delete_child(dev,
dev) β subr_bus.c:1292 device_detach(dev) β still DS_ATTACHED β
DEVICE_DETACH(dev) β nge_detach again β same call β infinite
recursion. Each frame re-runs the attached block: second
bus_teardown_intr (stale cookie), second ether_ifdetach on an
already-detached ifnet, contigfree of nge_ldata and the jumbo
arena a second time (if_nge.c:949-954) β double-free/list corruption
panics within a few recursion levels; unbounded stack otherwise.
Path B β attach-failure path (nge_attach goto fail β direct
nge_detach at if_nge.c:921, e.g. bus_setup_intr failure at
:913-916 after the TBI branch): live device freed β newbus UAF.
1. dev->state == DS_ATTACHING during the direct call, so
device_is_attached(dev) is false and the stop/teardown block is
skipped; sc->nge_miibus == dev is non-NULL β device_delete_child
(dev, dev).
2. Nested device_detach(dev) returns 0 immediately
(subr_bus.c:2136-2137: state != DS_ATTACHED β 0).
3. grandchild loop empty (TBI mode added no children) β
devclass_delete_device (removes the live device from the nge
devclass), TAILQ_REMOVE(&dev->children, dev, link)
(subr_bus.c:1304) β dev is not a member of its own children list;
the per-entry tqe links make this silently unlink dev from the
parent PCI bus children list mid-attach β then
kobj_delete((kobj_t)dev, M_BUS) (subr_bus.c:1306) frees the
live device.
4. device_attach/probe machinery continues on the freed device_t
(state stores, devname printf, sysctl) β use-after-free read/write
in newbus.
Impact ceiling: privileged DoS with certainty (panic/UAF); on TBI-equipped systems it also bricks driver re-probing for the slot. Not remotely triggerable; no unprivileged path.
Fix: never pass the self-reference to newbus β
if (sc->nge_miibus && sc->nge_miibus != dev) (fix.diff; validated as
text against the read-only tree, patch --dry-run clean; runtime
validation impossible without a TBI DP83820 β fix_status
not_tested).
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_testablefix.diff authored against the read-only sys/ tree (patch --dry-run clean); cannot be validated without a TBI-mode DP83820 board.
findings/poc/DF-4114/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4114/VERDICT.md (trace if_nge.c:860-864/913-922/941-942/949-954, subr_bus.c:1284-1310/2129-2153); presence_probe.sh + env.txt; fix.diff (self-child guard)
PoC changes
No seed PoC. Hardware-gated (no DP83820/TBI board on guest); presence probe only.
Verified recommended fix
Guard the detach self-reference: if (sc->nge_miibus && sc->nge_miibus != dev) device_delete_child(dev, sc->nge_miibus);
Verdict
Source-certain, hardware-gated. In TBI mode nge_attach sets sc->nge_miibus = dev (self-reference, sys/dev/netif/nge/if_nge.c:864); nge_detach then calls device_delete_child(dev, dev) (if_nge.c:941-942). Path A (normal detach via kldunload/devctl): the nested device_delete_child -> device_detach(dev) sees state still DS_ATTACHED (set to DS_NOTPRESENT only at subr_bus.c:2149, after DEVICE_DETACH returns) and re-enters nge_detach recursively - unbounded recursion re-running teardown each level (double bus_teardown_intr, double ether_ifdetach, double contigfree of nge_ldata and the jumbo arena, if_nge.c:949-954) -> guaranteed panic (list corruption/double-free or stack exhaustion). Path B (attach-fail: nge_attach goto fail calls nge_detach directly at if_nge.c:921, e.g. bus_setup_intr failure at :913-916 after the TBI branch): the nested device_detach returns 0 for DS_ATTACHING (subr_bus.c:2136-2137), the empty-grandchild loop is skipped, and TAILQ_REMOVE(&dev->children, dev, link) (subr_bus.c:1304) silently unlinks the live device from its PARENT PCI bus children list mid-attach, then kobj_delete((kobj_t)dev) (subr_bus.c:1306) frees the live device_t - use-after-free in newbus for the remainder of device_attach. Requires a TBI-mode DP83820 board plus root (or boot-time IRQ alloc failure) -> Low severity, privileged DoS + newbus corruption. Guest has no DP83820 (presence_probe.sh: NGE_ABSENT): honestly untested; not a false positive - every step is unconditional in the source.
No comments yet.