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

tlphy_attach NULL-deref: strcmp(device_get_name(sibling), "tlphy") panics on not-yet-probed miibus sibling

Summary

The sibling capability walk passes device_get_name() of children that miibus pre-added but bus_generic_attach has not yet probed (devclass==NULL -> name NULL) straight into strcmp - boot-time NULL-deref panic when a 2nd+ TLAN10T PHY attaches with any higher-addressed PHY still unprobed. Normally masked (the stock dual-PHY card has the external PHY first); compiled into stock GENERIC. Physical-hardware-gated; NULL-read-only class. NULL-guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4923 Β· 8 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 6.0 KB ↓ raw
check_hardware.sh β€” 812 B view raw
run.log β€” 195 B view raw
env.txt β€” 664 B view raw
fix.diff β€” 655 B view raw
manifest.json β€” 859 B view raw
verdict.json β€” 2.4 KB view raw

DF-4923 β€” tlphy_attach NULL-deref: strcmp(device_get_name(sibling), "tlphy") panics on unprobed sibling PHY

  • File: sys/dev/netif/mii_layer/tlphy.c:177
  • Severity: Low β€” CWE-476 NULL pointer dereference (boot/device-attach-time panic)
  • Confidence: speculative (source-proven trigger chain; no hardware available to exercise it)
  • PoC status: not_reproduced β€” the QEMU guest has no ThunderLAN NIC (verified, see env.txt)

Build

Nothing to build. check_hardware.sh (runs on the DragonFly guest) proves the trigger precondition (a TI ThunderLAN controller, PCI vendor 0x100c) is absent, which is why the code path cannot be exercised on this guest.

Run

./check_hardware.sh     # on the guest, as root

Expected output (on this guest)

TLPHY-DF-4923: no ThunderLAN (vendor 0x100c) or Atheros (0x168c) PCI device present
TLPHY-DF-4923: tl/miibus compiled into X86_64_GENERIC but no device to attach -> path unreachable
TLPHY-DF-4923: finding NOT reproduced (hardware absent)

What the trigger WOULD be (real hardware)

A ThunderLAN NIC whose MII bus has: tlphy at address A (attaches 1st, instance 0), tlphy at address B > A (attaches 2nd β€” takes the mii->mii_instance != 0 walk at tlphy.c:170-184), and any third PHY at address C > B that is still in the not-yet-probed state when tlphy@B attaches. device_get_name() returns NULL for that sibling (sys/kern/subr_bus.c:1632-1637), and strcmp(NULL, "tlphy") at tlphy.c:177 faults reading address 0 β†’ kernel panic during device attach.

Full source trace in VERDICT.md.

VERDICT.md
↓ download raw

DF-4923 VERDICT β€” not_reproduced (hardware absent); source trace confirms the NULL-deref chain

One-paragraph bottom line

tlphy_attach() walks all sibling children of the miibus when the tlphy being attached is the 2nd+ PHY on the bus, and passes each child's device_get_name() straight into strcmp() without a NULL check (tlphy.c:175-183). Children are all added up front by miibus_probe() (mii.c:130-167) and only probed/attached one-by-one afterwards by bus_generic_attach() (subr_bus.c:bus_generic_attach), so any sibling later in the child TAILQ than the currently-attaching tlphy still has devclass == NULL while the walk runs; device_get_name() returns NULL for exactly that state (sys/kern/subr_bus.c:1632-1637), and strcmp(NULL, "tlphy") dereferences address 0 β†’ kernel panic at device-attach time. The bug is real in the source but is gated on an exotic ThunderLAN topology (β‰₯2 TLAN10T PHYs plus a later- addressed PHY on one MII bus) that the audit guest (and virtually all modern hardware) does not have, so it is filed Low/speculative and could not be reproduced at runtime. Fix is a one-line NULL guard.

