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

txp(4) trusts NIC-fed rxd->rx_len: unbounded m_copydata heap OOB read+write past mbuf clusters

Summary

txp_rx_reclaim stamps the firmware-fed 16-bit rxd->rx_len (volatile field of the DMA-shared rx descriptor) into the receive cluster mbuf with no bound, then the always-compiled __STRICT_ALIGNMENT path copies rx_len bytes between two MCLBYTES clusters in one m_copydata bcopy. rx_len>2048 reads OOB from the source cluster and writes up to 63531 bytes past the destination cluster - kernel heap corruption with heap-sourced contents (slab-groomable to uid=0 for the device owner). Driver only advises the cap via TXP_CMD_MAX_PKT_SIZE_WRITE, never verifies it. Hardware-gated. Drop-out-of-range fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4231 Β· 8 files
FileTypeDescriptionSize
trigger.c β€” 2.7 KB view raw
build.log β€” 63 B view raw
run.log β€” 1.2 KB view raw
env.txt β€” 688 B view raw
fix.diff β€” 483 B view raw
VERDICT.md β€” 4.3 KB ↓ raw
README.md β€” 2.3 KB ↓ raw
verdict.json β€” 3.5 KB view raw

DF-4231 β€” txp(4) trusts NIC-fed rxd->rx_len β†’ unbounded m_copydata (heap OOB read+write)

File: sys/dev/netif/txp/if_txp.c (pass 2, GLM 5.3) Severity: High (memcorrupt bucket) β€” hardware-gated (3Com 3c990 Typhoon) Status: source-certain; runtime inconclusive on the audit guest (no 3c990 device, QEMU has no Typhoon model)

Build / run

scp trigger.c guest:/tmp/t4231.c
ssh guest-root 'cd /tmp && cc -o trig_4231 t4231.c && ./trig_4231 && kldstat -v | grep if_txp && (pciconf -l | grep -i 10b7 || echo NO_3COM_PCI_DEVICE)'

trigger.c is an annotated hardware-precondition probe (per DF-3604 convention): it prints the exact sink lines and proves the driver is in the running kernel while no 3Com device exists, i.e. the path cannot be exercised on this guest.

Expected output

DF-4231: txp_rx_reclaim() trusts NIC-fed rxd->rx_len
  ...
===DRIVER_IN_KERNEL===
        161 pci/if_txp
===THREECOM_DEVICE_PROBE===
NO_3COM_PCI_DEVICE

The bug

  • sys/dev/netif/txp/if_txp.c:676 β€” m->m_pkthdr.len = m->m_len = rxd->rx_len; rxd lives in the DMA-shared receive ring (sc_ldata), rx_len is volatile u_int16_t (if_txpreg.h:263) β†’ fully firmware-controlled, never checked against TXP_MAX_PKTLEN (2048) or MCLBYTES.
  • sys/dev/netif/txp/if_txp.c:688-704 (__STRICT_ALIGNMENT is defined at if_txp.c:85, so always compiled): destination mnew gets at most one 2048-byte cluster, then m_copydata(m, 0, m->m_pkthdr.len, mtod(mnew, void *)) copies rx_len bytes from the (2048-byte) source cluster into it.
  • sys/kern/uipc_mbuf.c:1671 β€” m_copydata() trusts m->m_len (which the driver just stamped with rx_len); count = min(m->m_len - off, len) = rx_len in one bcopy.

Result for rx_len > 2048: OOB read of up to 63531 bytes past the source cluster and an equal-size OOB write past the destination cluster β€” kernel heap corruption with heap-sourced contents, from a malicious/faulty Typhoon firmware. With the copy path compiled out the same inflated m_len would instead hand the network stack an over-long mbuf (heap info leak); here the write variant is the live one.

Fix

fix.diff (this pack) drops any frame whose length exceeds MCLBYTES before the stamp/copy, using the file's own error idiom.

VERDICT.md
↓ download raw

DF-4231 VERDICT β€” txp_rx_reclaim() rx_len trust β†’ m_copydata OOB read+write

Verdict: source-certain (High, memcorrupt); runtime inconclusive β€” the audit guest has no 3Com 3c990 (Typhoon) device and QEMU has no Typhoon model, so the DMA-fed precondition cannot be staged. Driver verified present in the running kernel; guest source byte-identical to the audited tree.

Root cause chain (path:line)

  1. Firmware caps are advisory only. txp_attach()/txp_init() tell the firmware TXP_CMD_MAX_PKT_SIZE_WRITE, TXP_MAX_PKTLEN (if_txp.c:263-267, if_txp.c:1112-1113) β€” TXP_MAX_PKTLEN = 0x0800 = 2048 (if_txpreg.h:631) β€” but nothing on the receive path ever verifies a length.

  2. The receive descriptors live in the DMA-coherent sc_ldata block (if_txp.c:255-256, contigmalloc), shared with the device: struct txp_rx_desc { ... volatile u_int16_t rx_len; ... } β€” if_txpreg.h:260-275. The driver reads rxd->rx_len straight from it.

  3. txp_rx_reclaim() (if_txp.c:649-744), called from txp_intr() (if_txp.c:624-627) whenever the device-fed hv_rx_*_write_idx differs from the read index: - if_txp.c:676 m->m_pkthdr.len = m->m_len = rxd->rx_len; β€” the receive cluster (one MCLGET = 2048 bytes, if_txp.c:770-774) is re-labeled to the firmware's length. - if_txp.c:688-704 (inside #ifdef __STRICT_ALIGNMENT, unconditionally defined at if_txp.c:85): a replacementmnewheader mbuf is allocated; it gets a cluster onlyif (m->m_len > (MHLEN - 2))(if_txp.c:693-700) β€” a singleMCLGET, still 2048 bytes;m_copydata(m, 0, m->m_pkthdr.len, mtod(mnew, void *))(if_txp.c:704`).

  4. m_copydata() (sys/kern/uipc_mbuf.c:1671-1696) does count = min(m->m_len - off, len); bcopy(mtod(m) + off, cp, count); β€” with m->m_len == len == rx_len this is a single bcopy of rx_len bytes: read from the 2048-byte source cluster, write into the 2048-byte destination cluster. No KASSERT fires (the chain legitimately has one mbuf whose m_len the driver itself inflated).

