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

Mbuf UAF and double-free in centre_test and send_state (sosend consumes m, loop reuses it)

Summary

LATENT (dead code). centre_test :339-347 and send_state :458-466: build ONE mbuf then for(i<count) so_pru_sosend(socks[i],...,m,...). sosendudp consumes+frees m on success (uipc_socket.c:1044-1053 top=NULL). 2nd iteration passes freed m (UAF). centre_test additionally m_free(m) :347 after loop (double-free). Fix: m_copym per iteration.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0702 Β· 11 files
FileTypeDescriptionSize
ipfw3_sync_deadpath.c trigger-source empirical dead-path demo 4.0 KB view raw
build.sh build-script cc -o ipfw3_sync_deadpath ... 292 B view raw
run.sh run-script issue the SYNC sockopt 391 B view raw
VERDICT.md verdict source trace (sosend-eats-mbuf UAF + double-free) + dead-code proof + fix 5.6 KB ↓ raw
fix.diff suggested-fix m_copym per iteration + single m_free (both centre_test and send_state) 1.5 KB view raw
run.log run-log dead-path demo output (ENOPROTOOPT) 399 B view raw
env.txt environment uname, cc version 365 B view raw
README.md readme human reproduce doc 197 B ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
live_reachability_check.txt reachability-test Live ipfw3_sync reachability evidence - dead code (ptr=NULL) 1.2 KB view raw
README.md readme human reproduce doc
↓ download raw

DF-0702 β€” PoC evidence pack

See VERDICT.md for the full analysis (verdict, mechanism, fix, fix-validation). Reproduce: ./build.sh && ./run.sh. Machine-readable catalog: manifest.json.

VERDICT.md verdict source trace (sosend-eats-mbuf UAF + double-free) + dead-code proof + fix
↓ download raw

DF-0702 β€” Mbuf UAF and double-free in centre_test and send_state (sosend consumes m, loop reuses it)

Same ipfw3_sync dead-code caveat as DF-0700 β€” see that VERDICT for the full unreachability proof. Summary: the bug is real at source level; both functions are unreachable because the sockopt dispatcher (ip_fw3_ctl_sync_ptr, ip_fw3.c:133) is never assigned, and the one live-looking pointer (ipfw_sync_send_state_prt, assigned in MOD_LOAD) is never called by anyone.

Verdict

NOT REPRODUCED β€” LATENT dead code. Real source bug (classic "sosend eats my mbuf" UAF + double-free); unreachable on this kernel. Fix is defense-in-depth.

The bug (source-level trace)

ip_fw3_ctl_sync_centre_test() (ip_fw3_sync.c:313-349)

m = m_getl(len, M_WAITOK, MT_DATA, M_PKTHDR, &nsize);   /* :330 β€” ONE mbuf built */
...
for (i = 0; i < fw3_sync_ctx.count; i++) {
    error = so_pru_sosend(fw3_sync_ctx.centre_socks[i],
            NULL, NULL, m, NULL, 0, td);                 /* :340 β€” passes SAME m */
    if (error) { ... return -1; }
}
m_free(m);                                                /* :347 β€” free again */

so_pru_sosend β†’ sosendudp (sys/kern/uipc_socket.c:960-1060). On the success path sosendudp consumes the mbuf: it either hands top to the lower layer which frees it after TX, or β€” on the in-place goto release paths β€” m_freem(top) at uipc_socket.c:1058-1059. In all cases sosendudp sets top = NULL at :1053 to mark the transfer of ownership (/* sent or freed in lower layer */). So after iteration i=0, the caller's m is freed. Iteration i=1 passes the freed m to sosendudp β†’ use-after-free read (and the lower layer dereferences/frees it again). After the loop, m_free(m) at :347 frees it a third time β†’ double-free (slab INVARIANTS trip, or silent cross-type corruption on a noinv kernel).

ip_fw3_sync_send_state() (ip_fw3_sync.c:438-467)

Same shape, no trailing m_free:

m = m_getl(len, M_WAITOK, MT_DATA, M_PKTHDR, &nsize);   /* :446 */
...
for (i = 0; i < fw3_sync_ctx.count; i++) {
    error = so_pru_sosend(fw3_sync_ctx.centre_socks[i],
            NULL, NULL, m, NULL, 0, td);                 /* :459 β€” SAME m each iter */
    ...
}
return;                                                   /* :466 β€” no m_free */

Iteration iβ‰₯1 reuses the freed m β†’ UAF (read + lower-layer deref/free). No double-free here (no trailing m_free), but the UAF is the same class.

Why the loop even runs (the count > 1 precondition)

Both loops are gated on fw3_sync_ctx.count, which is only ever set by ip_fw3_ctl_sync_centre_conf (the DF-0700/0701 function). So the UAF needs count β‰₯ 2, which needs centre_conf to have been called.

Why unreachable on this kernel

Two independent layers of dead code:

  1. centre_test is a sockopt handler β†’ only reachable through ip_fw3_ctl_sync_sockopt β†’ only dispatched via ip_fw3_ctl_sync_ptr (ip_fw3.c:133), which is never assigned anywhere (grep-verified). Dead. And centre_conf (needed to set count) is dead for the same reason β€” so count stays at its BSS zero and the loop body never executes even if centre_test were reached.

  2. send_state is assigned to ipfw_sync_send_state_prt in MOD_LOAD (ip_fw3_sync.c:474), so it looks live β€” but ipfw_sync_send_state_prt is never called anywhere in the tree (grep shows only the declaration at :85 and the assignment at :474; no call site). And even if it were called, count is 0 (needs dead centre_conf), so the loop is a no-op.

Plus the base-kernel ipfw3 stub returns ENOPROTOOPT until kldload ipfw3, and the modules aren't loaded by default. Empirically confirmed by ipfw3_sync_deadpath (run.log).

Fix (defense-in-depth)

Pass a per-iteration copy of the mbuf to so_pru_sosend (each sosend then owns+frees its own copy), and free the original exactly once after the loop. See fix.diff (both functions):

for (i = 0; i < fw3_sync_ctx.count; i++) {
    struct mbuf *mc = m_copym(m, 0, M_COPYALL, M_NOWAIT);   /* +DF-0702 */
    if (mc == NULL) { ...; m_free(m); return -1; }
    error = so_pru_sosend(fw3_sync_ctx.centre_socks[i],
            NULL, NULL, mc, NULL, 0, td);                   /* pass the copy */
    ...
}
m_free(m);   /* free the original once */

Fix validation

  • fix.diff applies cleanly (both hunks).
  • Compile-validated with DF-0701/0703, make -j6 nativekernel β†’ rc=0.
  • fix_status: not_testable β€” dead code; validated applies + compiles.

Files

file desc
ipfw3_sync_deadpath.c empirical dead-path demo
fix.diff m_copym per iteration + single m_free (both functions)
run.log dead-path demo output (ENOPROTOOPT)
env.txt guest environment

Kernel references (confirmed)

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

Detail

Exploit chain

none β€” dead code.

Evidence (decisive lines)

Same as DF-0700.

PoC changes

Added live_reachability_check.txt.

Verified recommended fix

fix.diff adds m_copym per iteration. Matches finding proposal.

Verdict

NOT REPRODUCED (dead code). Same root cause as DF-0700. The UAF/double-free in centre_test (reusing freed mbuf across loop iterations) is unreachable because ip_fw3_ctl_sync_ptr is NULL.