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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3278 Β· 8 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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_PREPEND on M_NOWAIT allocation failure frees the mbuf chain and leaves the variable NULL (sys/sys/mbuf.h:463-479; m_prepend() does m_freem(m); return NULL, sys/kern/uipc_mbuf.c:1508-1511).
  • After the first failure, error = ENOBUFS, so every subsequent loop iteration takes the else branch and calls m_freem(m) where m is fragment #1 β€” already freed in the failing iteration if the failure was on fragment #1, or freed on the first else iteration 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_nextpkt reads the m_nextpkt of 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_testable
baseline no→ patch + rebuild →patched clean

No 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
↓ fix.diffper-fix-DF-3278

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.