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

tl_start ignores tl_encap() failure: mbuf chain leak, NULL-mbuf BPF tap (panic with listener), non-READY list chained into TX

Summary

tl_encap's only failure path (m_getl OOM for the >10-fragment collapse) frees nothing and arms nothing, but tl_start discards the return and proceeds: m_head is leaked (self-amplifying toward mbuf exhaustion), cur_tx->tl_mbuf stays NULL and is passed to BPF_MTAP (panic when a BPF listener is attached), and the descriptor is chained with stale cstat and may receive TX GO. Handle-failure fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4111 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
presence_probe.sh β€” 521 B view raw
env.txt β€” 347 B view raw
fix.diff β€” 640 B view raw
manifest.json β€” 673 B view raw
verdict.json β€” 2.6 KB view raw

DF-4111 β€” tl(4): tl_start ignores tl_encap() failure β€” mbuf leak, NULL-mbuf BPF tap, non-READY list chained into TX

What this is

tl_encap() has exactly one failure path β€” the cluster allocation for the >10-fragment collapse (special case #1):

m_new = m_getl(m_head->m_pkthdr.len, M_NOWAIT, MT_DATA, M_PKTHDR, NULL);
if (m_new == NULL) {
        if_printf(&sc->arpcom.ac_if, "no memory for tx list\n");
        return (1);                 /* if_tl.c:1775-1777 */
}

tl_start() discards the return value and proceeds as if the descriptor were armed:

/* Pack the data into the list. */
tl_encap(sc, cur_tx, m_head);       /* if_tl.c:1856 β€” return ignored */
...
BPF_MTAP(ifp, cur_tx->tl_mbuf);     /* if_tl.c:1865 β€” tl_mbuf is NULL */

On failure tl_encap has assigned neither c->tl_mbuf (if_tl.c:1806 not reached) nor tlist_cstat = TL_CSTAT_READY (if_tl.c:1809 not reached). Consequences, all three from one ignored error:

  1. mbuf leak: m_head is neither freed nor stored anywhere β€” one full packet cluster chain leaked per failure; sustained memory pressure + traffic converts this into mbuf-pool exhaustion (system wide DoS: m_getcl failures are exactly the condition that triggers more of this path).
  2. NULL mbuf into BPF: cur_tx->tl_mbuf is NULL (tx entries are NULLed when recycled, if_tl.c:1493, and never assigned on the failed path). BPF_MTAP(ifp, NULL) with at least one listener attached calls bpf_mtap(bp, NULL) (sys/net/bpf.c:1299), which dereferences the mbuf after its bif_dlist check β€” kernel NULL-deref panic.
  3. Garbage TX descriptor chained: cur_tx is linked into the TX chain with a stale tlist_cstat (after tl_stop's bzero it is 0 β€” not READY) and up to 10 fragment entries pointing at the freed-by-caller nothing / previous packet data; if the chip is told to GO on this chain (head case) it walks a non-READY list β€” undefined behavior, watchdog recovery at best.

Trigger

Any transmit through tl(4) while m_getl(M_NOWAIT) fails (transient mbuf exhaustion β€” e.g. bursty RX under low-memory conditions, or an attacker-induced mbuf shortage via socket queues). The BPF panic additionally needs a bpf listener on the interface. Root-only BPF and memory-pressure timing β†’ Low severity.

PoC status β€” untested (hardware absent)

Guest has no ThunderLAN device (presence_probe.sh, identical to DF-4110's); tl_start never executes. Source-level certainty.

Fix

Check the return: free the packet, return the chain entry to the free list, continue (fix.diff).

VERDICT.md
↓ download raw

DF-4111 β€” VERDICT

Finding: tl_start() ignores tl_encap()'s failure return (sys/dev/netif/tl/if_tl.c:1856). The failure path (cluster OOM for the

10-fragment collapse, if_tl.c:1773-1777) frees nothing and arms nothing, but tl_start chains the descriptor anyway and BPF-taps it (if_tl.c:1865) with cur_tx->tl_mbuf == NULL.

Status: untested. The audit guest has no ThunderLAN device (vtnet0 lo0; presence_probe.sh), so tl_start cannot execute there. Source-level certainty β€” the ignored return is directly visible and the three consequences follow unconditionally:

  1. mbuf leak: on the failure path neither tl_encap nor tl_start frees m_head and it is stored nowhere (c->tl_mbuf = m_head at if_tl.c:1806 is not reached). Each failure leaks the packet's whole mbuf chain; failures are caused by mbuf scarcity, so the leak is self-amplifying toward mbuf exhaustion (system-wide hang class).
  2. NULL-mbuf BPF tap: TX chain entries are NULLed on recycle (if_tl.c:1493) and softc starts zeroed, so cur_tx->tl_mbuf is NULL when the tap runs. BPF_MTAP(ifp, m) guards only ifp->if_bpf (sys/net/bpf.h:265-272); with a listener attached, bpf_mtap(bp, NULL) (sys/net/bpf.c:1299) passes the early bp == NULL/SLIST_EMPTY(&bp->bif_dlist) checks and dereferences the NULL mbuf β†’ panic. (With no listener attached the tap is a no-op β€” hence only DoS, not always-panic.)
  3. non-READY list chained: tlist_cstat is left stale (0 after tl_stop's bzero, if_tl.c:2164-2165; never set to TL_CSTAT_READY because if_tl.c:1809 is not reached). If this descriptor becomes the TX head, tl_start issues TX GO on a non-READY list (if_tl.c:1881-1892) β€” chip-visible garbage state; watchdog recovers at best.

Impact ceiling: local DoS (panic with BPF attached; mbuf-pool exhaustion otherwise). Requires transient OOM at TX time; BPF panic additionally requires a tap listener (privileged). No memory-safety write primitive.

Fix: handle the return in tl_start β€” free the packet, push the chain entry back on the free list, continue (fix.diff). Cannot be validated without 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 without ThunderLAN hardware.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4111/VERDICT.md (trace if_tl.c:1856/1773-1777/1806/1809/1865/1493/1881-1892, bpf.c:1299); presence_probe.sh + env.txt; fix.diff (free packet + recycle chain entry on encap failure)

PoC changes

No seed PoC. Hardware-gated (no ThunderLAN NIC on guest); presence probe only.

Verified recommended fix

Check tl_encap's return in tl_start: m_freem(m_head), push cur_tx back on tl_tx_free, continue.

Verdict

Source-certain, hardware-gated. tl_start ignores tl_encap's failure return (sys/dev/netif/tl/if_tl.c:1856). The only failure path (cluster OOM for the >10-fragment collapse, if_tl.c:1773-1777) frees nothing and arms nothing, yet tl_start chains the descriptor and BPF_MTAPs it with cur_tx->tl_mbuf == NULL (if_tl.c:1865; entries NULLed on recycle at :1493, assignment at :1806 unreached). Three consequences: (1) m_head mbuf chain leaked per failure - self-amplifying toward mbuf exhaustion under the very memory pressure that causes the failure; (2) with a BPF listener attached, bpf_mtap(bp, NULL) (sys/net/bpf.c:1299) passes its bp/bif_dlist guards and NULL-derefs the mbuf - kernel panic; (3) a non-READY (stale cstat) list is chained into TX and may receive TX GO (if_tl.c:1881-1892) - chip-visible garbage state. Impact ceiling: local DoS (OOM-timed; BPF leg privileged). Guest has no ThunderLAN NIC (presence_probe.sh), tl_start never executes there - honestly untested; not a false positive.