Double-free of mbuf in XMIT_ALL error path via NG_FREE_ITEM + NG_FREE_M on a peeked (non-detached) mbuf
Summary
ng_one2many_rcvdata XMIT_ALL (ng_one2many.c:425-468): line 425 m=NGI_M(item) PEEK no detach (comment says just peaking mbuf still owned by item). Line 464 m2=m_dup(m,M_NOWAIT). Line 465 if m2==NULL: line 467 NG_FREE_ITEM(item) sets NGQF_FREE flag does NOT null _NGI_M(item). Line 468 NG_FREE_M(m) m_freem frees mbuf NOW. rcvdata returns ENOBUFS. ng_apply_item (ng_base.c:2109) ng_unref_item->ng_free_item (ng_base.c:2635) NG_FREE_M(_NGI_M(item)) on DANGLING pointer -> DOUBLE-FREE. ng_free_item comment ng_base.c:2627-2631 explicitly warns about this exact mistake. Correct pattern at :444 (numActiveMany==0): NG_FREE_ITEM alone no NG_FREE_M mbuf freed by ng_free_item. Trigger: ng_one2many xmitAlg=XMIT_ALL 2+ active many links. m_dup fails under mbuf pressure (flood/mmap/fork-bomb). INVARIANTS: panic freeing free mbuf. Production: silent heap corruption objcache reallocation exploitable for priv-esc with grooming. Attacker: unpriv user traffic traverses bonded interface induces mbuf pressure. Fix: remove NG_FREE_M(m) at :468.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0676 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ng_o2m_df.c | exploit-chain | harness replaying XMIT_ALL error-path macros -> double-free + aliasing/UAF proof; apply_fix tunable validates the fix | 4.7 KB | view raw |
| Makefile | build-config | kmod Makefile | 128 B | β download |
| build.sh | build-script | make -> ng_o2m_df.ko | 100 B | view raw |
| run.sh | run-script | kldload ./ng_o2m_df.ko | 129 B | view raw |
| build.log | build-log | final successful build | 6.0 KB | view raw |
| run.log | run-log | baseline: double-free + aliasing 2 hits | 1.1 KB | view raw |
| fix_run.log | run-log | fixed: _NGI_M NULL, 0 hits | 923 B | view raw |
| fix.diff | suggested-fix | NGI_GET_M detach before NG_FREE_ITEM/NG_FREE_M | 852 B | view raw |
| env.txt | environment | uname / kern.version | 209 B | view raw |
| VERDICT.md | verdict | narrative, primitive, root-only reachability, fix validation | 4.8 KB | β raw |
| README.md | readme | build/run/expected | 917 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 |
DF-0676 PoC β ng_one2many XMIT_ALL double-free / UAF
Build
cd findings/poc/DF-0676 ./build.sh # -> ng_o2m_df.ko
Run (as root; harness replays the XMIT_ALL m_dup-NULL error path deterministically)
# baseline (double-free): kldload ./ng_o2m_df.ko # hw.df0676.apply_fix defaults to 0 dmesg | grep DF0676 # fixed (NGI_GET_M detach, no double-free): kldunload ng_o2m_df kenv hw.df0676.apply_fix=1 kldload ./ng_o2m_df.ko dmesg | grep DF0676
Expected
- Baseline (
apply_fix=0):address 0x... returned 2 time(s)β double-free aliasing (UAF). The mbuf objcache does NOT trap the double-free on GENERIC, so it is silent. - Fixed (
apply_fix=1):_NGI_M(item)=0after the error path;address 0 returned 0 time(s)β no double-free.
The real fix is fix.diff (add NGI_GET_M(item, m); before the NG_FREE_ITEM/NG_FREE_M
pair in ng_one2many_rcvdata).
DF-0676 β ng_one2many XMIT_ALL double-free / use-after-free
Verdict: REPRODUCED (double-free + UAF primitive confirmed; fix validated)
In ng_one2many_rcvdata()'s NG_ONE2MANY_XMIT_ALL branch, the mbuf is only
peeked from the item (m = NGI_M(item), comment "just peaking"), then in the
m_dup()-failure error path it is freed by hand:
/* sys/netgraph7/one2many/ng_one2many.c:425,464-468 */
m = NGI_M(item); /* PEEK: mbuf still owned by item */
...
m2 = m_dup(m, M_NOWAIT);
if (m2 == NULL) {
mdst->stats.memoryFailures++;
NG_FREE_ITEM(item); /* el_flags |= NGQF_FREE; does NOT clear _NGI_M(item) */
NG_FREE_M(m); /* m_freem(m); _NGI_M(item) still == freed mbuf */
return (ENOBUFS);
}
NG_FREE_ITEM (netgraph7/netgraph.h:816) sets NGQF_FREE but does not null
_NGI_M(item). Control returns ENOBUFS to ng_apply_item()
(netgraph7/netgraph/ng_base.c:2027β2109), which calls ng_unref_item(item, error)
β ng_free_item(item) (ng_base.c:2612). The NGQF_DATA case of ng_free_item
does NG_FREE_M(_NGI_M(item)) (ng_base.c:2635) β freeing the already-freed mbuf
β double-free.
Primitive characterization (measured on this guest)
The harness ng_o2m_df.c replays the exact macro sequence from netgraph7/netgraph.h
on a real mbuf + a real struct ng_item:
baseline (buggy): _NGI_M(item)=0xfffff801186e3800 (DANGLING -> freed mbuf) -> ng_free_item frees it again: DOUBLE FREE (silent, objcache has no trap) -> 16 fresh m_gethdr: address 0xfffff801186e3800 returned 2 time(s) => ALIASING / UAF
| property | value |
|---|---|
| object double-freed | an mbuf (objcache-backed) |
| detection | none on GENERIC β the mbuf objcache has no double-free trap, so the corruption is silent |
| derived primitive | the freed mbuf is handed out to two subsequent m_gethdr() callers β they alias the same memory β writes to one corrupt the other β use-after-free |
This is a silent double-free β UAF: far more dangerous than a panicking one.
Reachability β ROOT-ONLY (valid hard blocker for unpriv uid=0)
Building the one2many topology (creating nodes/hooks) needs the NG_CONTROL socket,
whose ngc_attach() (sys/netgraph7/socket/ng_socket.c:182) does
caps_priv_check(SYSCAP_RESTRICTEDROOT) β root-only. An unprivileged user cannot
create the netgraph nodes, so the double-free is not reachable without root. The live
trigger additionally requires m_dup(M_NOWAIT) to fail (mbuf pressure), which is
environment-dependent; the harness removes that dependency by replaying the exact
error-path macros deterministically.
Escalation ceiling
The primitive is a UAF on a kernel mbuf: after the double-free, an attacker who can
reclaim the aliased mbuf (via netgraph traffic) and shape its content can corrupt a
victim object that shares the mbuf bucket. Given no SMAP/SMEP/KASLR, corrupting a
function-pointer-bearing object reachable from the mbuf bucket is a path to kernel code
execution. However, the whole chain requires root to build the topology and to drive
m_dup failure, so this is a rootβkernel primitive (rootβkernel is already
game-over); there is no unprivilegedβuid=0 path because the privilege boundary
(NG_CONTROL socket) is not crossed.
PoC changes
ng_o2m_df.c+Makefile: harness that builds a real mbuf +struct ng_item, replays the XMIT_ALL error-path macros +ng_free_item's NGQF_DATA case, and detects the double-free via free-list aliasing.hw.df0676.apply_fixloader tunable selects baseline (0, double-free) vs the fix (1,NGI_GET_Mdetach, no double-free).
Fix (fix.diff)
Detach the mbuf from the item before freeing it in the error path:
if (m2 == NULL) {
mdst->stats.memoryFailures++;
NGI_GET_M(item, m); /* m = _NGI_M(item); _NGI_M(item) = NULL */
NG_FREE_ITEM(item);
NG_FREE_M(m); /* freed exactly once; ng_free_item sees _NGI_M=NULL (no-op) */
return (ENOBUFS);
}
NGI_GET_M (netgraph.h:825) NULLs _NGI_M(item), so ng_free_item's
NG_FREE_M(_NGI_M(item)) becomes a no-op β eliminating the double free. Validated:
harness aliasing count drops 2 β 0 with the fix.
Kernel references (verified)
sys/netgraph7/one2many/ng_one2many.c:425βm = NGI_M(item)peeksys/netgraph7/one2many/ng_one2many.c:464-468β the double-free error pathsys/netgraph7/netgraph.h:816-820βNG_FREE_ITEM(does NOT clear_NGI_M)sys/netgraph7/netgraph.h:825-828βNGI_GET_M(the fix primitive)sys/netgraph7/netgraph.h:919-924βNG_FREE_Msys/netgraph7/netgraph/ng_base.c:2027,2109βng_apply_itemβng_unref_itemsys/netgraph7/netgraph/ng_base.c:2612,2632-2635βng_free_itemNGQF_DATA double-freesys/netgraph7/socket/ng_socket.c:182βngc_attachroot-only (SYSCAP_RESTRICTEDROOT)
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED. ng_one2many XMIT_ALL error path NG_FREE_ITEM+NG_FREE_M -> ng_free_item double-free. Silent on GENERIC (no objcache trap). Harness: aliasing 2 hits. Root-only.
No comments yet.