For rx_len = 65535 the write runs 63531 bytes past the destination cluster. Both clusters come from the mbuf cluster zone, so the overrun smashes adjacent 2KB cluster slabs with content sourced from kernel heap β€” with slab grooming, a corrupted object of the attacker's (firmware's) choosing. This is the same defect class accepted in this audit as DF-3604 (bce), DF-1478 (my), DF-1481 (vr), DF-1490 (tx), DF-1514 (ste), DF-1519 (lge), DF-1562 (pcn), DF-1351 (nfe) β€” txp was simply missing the clamp.

Pass-1 overlap check: the only prior txp finding is DF-1461 (kfree UAF in txp_rxbuf_reclaim's error path) β€” different function, different bug; the DB was queried, no rx_len finding exists for this file.

Exploit chain (on real hardware)

device/firmware writes rx_desc{rx_flags=RX_FLAGS_DATA, rx_len=65535} β†’ intr β†’ txp_rx_reclaim β†’ 63KB bcopy past mnew's cluster β†’ adjacent mbuf-cluster-zone objects overwritten with heap content β†’ e.g. corrupt an in-flight mbuf's m_data/m_ext of a victim socket, or a neighbouring kernel object, β†’ uid=0 equivalent for the device's owner. For a malfunctioning (not malicious) firmware the same code is a kernel-crash generator. (A PCIe device can of course DMA anywhere; the finding's value is (a) faulty-firmware robustness, (b) virtualization passthrough where the "device" is a guest, (c) firmware compromise.)

Why inconclusive at runtime (evidence)

run.log:

===DRIVER_IN_KERNEL===
        161 pci/if_txp
===THREECOM_DEVICE_PROBE===
NO_3COM_PCI_DEVICE

Guest /usr/src/sys/dev/netif/txp/if_txp.c md5 5757b04710b9a2f3b6bbd17196d6df81 == audit tree. No QEMU Typhoon machine model exists to emulate the DMA side.

Fix validation

fix.diff clamps with the file's own error idiom (ierrors++ + skip, cf. if_txp.c:664-668). Not runtime-validable for the same hardware-absence reason (fix_status = not_testable).

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 uses the file's own error idiom (ierrors + goto next, cf. if_txp.c:664-668) so the buffer slot keeps its cluster for the next refill. Not runtime-validable: no 3c990 on the guest, same reason the PoC is inconclusive.

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

Confirmed kernel references

Detail

Exploit chain

On real/passthrough hardware: firmware writes rx_desc{flags=DATA, rx_len=65535} -> intr -> txp_rx_reclaim -> 63KB bcopy past mnew's cluster -> adjacent mbuf-cluster-zone objects overwritten with heap-sourced content -> groom a victim cluster-adjacent object (e.g. in-flight mbuf m_data/m_ext) -> kernel R/W -> uid=0 for the device owner; faulty-firmware case = crash generator. Same class as audit-accepted DF-3604 (bce) family.

Evidence (decisive lines)

run.log (driver-in-kernel + NO_3COM_PCI_DEVICE + md5 parity), VERDICT.md (path:line trace + m_copydata mechanics), trigger.c (annotated precondition probe), fix.diff

PoC changes

No seed existed (pass-2 new finding); trigger authored as an annotated hardware-precondition probe per the DF-3604 convention since no syscall path can forge DMA state.

Verified recommended fix

In txp_rx_reclaim, drop frames with rx_len outside [ETHER_MIN_LEN, MCLBYTES] before stamping m_len (see fix.diff).

Verdict

Source-level certain: txp_rx_reclaim() (sys/dev/netif/txp/if_txp.c:676) stamps the firmware-fed 16-bit rxd->rx_len (volatile, inside the DMA-shared rx ring, if_txpreg.h:263) into the receive cluster mbuf with no bound, then m_copydata(m, 0, rx_len, mtod(mnew)) at :704 (__STRICT_ALIGNMENT is always defined, if_txp.c:85) copies rx_len bytes between two 2048-byte clusters in one bcopy (m_copydata trusts m->m_len, uipc_mbuf.c:1671/1690) -- for rx_len>2048 both an OOB heap read of the source cluster and an equal OOB heap write past the destination cluster, up to 63531 bytes, contents = adjacent cluster-zone heap. No KASSERT fires (driver itself stamped m_len). Runtime honestly inconclusive: driver verified present in the running kernel (kldstat -v: 161 pci/if_txp) but the guest has no 3Com 3c990 (pciconf: NO_3COM_PCI_DEVICE) and QEMU has no Typhoon model, so the DMA-fed precondition cannot be staged.