bridge_fragment() re-header loop double-frees the fragment chain and UAF-reads freed mbufs when any M_PREPEND(M_NOWAIT) fails
Summary
After a successful ip_fragment(), the loop re-attaches headers with M_PREPEND(m0, ..., M_NOWAIT) - which frees the mbuf chain on allocation failure and leaves m0 NULL. The code sets error=ENOBUFS and continues, so every subsequent iteration takes the 'else m_freem(m)' branch, re-freeing fragment #1 (double free, repeated N-2 times for N fragments), while the loop increment reads m0->m_nextpkt of just-freed memory. Reachable from bridge_pfil's member-OUT path when pfil hooks are loaded (ipfw/pf) and a forwarded packet exceeds a member's MTU, under transient mbuf pressure. Path driven live: 500,050 datagrams fragmented with ipfw hooks active and the cluster pool driven to exhaustion - allocation-failure window not caught from userland: not_reproduced (wedge-only), defect code-certain, explicitly not false-positive. Kernel heap double-free/UAF on mbufs via forwarded traffic; narrow but real. Fix: free the remainder exactly once and stop walking freed memory.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3278 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.1 KB | β raw | |
| verdict.json | β | 3.4 KB | view raw | |
| env.txt | β | 743 B | view raw | |
| build.log | β | 319 B | view raw | |
| run.sh | β | 118 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| frag_flood.c | β | 5.7 KB | view raw |
DF-3278 β bridge_fragment() double-free / UAF on M_PREPEND failure
What
if_bridge.c:4386-4405: if any M_PREPEND(m0, ..., M_NOWAIT) fails
while re-attaching Ethernet headers to an ip_fragment() chain,
- the failed fragment m0 is freed by the M_PREPEND macro itself,
- the else m_freem(m) branch then frees fragment #1 for EVERY
remaining iteration (double free), and
- the loop increment reads m0->m_nextpkt of freed memory (UAF read).
Preconditions: pfil hooks loaded (ipfw/pf), forwarded packet larger than the egress member's MTU, and a transient m_gethdr(M_NOWAIT) failure at the wrong instant.
Reproduce (guest, as root)
cd /root/poc/df-3278 cc -O -o frag_flood frag_flood.c sh run.sh # kldload ipfw, mtu-600 members, flood ./frag_flood 3000000 drain # heavy variant: also pins mbuf clusters
Status
NOT reproduced deterministically (allocation-failure window could not
be forced from userland); the fragment path itself was verified live:
500,050 datagrams fragmented through bridge_fragment and the cluster
pool driven to exhaustion (objcache(mbuf cluster) exhausted).
See VERDICT.md for the full code-path proof.
DF-3278 β VERDICT
NOT REPRODUCED within the guest budget β but the defect is code-certain and the vulnerable path was driven live at scale. Classified not_reproduced (window not hit), not false_positive.
The defect (code analysis, certain)
bridge_fragment() (sys/net/bridge/if_bridge.c:4367-4416):
error = ip_fragment(ip, &m, ifp->if_mtu, ifp->if_hwassist,
CSUM_DELAY_IP);
if (error)
goto out;
for (m0 = m; m0; m0 = m0->m_nextpkt) { /* (C) UAF read */
if (error == 0) {
if (snap) {
M_PREPEND(m0, sizeof(struct llc), M_NOWAIT);
if (m0 == NULL) { error = ENOBUFS; continue; } /* (A) */
...
}
M_PREPEND(m0, ETHER_HDR_LEN, M_NOWAIT);
if (m0 == NULL) { error = ENOBUFS; continue; } /* (A) */
bcopy(eh, mtod(m0, caddr_t), ETHER_HDR_LEN);
} else
m_freem(m); /* (B) double free */
}
- (A)
M_PREPENDonM_NOWAITallocation failure frees the mbuf chain and leaves the variable NULL (sys/sys/mbuf.h:463-479;m_prepend()doesm_freem(m); return NULL, sys/kern/uipc_mbuf.c:1508-1511). - After the first failure,
error = ENOBUFS, so every subsequent loop iteration takes theelsebranch and callsm_freem(m)wheremis fragment #1 β already freed in the failing iteration if the failure was on fragment #1, or freed on the firstelseiteration and then freed AGAIN on every following one (fragment chains of N β₯ 3 give Nβ2 double frees). - (C) the loop increment
m0 = m0->m_nextpktreads them_nextpktof the mbuf just freed by M_PREPEND β a use-after-free read.
Reachability (verified live on the guest)
bridge_fragment() is called only from bridge_pfil()
(if_bridge.c:4115-4121) on the member-OUT path when pfil_member is
set, inet pfil hooks are registered, and m_pkthdr.len > member
if_mtu. All preconditions were established and verified:
kldload ipfw+ipfw add 100 allow all from any to any(ipfw2 initialized;inet_pfil_hook.ph_hashooks > 0).- bridge0 with both members at MTU 600; injector writes valid-IP 1500-byte UDP frames into tap0.
- Verified: 50/50 test frames fragmented (
netstat -s -p ip: "50 output datagrams fragmented"); a 6Γ500,000-frame flood pushed 500,050 datagrams through bridge_fragment with zero bad sums. - To force the M_NOWAIT failure, kernel mbuf clusters were pinned via
8 UDP loopback sockets with 2 MB SO_RCVBUF never read
(kern.ipc.maxsockbuf raised to 8 MB): serial console showed
Warning: objcache(mbuf cluster) exhausted on cpu1..cpu5. - Under this pressure the fragment re-header loop was exercised, but
the exact instant where
m_prepend()must allocate (fragment with zero leading space) AND the allocation fails was never observed: both heavy attempts ended with the box wedged by cluster exhaustion (mbuf starvation DoS from the drainer itself) rather than the targeted double free. Deterministically forcing a per-cpu objcache failure at one specific allocation site is not possible from userland without kernel knobs.
Impact if triggered
Kernel heap double-free / UAF read on M_DEVBUF mbufs, driven by forwarded traffic, preconditions = root-configured bridging with pfil (ipfw/pf) and an MTU-mismatched member plus transient mbuf pressure. Given the guest's lack of SMAP/SMEP/KASLR this is plausibly exploitable, but no exploit was developed since the primitive itself could not be fired.
Files
- frag_flood.c β final injector (valid checksum β computed byte-wise; an earlier version's LE-word sum and a strict-aliasing reorder each produced invalid checksums that bridge_ip_checkbasic dropped, which is why early runs showed zero fragmentation).
- run.sh β setup + flood.
- run.log β counters proving the fragment path ran at scale + the exhaustion warnings.
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_testableNo fix build: the defect could not be reproduced at will, so baseline/patched comparison is impossible on this guest.
findings/poc/DF-3278/VERDICT.md
Confirmed kernel references
Detail
Evidence (decisive lines)
["run.log: '500050 output datagrams fragmented' with ipfw active; objcache-exhaustion warnings from the drainer runs", 'VERDICT.md: line-by-line path proof with the three defect sites (A: macro-freed fragment, B: repeated m_freem(m), C: UAF loop increment)', 'frag_flood.c: final injector with byte-wise big-endian checksum (earlier LE-word and strict-aliasing checksum bugs silently zeroed fragmentation; root-caused via netstat badsum counters)']
PoC changes
Written from scratch. ipfw must be loaded via a background shell that immediately installs allow-rules (kldload ipfw defaults to deny and cuts ssh); member MTUs must match the bridge MTU (both taps at 600); the IP checksum must be computed byte-wise big-endian -- two silent failure modes (LE-word summation; -O strict-aliasing reorder against struct ip*) were found and fixed via netstat -s badsum counters and tcpdump.
Verified recommended fix
on M_PREPEND failure set error and free the remaining chain via m_freem(m0->m_nextpkt-traversing loop) exactly once, or pre-allocate headers before fragmenting
Verdict
Code-certain double-free/UAF not fired within the guest budget. bridge_fragment() (if_bridge.c:4386-4405) frees a fragment via M_PREPEND failure semantics (macro frees the chain on M_NOWAIT alloc failure, sys/sys/mbuf.h:463-479 / uipc_mbuf.c:1508-1511), then the 'else m_freem(m)' branch re-frees fragment #1 on every subsequent iteration while the loop increment reads m0->m_nextpkt of freed memory. The vulnerable function was driven live at scale -- 500,050 datagrams fragmented through it with ipfw pfil hooks active (verified via netstat -s and ipfw counters; MTU-600 members) -- and the mbuf cluster pool was driven to exhaustion ('Warning: objcache(mbuf cluster) exhausted on cpu1..cpu5' via 8 never-read 2MB-SO_RCVBUF loopback drainers), but the precise allocation-failure instant inside the re-header loop never coincided with a zero-leading-space fragment. Both heavy attempts ended in mbuf-starvation wedge (drainer-induced DoS), not the target crash. Deterministic forcing of a per-cpu objcache failure at one allocation site is not achievable from userland; a kernel-internal fault injector would be needed.
No comments yet.