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)
PoC verification
Evidence pack
findings/poc/DF-4111 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- mbuf leak:
m_headis 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_getclfailures are exactly the condition that triggers more of this path). - NULL mbuf into BPF:
cur_tx->tl_mbufis 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 callsbpf_mtap(bp, NULL)(sys/net/bpf.c:1299), which dereferences the mbuf after itsbif_dlistcheck β kernel NULL-deref panic. - Garbage TX descriptor chained:
cur_txis linked into the TX chain with a staletlist_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).
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:
- mbuf leak: on the failure path neither
tl_encapnortl_startfreesm_headand it is stored nowhere (c->tl_mbuf = m_headat 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). - NULL-mbuf BPF tap: TX chain entries are NULLed on recycle
(if_tl.c:1493) and softc starts zeroed, so
cur_tx->tl_mbufis NULL when the tap runs.BPF_MTAP(ifp, m)guards onlyifp->if_bpf(sys/net/bpf.h:265-272); with a listener attached,bpf_mtap(bp, NULL)(sys/net/bpf.c:1299) passes the earlybp == 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.) - non-READY list chained:
tlist_cstatis 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_startissues 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_testablefix.diff authored against the read-only sys/ tree (patch --dry-run clean); cannot be validated without ThunderLAN hardware.
findings/poc/DF-4111/fix.diff
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.
No comments yet.