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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4232 Β· 8 files
FileTypeDescriptionSize
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:

  1. txp_rx_reclaim() β€” if_txp.c:658-737. roff = *r->r_roff; woff = *r->r_woff; (device-fed), then rxd = r->r_desc + (roff / sizeof(struct txp_rx_desc)) and while (roff != woff) advancing roff by 16 with an exact-equality wrap at RX_ENTRIES * 16 (2048). A roff/woff outside [0,2048) or not a multiple of 16 β‡’ (a) rxd starts/continues out of bounds past the contigmalloc'd sc_ldata block, (b) the loop can never terminate (roff cycles values never equal to woff) β€” 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, then m = sd->sd_mbuf (if_txp.c:673) dereferences it and the wild m is fed to m_copydata/m_freem/if_input (if_txp.c:704-728).

  2. txp_response() β€” if_txp.c:1418-1453. while (ridx != hv->hv_resp_write_idx) with firmware-fed widx; ridx only takes values {0,16,…,496}, so a widx that is not one of those makes the loop cycle forever (console spam + serializer-held livelock). Entry ridx = 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, and rsp->rsp_numdesc from it sizes a kmalloc (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.

VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4232

Confirmed kernel references

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