# DF-1461 — VERDICT

## Verdict: REPRODUCED (source-level + harness; runtime-unreachable without 3Com HW)

## Bug mechanism

`txp_rxbuf_reclaim()` in `sys/dev/netif/txp/if_txp.c:747-797` refills the
receive buffer ring by allocating an mbuf for each empty slot. On
`MGETHDR` or `MCLGET` failure, the error path frees the **persistent
per-slot** `struct txp_swdesc *sd` — an allocation that lives for the
entire lifetime of the interface and is only meant to be freed in
`txp_detach`.

### The vulnerable code (if_txp.c:761-797)

```c
while (1) {
    sd = rbd->rb_sd;                    // line 762: read persistent ptr
    if (sd->sd_mbuf != NULL)
        break;                          // slot already set up

    MGETHDR(sd->sd_mbuf, M_NOWAIT, MT_DATA);
    if (sd->sd_mbuf == NULL)
        goto err_sd;                    // line 768: MGETHDR failed

    MCLGET(sd->sd_mbuf, M_NOWAIT);
    if ((sd->sd_mbuf->m_flags & M_EXT) == 0)
        goto err_mbuf;                  // line 772: MCLGET failed
    // ... success path ...
}
sc->sc_rxbufprod = i;                   // line 789: ONLY reached on success
return;

err_mbuf:
    m_freem(sd->sd_mbuf);
err_sd:
    kfree(sd, M_DEVBUF);               // line 796: BUG — frees persistent alloc
}
```

### Three consequences

1. **Dangling pointer + UAF read.** After the error path, `rbd->rb_sd` still
   points to the freed `sd` (never NULLed), and `sc->sc_rxbufprod` is never
   advanced (line 789 only runs on the success path). The next call to
   `txp_rxbuf_reclaim` — from `txp_intr` (line 630) or `txp_tick` (line
   1155) — reads `sd = rbd->rb_sd` (line 762), a dangling pointer, then
   dereferences `sd->sd_mbuf` (line 763). Under INVARIANTS, the freed slab
   chunk is poisoned with `0xdeadc0de`, so this UAF read panics immediately.
   Without INVARIANTS, it silently reads whatever now occupies that slab.

2. **Double-free in `txp_detach`.** At detach time (line 349-350):
   ```c
   for (i = 0; i < RXBUF_ENTRIES; i++)
       kfree(sc->sc_rxbufs[i].rb_sd, M_DEVBUF);
   ```
   The slot whose `rb_sd` was already freed in the error path is freed a
   second time → double-free. Under INVARIANTS this panics with a slab
   assertion (`chunk_mark_free` on an already-free chunk).

3. **Wild free in `txp_rxring_empty`.** The `ifconfig down` path calls
   `txp_rxring_empty` (if_txp.c:1076-1090) which frees `sd->sd_mbuf` for
   each slot. On the UAF slot, `sd` is a dangling pointer → wild free.

### Trigger conditions

The error path fires when `MGETHDR` or `MCLGET` fails under memory pressure
(mbuf exhaustion). On a system with 3Com Typhoon hardware, an attacker can
trigger this by flooding the interface to exhaust the mbuf pool, or under
natural memory pressure. The bug is in the GENERIC kernel (`device txp`),
but the code path is only reachable when a 3cR990 NIC is present.

## Reachability on this guest

- **txp driver**: compiled into X86_64_GENERIC kernel (`device txp`).
- **3Com hardware**: **absent**. `pciconf -l` shows only virtio and Intel
  440FX devices. No PCI vendor 0x10b7 device exists.
- **Result**: the driver never probes or attaches; no `txp_softc` exists;
  `txp_rxbuf_reclaim` is unreachable at runtime.

This is case **(d)** from the procedure: "genuinely not reachable on this
kernel" — the vulnerable code path is dead code at runtime because no
matching hardware is present. The bug is a real latent defect confirmed by
source tracing and demonstrated by a pattern-faithful harness.

## Harness evidence

The harness (`harness.c`) replicates the exact memory-management pattern of
`txp_rxbuf_reclaim` using userspace `malloc`/`free`:

- **Buggy version**: `kfree(sd)` on MGETHDR failure → `rb_sd` dangling,
  UAF read on next call, double-free in detach.
- **Fixed version**: no free of `sd`; NULL `sd_mbuf`; next call succeeds
  normally; detach is clean.

Output (from `run.log`):
```
--- Testing BUGGY txp_rxbuf_reclaim (if_txp.c:793-796) ---
  rb_sd[0] = 0x... (NOT NULLed — dangling pointer)
  sd = rb_sd[0] = 0x... => UAF READ: sd->sd_mbuf deref = 0x0
  detach: freeing rb_sd[0] — already freed in err_sd => DOUBLE-FREE
  => BUG CONFIRMED: dangling pointer + UAF read + double-free

--- Testing FIXED txp_rxbuf_reclaim ---
  rb_sd[0] = 0x... (still valid — NOT freed)
  rb_sd[0]->sd_mbuf = 0x0 (NULLed — safe)
  rc=0, rxbufprod now =1 (advanced)
  => FIX CONFIRMED: no double-free, no UAF
```

## Fix

`fix.diff` — two changes to the error path of `txp_rxbuf_reclaim`:

1. **Remove `kfree(sd, M_DEVBUF)`** at line 796. `sd` is a persistent
   per-slot allocation (allocated at `txp_alloc_rings:952` with `M_WAITOK`,
   only freed in `txp_detach:350`). Freeing it in the error path is the
   root cause of the UAF/double-free.

2. **Add `sd->sd_mbuf = NULL`** after `m_freem(sd->sd_mbuf)` in the
   `err_mbuf` path. When `MCLGET` fails, the mbuf header was allocated but
   the cluster attach failed; `m_freem` releases it, and NULLing
   `sd_mbuf` leaves the slot in a clean state so the next reclaim attempt
   can retry the allocation.

The `err_sd` label is kept (the `goto err_sd` at line 768 still references
it) but its body is now just `return;`.

### Fix validation

- **Diff applies**: `git apply --check` → clean (rc=0).
- **Compiles**: `make -j6 nativekernel KERNCONF=X86_64_GENERIC` → rc=0,
  0 errors. Full build log in `fix_build.log`.
- **Boots**: patched kernel (#1, Fri Jul 17 18:09:16 UTC 2026) boots and
  is stable. `kern.version` confirms the rebuild.
- **Runtime test**: `not_testable` — the vulnerable code path requires
  3Com Typhoon hardware not present on this guest. The fix is verified at
  the source level (no `kfree(sd)` in `txp_rxbuf_reclaim`, confirmed via
  `grep`) and structurally (the error path now NULLs `sd_mbuf` and returns
  without freeing the persistent allocation).

## Impact

On a system with 3Com 3cR990 hardware, an attacker who can cause mbuf
exhaustion (via network flood or local memory pressure) triggers a
kernel UAF read → double-free. Under INVARIANTS (default GENERIC): panic
(DoS). Without INVARIANTS: potential heap corruption exploitable for
privilege escalation, as the double-free gives a write primitive into
the `M_DEVBUF` slab.

**CWE-416** (Use After Free), **CWE-415** (Double Free).
**CVSS 3.1**: AV:A/AC:M/PR:N/UI:N/S:U/C:H/I:H/A:H (7.1 High with hardware).
