txp(4) walks device-fed ring cursors unvalidated: OOB descriptor walk via kernel pointer in DMA memory, wild rx_sd deref, non-terminating loops
Summary
Every ring cursor is a 32-bit word of the DMA-shared hostvar block consumed with no range/alignment validation: the rx walk indexes descriptors unbounded, dereferences rx_sd (a kernel pointer stored in DMA memory) from OOB descriptors, and loops while(roff!=woff) which never terminates for misaligned/off-ring woff; the response walk loops on a device-writable index that cycles 32 values forever if not one of the valid set. One DMA word from firmware = serializer-held livelock (hard hang); crafted cursors = wild-pointer deref and wild-mbuf free/copy. Sibling DFly drivers (fxp/bge/em/re) mask device-fed indices, txp does not anywhere. Hardware-gated. Validate-cursors fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4232 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | β | 3.4 KB | view raw | |
| build.log | β | 63 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| env.txt | β | 688 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| README.md | β | 2.3 KB | β raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-4232 β txp(4) walks device-fed ring cursors unvalidated (OOB descriptor walk, wild rx_sd deref, non-terminating loops)
File: sys/dev/netif/txp/if_txp.c (pass 2)
Severity: High (memcorrupt bucket) β hardware-gated (3c990 Typhoon)
Status: source-certain; runtime inconclusive (no 3c990 on the guest)
Build / run β same as DF-4231 (see that README); t4232.c prints the sink map.
The bug (two sites, one systemic cause)
All ring cursors live in the DMA-shared hostvar block (struct txp_hostvar,
if_txpreg.h:542-554) or in the DMA-shared descriptor rings; the driver
consumes them with no range or alignment validation:
-
txp_rx_reclaim()βif_txp.c:658-737.roff = *r->r_roff; woff = *r->r_woff;(device-fed), thenrxd = r->r_desc + (roff / sizeof(struct txp_rx_desc))andwhile (roff != woff)advancingroffby 16 with an exact-equality wrap atRX_ENTRIES * 16(2048). Aroff/woffoutside[0,2048)or not a multiple of 16 β (a)rxdstarts/continues out of bounds past thecontigmalloc'dsc_ldatablock, (b) the loop can never terminate (roffcycles values never equal towoff) β unbounded walk under the ifnet serializer = livelock, and (c)sd = rxd->rx_sd(if_txp.c:671) reads a kernel pointer stored in DMA memory from OOB memory, thenm = sd->sd_mbuf(if_txp.c:673) dereferences it and the wildmis fed tom_copydata/m_freem/if_input(if_txp.c:704-728). -
txp_response()βif_txp.c:1418-1453.while (ridx != hv->hv_resp_write_idx)with firmware-fedwidx;ridxonly takes values {0,16,β¦,496}, so awidxthat is not one of those makes the loop cycle forever (console spam + serializer-held livelock). Entryridx = hv->hv_resp_read_idx(if_txp.c:1393) is also device- writable shared memory:rsp = base + ridx(if_txp.c:1419) is an OOB descriptor read, andrsp->rsp_numdescfrom it sizes akmalloc(if_txp.c:1422) and a bcopy walk (txp_rsp_fixup,if_txp.c:1458-1481).
Siblings already accepted by this audit: DF-3761 (unbounded-walk family).
Not covered by DF-1461 (that is the txp_rxbuf_reclaim kfree error path).
Fix
fix.diff: validate/mask cursors to the ring byte size and 16-alignment
before walking; bail the descriptor instead of dereferencing garbage.
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 thetxp_rxhiringarray intotxp_rxloring/txp_cmdring/txp_rspring/β¦ and past the wholesc_ldatablock (next contigmalloc'd pages).roff/woffnot congruent mod 16, orwoff > 2048β the exact-equality wrap at :733 never producesroff == woffβ non-terminating walk β serializer-held livelock (system hang).struct txp_rx_desc'srx_sdis a host kernel pointer stored in device-shared memory (if_txpreg.h:268-271, union withu_int64_t) β by design the firmware round-trips it. An OOBrxdtherefore feeds a garbage pointer tosd->sd_mbuf; the resultingmis 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).ridxreaching this function is whatever the (shared-memory) read index says βrspcan be anywhere;rsp_numdesc(u_int8_t, β€ 255) from OOB memory then sizes a β€ 4 KB kmalloc and abcopywalk from OOB source. - Non-termination:
txp_rsp_fixupadvancesridxby 16 mod 512; if the device holdshv_resp_write_idxat, say, 8, noridxever equals it β the loop spins forever (withif_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.
Fix verification
not_testablefix.diff clamps-and-checks both cursor pairs and resets to 0 on violation (recovery equivalent to the driver's existing chip-reset paths). Not runtime-validable without the device.
fix.diff; run.log absence proof
Confirmed kernel references
- sys/dev/netif/txp/if_txp.c:658
- sys/dev/netif/txp/if_txp.c:660
- sys/dev/netif/txp/if_txp.c:671
- sys/dev/netif/txp/if_txp.c:673
- sys/dev/netif/txp/if_txp.c:733
- sys/dev/netif/txp/if_txp.c:1393
- sys/dev/netif/txp/if_txp.c:1418
- sys/dev/netif/txp/if_txp.c:1419
- sys/dev/netif/txp/if_txp.c:1422
- sys/dev/netif/txp/if_txpreg.h:522
- sys/dev/netif/txp/if_txpreg.h:525
- sys/dev/netif/txp/if_txpreg.h:542
Detail
Exploit chain
Malicious/faulty firmware (or passthrough device): one DMA word aimed at hv_resp_write_idx = hard livelock DoS; crafted roff/rx_sd aims a wild deref and a wild-mbuf free/copy -- arbitrary-deref primitive leading to kernel R/W for the device owner.
Evidence (decisive lines)
run.log (driver in kernel + absence proof), VERDICT.md (both walks traced path:line), trigger.c, fix.diff
PoC changes
No seed (pass-2 new); annotated precondition trigger per DF-3604/DF-3761 convention.
Verified recommended fix
Validate ring cursors (16-byte alignment + ring byte-size bound) at both walk entry points before computing any descriptor pointer; see fix.diff.
Verdict
Source-level certain: every ring cursor txp(4) walks is a 32-bit word in the DMA-shared hostvar block (if_txpreg.h:542-554) or the DMA-shared descriptor rings, consumed with zero validation. (1) txp_rx_reclaim (if_txp.c:658-737): roff/woff device-fed, rxd = r_desc + roff/16 unbounded; roff outside [0,2048) or misaligned walks descriptors OOB past the contigmalloc'd ldata block while roff!=woff may never terminate (serializer-held livelock); rx_sd is a kernel pointer stored in DMA memory (if_txpreg.h:268-271) read at :671 and dereferenced at :673 -- wild mbuf then lengthened (:676), copied from (:704), freed. (2) txp_response (if_txp.c:1418-1453): ridx from device-writable hv_resp_read_idx gives rsp = base + ridx OOB read; hv_resp_write_idx not in {0,16..496} makes the walk cycle the 32-entry ring forever; rsp_numdesc from OOB memory sizes a kmalloc (:1422) and bcopy walk. Sibling drivers (fxp/bge/em/re) mask their device-fed indices; txp does not. Runtime inconclusive: driver in kernel, NO_3COM_PCI_DEVICE on the guest (run.log).
No comments yet.