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

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)

No comments yet.

PoC verification

Evidence pack

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

  1. txp_stop() at :1110 β†’ txp_rxring_empty() (:1522 β†’ :1073-1094) frees all 256 receive clusters and NULLs sd->sd_mbuf, but the DMA-shared rb_paddrlo ring entries keep pointing at the freed clusters, and hv_rx_buf_write_idx keeps its stale value ("buffers available").
  2. txp_rxring_fill(sc) at :1127 β€” return value ignored. On the first MGETHDR/MCLGET(M_NOWAIT) failure it returns ENOBUFS (:1051, :1056) after refilling only a prefix; hv_rx_buf_write_idx is updated only on success (:1066-1067), so the firmware still believes the old (stale) buffer count.
  3. TXP_CMD_TX_ENABLE/TXP_CMD_RX_ENABLE are issued anyway (:1129-1130) and the interface is marked IFF_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.

VERDICT.md
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

fix.diff is a 7-line guarded early-return; not runtime-validable without the device.

fix.diff; run.log absence proof
↓ fix.diffper-fix-DF-4233

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