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

tl_intvec_rxeof trusts device-written 16-bit tlist_frsize: unbounded mbuf length on an MCLBYTES cluster (OOB read propagated up the network stack)

Summary

total_len comes from the device-DMA-written tlist_frsize u16 consumed with no clamp: m_len = total_len on an m_getcl MCLBYTES cluster. A faulty or hostile NIC setting FRAMECMP with frsize up to 65535 hands ether_input a ~63KB over-long mbuf on a 2KB buffer - OOB read whose contents are copied to userland via sockets/BPF (kernel heap info leak) or page-fault panic. TL_MAXRX is only a register hint to a compliant chip. Clamp-and-recycle fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4110 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
presence_probe.sh β€” 521 B view raw
env.txt β€” 347 B view raw
fix.diff β€” 820 B view raw
manifest.json β€” 780 B view raw
verdict.json β€” 2.7 KB view raw

DF-4110 β€” tl(4): tl_intvec_rxeof trusts device-written tlist_frsize β€” unbounded mbuf length on an MCLBYTES cluster

What this is

tl_intvec_rxeof() (sys/dev/netif/tl/if_tl.c:1386-1441) reads the received frame length straight out of the DMA-visible RX list header:

total_len = cur_rx->tl_ptr->tlist_frsize;   /* if_tl.c:1405, u_int16_t, chip-written */
...
m->m_pkthdr.len = m->m_len = total_len;     /* if_tl.c:1422 */
ifp->if_input(ifp, m, NULL, -1);            /* if_tl.c:1437 */

The RX buffer is a single m_getcl() cluster β€” MCLBYTES = 2048 bytes (if_tl.c:1347, tl_newbuf). The only bound the driver programs is the TL_MAXRX register (if_tl.c:1941, tl_dio_write16(sc, TL_MAXRX, MCLBYTES)) β€” a hint to a correctly-functioning chip, not a kernel-side check. Nothing clamps total_len against MCLBYTES before it becomes the mbuf length, so a device that writes a tlist_frsize of up to 65535 with TL_CSTAT_FRAMECMP set hands the network stack an m_len/m_pkthdr.len of up to 65535 on a 2048-byte allocation. ether_input and everything above it (VLAN unwrap, IP demux, BPF taps, socket copies) walks that mbuf linearly β†’ up to ~63 KB out-of-bounds read whose contents are propagated to userland (kernel heap info leak) or crash the kernel on unmapped pages.

Sibling of the well-known device-length-trust family (the reason many BSD drivers gained if (total_len > MCLBYTES) drop guards).

Threat model

Requires a malicious, compromised, or malfunctioning ThunderLAN NIC (or any PCIe device able to DMA into the RX lists β€” e.g. faulty DMA engine writing stale/garbage descriptor state). Not triggerable by an unprivileged local user on a healthy NIC; a remote peer cannot exceed TL_MAXRX on a compliant chip. Genuinely hardware-gated, hence severity Medium rather than High.

PoC status β€” untested (hardware absent)

The audit guest has vtnet0 lo0 only β€” no ThunderLAN PCI device (presence_probe.sh). The OOB read cannot be entered on the guest, so no PoC run was attempted; the finding is certain at source level.

Fix

Clamp tlist_frsize to MCLBYTES and recycle the descriptor (same shape as the existing ENOBUFS path) β€” see fix.diff.

VERDICT.md
↓ download raw

DF-4110 β€” VERDICT

Finding: tl_intvec_rxeof() uses the device-written 16-bit tlist_frsize as the mbuf length without clamping it to the MCLBYTES cluster backing the RX buffer (sys/dev/netif/tl/if_tl.c:1405 β†’ 1422), with if_input at :1437 propagating the over-long mbuf up the stack.

Status: untested. The audit guest has no ThunderLAN device (ifconfig -l β†’ vtnet0 lo0; no 0x104C/0x0E11 ThunderLAN PCI IDs β€” see presence_probe.sh), so the corrupted-descriptor state this bug consumes cannot be produced on the guest. No PoC run attempted; not a false positive β€” the missing clamp is directly visible in the source and the sink (m->m_pkthdr.len = m->m_len = total_len on a 2 KB cluster) is unconditional on every received frame.

Source-level trace (proof): 1. tl_newbuf allocates the RX mbuf with m_getcl() β€” MCLBYTES = 2048 (if_tl.c:1347; buffer also what tl_dio_write16(sc, TL_MAXRX, MCLBYTES) at if_tl.c:1941 advertises to the chip). 2. The ThunderLAN chip DMA-writes tlist_cstat/tlist_frsize into the RX list header (struct tl_list_onefrag, if_tlreg.h:75-80) β€” both are device-controlled memory. 3. On RXEOF the driver checks only TL_CSTAT_FRAMECMP (if_tl.c:1400), takes total_len = cur_rx->tl_ptr->tlist_frsize (:1405) β€” a u_int16_t the kernel never bounds β€” and assigns it to m->m_pkthdr.len = m->m_len (:1422). 4. ifp->if_input (:1437) β†’ ether_input walks the mbuf for m_pkthdr.len bytes: for any device-fed value > 2048 this reads past the 2 KB cluster (up to ~63 KB OOB), copying the bytes into the network stack / sockets / BPF (kernel heap disclosure) or faulting on an unmapped page (panic).

Impact ceiling: kernel heap info leak via any socket that receives the over-long frame + kernel panic; read primitive only, no write.

Fix: clamp total_len > MCLBYTES β†’ count ierror and recycle the descriptor in place (fix.diff, mirrors the existing ENOBUFS recycling at if_tl.c:1407-1413). Cannot be validated on this guest (no hardware) β†’ fix_status not_tested.

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 authored against the read-only sys/ tree (patch --dry-run clean); cannot be validated on a guest without ThunderLAN hardware.

findings/poc/DF-4110/fix.diff
↓ fix.diffper-fix-DF-4110

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4110/VERDICT.md (source trace if_tl.c:1347/1400/1405/1422/1437/1941); presence_probe.sh + env.txt (hardware absent on guest); fix.diff (MCLBYTES clamp + in-place recycle)

PoC changes

No seed PoC. presence_probe.sh documents the hardware gate; the OOB read cannot be entered on a guest without a ThunderLAN NIC (or a way to inject device-side DMA state).

Verified recommended fix

Clamp total_len to MCLBYTES in tl_intvec_rxeof and recycle the descriptor in place (ENOBUFS-path shape) when exceeded.

Verdict

Source-certain, hardware-gated. tl_intvec_rxeof takes the received frame length from the device-DMA-written 16-bit tlist_frsize (sys/dev/netif/tl/if_tl.c:1405) and assigns it unconditionally as m->m_pkthdr.len = m->m_len (if_tl.c:1422) on an m_getcl() MCLBYTES=2048 cluster (if_tl.c:1347). No kernel-side clamp exists; TL_MAXRX=MCLBYTES (if_tl.c:1941) is only a register hint to a compliant chip. A faulty or hostile NIC that sets TL_CSTAT_FRAMECMP with tlist_frsize up to 65535 hands ether_input (if_tl.c:1437) an over-long mbuf: up to ~63KB out-of-bounds read past the cluster, contents propagated up the stack (kernel heap info leak via sockets/BPF) or page-fault panic. Guest has no ThunderLAN device (vtnet0 lo0 only; presence_probe.sh), so the PoC phase is honestly untested; not a false positive - the missing bound is unconditional on every RXEOF.