# DF-4232 VERDICT — unvalidated device-fed ring cursors in txp_rx_reclaim()/txp_response()

**Verdict: source-certain (High, memcorrupt/unbounded-walk); runtime
inconclusive — no 3c990 device on the audit guest (proof in run.log), guest
source byte-identical.**

## Walk 1: receive rings — if_txp.c:649-744

`txp_intr()` (`if_txp.c:624-627`) calls `txp_rx_reclaim()` whenever the
device-fed `*r->r_woff != *r->r_roff`. Both are `volatile u_int32_t` words of
`struct txp_hostvar` (`if_txpreg.h:544-554`) inside the DMA-coherent
`sc_ldata` allocation — the Typhoon firmware writes them directly.

```
if_txp.c:658   roff = *r->r_roff;            // device-fed, unvalidated
if_txp.c:659   woff = *r->r_woff;            // device-fed, unvalidated
if_txp.c:660   rxd = r->r_desc + (roff / sizeof(struct txp_rx_desc));
if_txp.c:662   while (roff != woff) {
if_txp.c:671       sd = rxd->rx_sd;          // kernel ptr read from DMA/OOB mem
if_txp.c:673       m = sd->sd_mbuf;          // wild deref
if_txp.c:676       m->m_pkthdr.len = m->m_len = rxd->rx_len;
if_txp.c:704       m_copydata(m, 0, ..., mtod(mnew, void *));
if_txp.c:732       roff += sizeof(struct txp_rx_desc);
if_txp.c:733       if (roff == (RX_ENTRIES * sizeof(struct txp_rx_desc))) { roff=0; ... }
```

- Ring is `RX_ENTRIES=128` × 16-byte descriptors = 2048 bytes (`if_txpreg.h:522`).
- `roff = 0x7fffffff` ⇒ `rxd = r_desc + 134217182` — immediately wild; even
  modest lies (e.g. `roff = 2048`, `woff = 0`) walk off the end of the
  `txp_rxhiring` array into `txp_rxloring`/`txp_cmdring`/`txp_rspring`/…
  and past the whole `sc_ldata` block (next contigmalloc'd pages).
- `roff`/`woff` not congruent mod 16, or `woff > 2048` ⇒ the exact-equality
  wrap at :733 never produces `roff == woff` ⇒ non-terminating walk ⇒
  serializer-held livelock (system hang).
- `struct txp_rx_desc`'s `rx_sd` is a **host kernel pointer stored in
  device-shared memory** (`if_txpreg.h:268-271`, union with `u_int64_t`) —
  by design the firmware round-trips it. An OOB `rxd` therefore feeds a
  garbage pointer to `sd->sd_mbuf`; the resulting `m` is lengthened
  (`:676`), copied from (`:704`), and freed — arbitrary-deref / arbitrary-
  read-ish primitive, plus NULL-word crashes, entirely device-chosen.

## Walk 2: response ring — if_txp.c:1392-1453

`txp_command2()` polls `hv->hv_resp_read_idx` vs `hv->hv_resp_write_idx`
(`if_txp.c:1393-1394`, both device-writable) and calls `txp_response(sc,
idx, ...)` with the unvalidated `ridx`:

```
if_txp.c:1418  while (ridx != hv->hv_resp_write_idx) {   // firmware-fed widx
if_txp.c:1419      rsp = (struct txp_rsp_desc *)(((u_int8_t *)sc->sc_rspring.base) + ridx);
if_txp.c:1421      if (id == rsp->rsp_id && rsp->rsp_seq == seq) {
if_txp.c:1422-24       *rspp = kmalloc(sizeof(rsp_desc) * (rsp->rsp_numdesc + 1), ...);
if_txp.c:1427          txp_rsp_fixup(sc, rsp, *rspp);     // bcopy walk
```

- Response ring is `RSP_ENTRIES=32` × 16 = 512 bytes (`if_txpreg.h:525`).
  `ridx` reaching this function is whatever the (shared-memory) read index
  says — `rsp` can be anywhere; `rsp_numdesc` (u_int8_t, ≤ 255) from OOB
  memory then sizes a ≤ 4 KB kmalloc and a `bcopy` walk from OOB source.
- Non-termination: `txp_rsp_fixup` advances `ridx` by 16 mod 512; if the
  device holds `hv_resp_write_idx` at, say, 8, no `ridx` ever equals it —
  the loop spins forever (with `if_printf("unknown id")` console flood) under
  the serializer: hard DoS from a single DMA word.

Contrast with sibling DFly NIC drivers (fxp, bge, em, re): all mask their
device-fed completion indices (`idx &= (count-1)` or explicit bounds check)
before use. txp does not, anywhere.

## Exploitability

Malicious/faulty firmware or a passthrough device: pick `roff`/`rx_sd`/`ridx`
to aim the wild deref; livelock needs one word. Same hardware-gated threat
model as DF-4231/DF-3761 (see DF-3604's precedent discussion).

## Runtime evidence (inconclusive)

run.log: driver in kernel (`161 pci/if_txp`), `NO_3COM_PCI_DEVICE`,
`md5 5757b04710b9a2f3b6bbd17196d6df81` == audit tree. The walk cannot be
staged without a Typhoon.

## Fix

fix.diff: clamp-and-check cursors (size + 16-alignment + ring-bound) at both
walk entry points; on violation, reset the cursor to the last known-good
value and drop the batch (recovery identical to a chip reset, which the
driver already does for `TXP_INT_SELF` etc.).

## 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.
