# 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 `break`s 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.

4. `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:

```diff
--- 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.
