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)
PoC verification
Evidence pack
findings/poc/DF-4231 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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;rxdlives in the DMA-shared receive ring (sc_ldata),rx_lenisvolatile u_int16_t(if_txpreg.h:263) β fully firmware-controlled, never checked againstTXP_MAX_PKTLEN(2048) orMCLBYTES.sys/dev/netif/txp/if_txp.c:688-704(__STRICT_ALIGNMENTis defined atif_txp.c:85, so always compiled): destinationmnewgets at most one 2048-byte cluster, thenm_copydata(m, 0, m->m_pkthdr.len, mtod(mnew, void *))copiesrx_lenbytes from the (2048-byte) source cluster into it.sys/kern/uipc_mbuf.c:1671βm_copydata()trustsm->m_len(which the driver just stamped withrx_len);count = min(m->m_len - off, len)=rx_lenin onebcopy.
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.
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)
-
Firmware caps are advisory only.
txp_attach()/txp_init()tell the firmwareTXP_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. -
The receive descriptors live in the DMA-coherent
sc_ldatablock (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 readsrxd->rx_lenstraight from it. -
txp_rx_reclaim()(if_txp.c:649-744), called fromtxp_intr()(if_txp.c:624-627) whenever the device-fedhv_rx_*_write_idxdiffers from the read index: -if_txp.c:676m->m_pkthdr.len = m->m_len = rxd->rx_len;β the receive cluster (oneMCLGET= 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 atif_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`). -
m_copydata()(sys/kern/uipc_mbuf.c:1671-1696) doescount = min(m->m_len - off, len); bcopy(mtod(m) + off, cp, count);β withm->m_len == len == rx_lenthis is a singlebcopyofrx_lenbytes: read from the 2048-byte source cluster, write into the 2048-byte destination cluster. NoKASSERTfires (the chain legitimately has one mbuf whosem_lenthe 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_testablefix.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
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.
No comments yet.