Full source trace (attacker-input β†’ sink)

  1. sys/dev/netif/mii_layer/mii.c:130-167 β€” miibus_probe() iterates ma.mii_phyno = 0..MII_NPHY-1 and does device_add_child(dev, NULL, -1) for every address whose MDIO read succeeds (miibus_no_phy() at mii.c:118-127 returns false). All sibling children therefore exist in the miibus dev->children TAILQ before any of them is probed. Children are ordered by ascending MII address.

  2. miibus_attach() (mii.c:169-183) ends with bus_generic_attach(dev), which walks the child TAILQ in order and runs device_probe_and_attach(child) sequentially (sys/kern/subr_bus.c, bus_generic_attach). While child N attaches, children N+1.. have devclass == NULL (a child's devclass is only set once a driver is selected for it β€” device_probe_child() sets/clears it transiently during probing at subr_bus.c:1412-1445 and persists it only for the winner).

  3. sys/dev/netif/mii_layer/tlphy.c:170-184 β€” when a tlphy is the 2nd+ PHY attaching on the bus (mii->mii_instance != 0, incremented at tlphy.c:186), attach fetches the full child list:

c device_get_children(sc->sc_mii.mii_dev, &devlist, &devs); for (i = 0; i < devs; i++) { if (strcmp(device_get_name(devlist[i]), "tlphy")) { /* line 177 */ other = device_get_softc(devlist[i]); capmask &= ~other->mii_capabilities; break; } }

The walk only breaks on a non-tlphy name. If every earlier sibling is a tlphy and ANY later sibling exists (still unprobed β‡’ devclass == NULL), the loop reaches it.

  1. sys/kern/subr_bus.c:1632-1637:

c const char * device_get_name(device_t dev) { if (dev->devclass) return devclass_get_name(dev->devclass); return(NULL); }

β†’ strcmp(NULL, "tlphy") at tlphy.c:177 β†’ page fault on address 0 β†’ kernel panic (panic: page fault in strcmp called from tlphy_attach).

Why the walk's break usually saves it (and when it doesn't)

  • On the stock dual-PHY ThunderLAN card (internal TLAN10T + external non-TI 100baseTX PHY): the external PHY normally sits at the lower address, so the walk hits an already-attached non-tlphy sibling and breaks immediately β€” no crash. This is why the bug has never been seen in the field.
  • Crash requires: tlphy@A (1st), tlphy@B>A (2nd, takes the walk), and any PHY @C>B. I.e. at least two TLAN10T PHYs on one bus β€” hardware I cannot confirm ever shipped, hence speculative confidence.
  • Note DFly's device_get_children() cannot fail (subr_bus.c:1595-1617, M_INTWAIT|M_ZERO), so the unchecked return at tlphy.c:175 is harmless here; the NULL-name case is the actual defect.

Mitigating facts

  • ukphy binds (ukphy_probe returns -100, ukphy.c) to every readable PHY, so no child stays unbound after the full attach pass β€” the NULL window is only the not-yet-probed state of later siblings.
  • Attach-time only: no runtime/unprivileged trigger; requires physically inserting the exotic NIC (or a PCI hot-plug event during boot attach).
  • Softc is M_ZERO-allocated (subr_bus.c:1952-1954), so sc_need_acomp starts 0 and the other->mii_capabilities read is in-bounds for any PHY softc (all begin with struct mii_softc).

Reproduction attempts on the audit guest

  1. dfbsd-qemu/vm.sh status β†’ up (stock X86_64_GENERIC, built 2026-07-02).
  2. pciconf -l β†’ 7 PCI devices, zero with vendor 0x100c (TI ThunderLAN) or 0x168c (Atheros). tl/miibus/tlphy are compiled into the kernel (sys/config/X86_64_GENERIC:223-243) but never attach.
  3. Conclusion: the code path is unreachable on this guest; runtime reproduction requires physical ThunderLAN hardware with the multi-PHY topology above. Status honestly recorded as not_reproduced, impact none on this guest.

Fix (authored, not applied β€” sys/ is read-only)

tlphy.c:177 β€” skip children that have no devclass yet:

--- a/sys/dev/netif/mii_layer/tlphy.c
+++ b/sys/dev/netif/mii_layer/tlphy.c
@@ -174,7 +174,9 @@ tlphy_attach(device_t dev)
        device_get_children(sc->sc_mii.mii_dev, &devlist, &devs);
        for (i = 0; i < devs; i++) {
-           if (strcmp(device_get_name(devlist[i]), "tlphy")) {
+           const char *dname = device_get_name(devlist[i]);
+           if (dname != NULL && strcmp(dname, "tlphy") != 0) {
                other = device_get_softc(devlist[i]);
                capmask &= ~other->mii_capabilities;
                break;

(fix.diff carries the same change; fix validation not possible without the trigger hardware β†’ 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 adds the NULL guard at tlphy.c:177, but baseline itself cannot be triggered without the exotic ThunderLAN multi-PHY hardware, so patched-kernel validation is impossible on this guest; fix correctness is established by the source trace alone

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

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4923/VERDICT.md carries the full path:line trace (mii.c:130-167 child pre-addition vs sequential bus_generic_attach; tlphy.c:170-186 walk; subr_bus.c:1632 NULL return); run.log shows 7 PCI devices and zero vendor 0x100c/0x168c matches on the stock X86_64_GENERIC guest

PoC changes

no seed PoC existed; replaced with a hardware-precondition check (check_hardware.sh) plus a source-trace proof since the trigger is physical-NIC topology

Verified recommended fix

Skip siblings whose device_get_name() is NULL in tlphy_attach's capability walk

Verdict

tlphy_attach() passes each miibus sibling's device_get_name() straight into strcmp() (tlphy.c:177); siblings not yet probed have devclass==NULL so device_get_name() returns NULL (subr_bus.c:1632-1637) and strcmp(NULL, ...) panics at device-attach time. Trigger needs >=2 TLAN10T PHYs plus a later-addressed PHY on one ThunderLAN MII bus; the audit guest has no TI 0x100c PCI device at all (pciconf evidence in run.log/env.txt), so the path is unreachable there and the finding stays speculative/Low. Fix: skip NULL-named siblings (fix.diff).