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)
PoC verification
Evidence pack
findings/poc/DF-0702 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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:
-
centre_testis a sockopt handler β only reachable throughip_fw3_ctl_sync_sockoptβ only dispatched viaip_fw3_ctl_sync_ptr(ip_fw3.c:133), which is never assigned anywhere (grep-verified). Dead. Andcentre_conf(needed to setcount) is dead for the same reason β socountstays at its BSS zero and the loop body never executes even ifcentre_testwere reached. -
send_stateis assigned toipfw_sync_send_state_prtin MOD_LOAD (ip_fw3_sync.c:474), so it looks live β butipfw_sync_send_state_prtis never called anywhere in the tree (grep shows only the declaration at:85and the assignment at:474; no call site). And even if it were called,countis 0 (needs deadcentre_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.diffapplies 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)
sys/net/ipfw3_basic/ip_fw3_sync.c:313-349βcentre_test(UAF + double-free)sys/net/ipfw3_basic/ip_fw3_sync.c:330β singlem_getlsys/net/ipfw3_basic/ip_fw3_sync.c:340-341βso_pru_sosend(..., m, ...)in loopsys/net/ipfw3_basic/ip_fw3_sync.c:347βm_free(m)after loop (double-free)sys/net/ipfw3_basic/ip_fw3_sync.c:438-467βsend_state(UAF, no double-free)sys/net/ipfw3_basic/ip_fw3_sync.c:446β singlem_getlsys/net/ipfw3_basic/ip_fw3_sync.c:459-460βso_pru_sosend(..., m, ...)in loopsys/kern/uipc_socket.c:1044-1053βsosendudpconsumestop(top = NULL)sys/net/ipfw3/ip_fw3.c:133βip_fw3_ctl_sync_ptr = NULL(dead dispatcher)
Fix verification
not_testablecompile 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.
No comments yet.