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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0676 Β· 13 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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)=0 after 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).

VERDICT.md verdict narrative, primitive, root-only reachability, fix validation
↓ download raw

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_fix loader tunable selects baseline (0, double-free) vs the fix (1, NGI_GET_M detach, 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)

Fix verification

fixed

validated

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.