txp_init() ignores txp_rxring_fill() failure and enables RX over a stale buffer ring: firmware DMA into freed clusters + NULL deref
Summary
A prior txp_stop freed all 256 rx clusters without clearing the DMA-shared ring or write index; when txp_rxring_fill fails mid-fill (MCLGET M_NOWAIT failure = remotely-seedable memory pressure), txp_init ignores the return and issues RX_ENABLE anyway - the firmware consumes stale entries whose paddrs point at freed clusters. Inbound frame DMA'd into a freed 2KB cluster with attacker-supplied content (heap UAF write, groomable) followed by NULL-deref panic. Abort-on-fill-failure fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4233 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | β | 2.5 KB | view raw | |
| build.log | β | 63 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| env.txt | β | 688 B | view raw | |
| fix.diff | β | 598 B | view raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| README.md | β | 1.8 KB | β raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4233 β txp_init() ignores txp_rxring_fill() failure; RX enabled over a stale buffer ring β firmware DMA into freed clusters + NULL deref
File: sys/dev/netif/txp/if_txp.c (pass 2)
Severity: High (memcorrupt bucket) β hardware-gated; precondition is a
transient MCLGET(M_NOWAIT) failure (kernel memory pressure) during
ifconfig txp0 up, then any inbound frame.
Build / run β same guest probe as DF-4231 (t4233.c).
The bug
txp_init() (if_txp.c:1097-1144):
txp_stop()at:1110βtxp_rxring_empty()(:1522β:1073-1094) frees all 256 receive clusters and NULLssd->sd_mbuf, but the DMA-sharedrb_paddrloring entries keep pointing at the freed clusters, andhv_rx_buf_write_idxkeeps its stale value ("buffers available").txp_rxring_fill(sc)at:1127β return value ignored. On the firstMGETHDR/MCLGET(M_NOWAIT)failure it returnsENOBUFS(:1051,:1056) after refilling only a prefix;hv_rx_buf_write_idxis updated only on success (:1066-1067), so the firmware still believes the old (stale) buffer count.TXP_CMD_TX_ENABLE/TXP_CMD_RX_ENABLEare issued anyway (:1129-1130) and the interface is markedIFF_RUNNING.
When a frame then arrives for a stale ring entry:
- the firmware DMAs the (attacker-supplied) frame into the freed mbuf
cluster listed in rb_paddrlo β use-after-free write with attacker
content into the mbuf cluster zone;
- on completion, txp_rx_reclaim() does m = sd->sd_mbuf (NULL for the
unfilled prefix) then m->m_pkthdr.len = ... at :676 β NULL write /
panic.
Fix
fix.diff: propagate the fill failure β bail out of txp_init() (leave the
interface down) instead of enabling RX over a half-filled, stale ring.
DF-4233 VERDICT β txp_init() ignores txp_rxring_fill() failure
Verdict: source-certain ignore + stale-ring mechanics (High, memcorrupt); the firmware-side half (whether the 3XP honors the stale hv_rx_buf_write_idx) is likely-but-not-certain, so overall confidence "likely". Runtime inconclusive: no 3c990 on the audit guest (run.log).
Trace
txp_init() if_txp.c:1097
:1107 if (ifp->if_flags & IFF_RUNNING) return; // no double init
:1110 txp_stop(sc);
ββ txp_rxring_empty(sc) if_txp.c:1522 β :1081-1090
for each of 256 slots: m_freem(sd->sd_mbuf); sd->sd_mbuf = NULL;
// rb_paddrlo NOT cleared; hv_rx_buf_write_idx NOT reset
:1112 MAX_PKT_SIZE cmd
:1122 STATION_ADDRESS cmd
:1125 txp_set_filter()
:1127 txp_rxring_fill(sc); // <-- return value IGNORED
ββ :1047-1064 for (i = 0; i < RXBUF_ENTRIES; i++) {
MGETHDR/MCLGET(M_NOWAIT); on failure return(ENOBUFS); }
:1066-1067 hv_rx_buf_write_idx = (RXBUF_ENTRIES-1)*16; // success only
:1129 TXP_CMD_TX_ENABLE
:1130 TXP_CMD_RX_ENABLE // <-- RX starts over a half-filled ring
:1139 ifp->if_flags |= IFF_RUNNING;
State after a failed re-init: prefix of the buffer ring holds fresh
clusters; the rest holds rb_paddrlo of freed clusters and
sd->sd_mbuf == NULL; the firmware's view of available buffers
(hv_rx_buf_write_idx, a hostvar word β if_txpreg.h:546) still says
"up to the old producer".
Inbound frame on a stale slot:
1. firmware DMAs frame content into the freed cluster (heap UAF write,
attacker-controlled content, mbuf cluster zone);
2. firmware posts the rx descriptor (round-tripping the slot's rb_sd
pointer, see DF-4232 analysis);
3. txp_rx_reclaim() if_txp.c:671-676: sd = rxd->rx_sd; m = sd->sd_mbuf;
β m == NULL β m->m_pkthdr.len = m->m_len = rxd->rx_len; writes
through NULL β immediate kernel panic.
So even in the least favorable firmware interpretation, the NULL deref at :676 fires the moment a completion arrives for an unfilled slot; in the natural interpretation (firmware trusts hostvar, which is the whole design of the hostvar block) the DMA-into-freed-cluster happens first.
Precondition realism
MCLGET(M_NOWAIT) failure = transient cluster exhaustion. A remote peer
sustaining an inbound flood while a local (or scripted) ifconfig txp0 up
runs is the realistic recipe; DFly clusters come from a limited zone, and
memory pressure is externally seedable. First-up after attach can also
fail mid-fill under pressure (same consequence, stale = zero paddrs β
firmware DMA to physical 0 β see notes in the finding).
Why inconclusive at runtime
Same as DF-4231/4232: no Typhoon device on the guest (run.log:
NO_3COM_PCI_DEVICE, driver present in kernel, source md5 identical).
The MGET failure precondition could be staged in the guest, but with no
device to consume the ring there is nothing to observe.
Fix
fix.diff: if (txp_rxring_fill(sc) != 0) { /* leave interface down */ ... return; }
mirroring how every other DFly NIC driver aborts init when its rx ring
cannot be filled (cf. re(4) re_init β re_rx_list_init error paths).
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 is a 7-line guarded early-return; not runtime-validable without the device.
fix.diff; run.log absence proof
Confirmed kernel references
Detail
Exploit chain
memory pressure during ifconfig txp0 up (or a peer flooding while a re-init runs) -> half-filled ring -> inbound frame DMA'd into a freed 2KB cluster with attacker-controlled bytes -> mbuf-cluster-zone UAF write -> groom victim object adjacent in the cluster zone -> kernel R/W; independently, the NULL-deref at :676 panics the kernel.
Evidence (decisive lines)
run.log (driver in kernel, NO_3COM_PCI_DEVICE, md5 parity), VERDICT.md (init/stop/fill traced path:line incl. the success-only write_idx at :1066), trigger.c, fix.diff
PoC changes
No seed (pass-2 new); annotated precondition probe.
Verified recommended fix
Abort txp_init() when txp_rxring_fill() fails (leave the interface down), mirroring every other DFly NIC driver's init error path; see fix.diff.
Verdict
Source-certain control-flow bug with a likely-corrupting consequence: txp_init() ignores txp_rxring_fill()'s return (if_txp.c:1127) and enables TX/RX anyway (:1129-1130). A prior txp_stop freed all 256 rx clusters (txp_rxring_empty, :1081-1090) without clearing the DMA-shared rb_paddrlo ring or hv_rx_buf_write_idx; a mid-fill MCLGET(M_NOWAIT) failure (memory pressure, remotely seedable) leaves a stale prefix that the firmware still believes is available: inbound frames are DMA'd into freed mbuf clusters (heap use-after-free WRITE with attacker-supplied frame content), and the completion descriptor hits m = sd->sd_mbuf == NULL -> NULL write at :676 (panic). Confidence 'likely' only because the firmware's exact stale-write_idx behavior is inferred from the hostvar design, not observed. Runtime inconclusive: no 3c990 on the guest (run.log).
No comments yet.