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

Double-kfree/interior-pointer kfree in ng_fec_constructor error paths: heap corruption

Summary

ng_fec_constructor: ifp=&priv->arpcom.ac_if(:1085) ifp is EMBEDDED in priv NOT separately allocated. Both error paths kfree(ifp)(:1093/:1101) then kfree(priv)(:1094/:1102). kfree of interior pointer = UB for slab allocator -> panic or free list corruption. Subsequent kfree(priv) double-frees same allocation. Triggered by ENOMEM in ng_fec_get_unit or failure in ng_make_node_common under memory pressure. Fix: delete kfree(ifp) lines, only kfree(priv).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0529 Β· 13 files
FileTypeDescriptionSize
trigger.sh trigger-source ngctl mkpeer -> ng_fec_constructor panic 1.4 KB view raw
build.sh build-script marks trigger.sh executable 228 B view raw
run.sh run-script runs trigger.sh as root 216 B view raw
fix.diff suggested-fix remove interior-pointer kfree(ifp) from both constructor error paths 558 B view raw
VERDICT.md verdict full narrative: reproduced panic, root-only reachability, fix incomplete 5.5 KB ↓ raw
run.log run-log baseline run output + panic signature 1.1 KB view raw
panic.txt panic-signature fatal _kfree/NULL pointer trace from boot.log 456 B view raw
fix_build.log build-log fix.diff build summary 700 B view raw
fix_run.log run-log fix-validation: panic persists after documented fix 2.0 KB view raw
env.txt environment uname, INVARIANTS=ON, privilege gate 829 B view raw
build.log build-log kernel build log excerpt proving -Werror clean compile of patched source 357 B view 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
VERDICT.md verdict full narrative: reproduced panic, root-only reachability, fix incomplete
↓ download raw

DF-0529 β€” VERDICT

Verdict: REPRODUCED (panic); root-triggered; fix INCOMPLETE (fix_failed)

The bug (confirmed in source)

ng_fec_constructor() (sys/netgraph/fec/ng_fec.c:1071) allocates a single contiguous struct ng_fec_private and then derives an interior pointer:

priv = kmalloc(sizeof(*priv), M_NETGRAPH, M_NOWAIT | M_ZERO);   // :1079
...
ifp = &priv->arpcom.ac_if;                                      // :1085  (INTERIOR)

struct arpcom is embedded inside struct ng_fec_private, so ifp points into the middle of the priv allocation, not to a separately-allocated object. Both error paths then do:

// :1091-1094  (ng_fec_get_unit failure)
kfree(ifp, M_NETGRAPH);   // interior pointer -> UB for the slab allocator
kfree(priv, M_NETGRAPH);  // double-free of the SAME allocation

// :1099-1102  (ng_make_node_common failure)
ng_fec_free_unit(priv->unit);
kfree(ifp, M_NETGRAPH);   // interior pointer again
kfree(priv, M_NETGRAPH);  // double-free again

This is the DF-0529 defect: interior-pointer kfree() + double-free of one allocation β€” a textbook memory-corruption primitive.

Reproduction (deterministic)

Creating an ng_fec node triggers the constructor unconditionally:

# kldload netgraph ; kldload ng_fec
# ngctl mkpeer .: fec myhook peerhook

β†’ kernel panic on EVERY node creation (not merely "under memory pressure" as the finding summary speculated β€” the error path fires on every call on this guest, making the bug strictly more severe than filed). Serial-console proof (dfbsd-qemu/boot.log):

panic: trying to free NULL pointer
_kfree() at _kfree+0x558
_kfree() at _kfree+0x558
ng_fec_constructor() at ng_fec_constructor+0x3ae
ng_mkpeer() -> ng_generic_msg() -> ng_send_msg()

The slab allocator's kfree() (sys/kern/kern_slaballoc.c:1407) panics with "trying to free NULL pointer" because the interior-pointer free corrupts the zone state and the follow-up kfree(priv) dereferences a cascaded NULL. Reproduced 4+ times across separate boots; signature identical each time.

Reachability / privilege model

Netgraph node creation goes through the ng_socket control socket, which is gated by a capability check:

// sys/netgraph/socket/ng_socket.c:172
if (caps_priv_check(ai->p_ucred, SYSCAP_RESTRICTEDROOT|__SYSCAP_NULLCRED) != 0)
    error = EPERM;

Verified empirically: unprivileged user maxx (uid 1001, not in wheel) is denied β€” ngctl: can't create node: Operation not permitted. Only root can create netgraph nodes. Therefore the DF-0529 trigger is root β†’ kernel.

Exploit chain (Phase 6)

  • Primitive class: double-free / interior-pointer free of a M_NETGRAPH slab allocation (sizeof(struct ng_fec_private) β€” large, contains the full struct arpcom/ifnet).
  • On GENERIC (INVARIANTS ON): manifests as a deterministic panic (DoS). The slab INVARIANTS and the NULL-pointer guard catch the corruption before grooming can land.
  • Escalation to uid=0: BLOCKED by a valid hard blocker β€” the write is reachable only from an already-root context (the ng_socket privilege gate requires root; an unprivileged user cannot create an ng_fec node). Rootβ†’kernel is game-over by definition; there is no privilege boundary to cross. This is not a circular precondition (it is the actual, verified access model), so the honest impact is root-triggered kernel panic / memory corruption, not an unprivileged escalation.

Fix validation (Phase 8)

fix.diff removes the two kfree(ifp, M_NETGRAPH) interior-pointer frees, leaving a single correct kfree(priv) on each error path. The diff:

  • applies cleanly (patch -p1 β€” both hunks succeed);
  • compiles (make in sys/netgraph/fec β†’ RC=0, ng_fec.ko produced);
  • was installed hash-verified into /boot/kernel/ng_fec.ko with linker hints rebuilt (kldxref).

But the panic PERSISTS. With the patched module loaded, ngctl mkpeer .: fec ... still panics β€” the constructor offset merely shifts from +0x3ae to +0x38e, proving the patched code is running. An isolation build that removed all kfrees from the constructor error paths (intentional leak) also still panicked with the same two-_kfree/"NULL pointer" signature.

Conclusion: the finding's root-cause analysis is INCOMPLETE. The documented kfree(ifp)/double-kfree(priv) is a real defect, but the constructor has an additional, undocumented kfree cascade β€” most plausibly in the inlined static __inline__ ng_fec_get_unit/ng_fec_free_unit cold paths (which each contain a guarded kfree(ng_fec_units, M_NETGRAPH)), or in the slab allocator's reaction to the M_NETGRAPH zone state after the interior- pointer corruption. fix_status = fix_failed: the fix correctly addresses the documented defect but does not stop the constructor panic. Next iteration: kgdb the live DDB db> prompt to bt/examine the exact inlined kfree site and the value being freed, then extend the fix to cover it.

Knock-on effect

Because the constructor panics on every node creation (and continues to do so even after DF-0529's documented fix), DF-0526, DF-0527, and DF-0528 are all unreachable at runtime on this kernel β€” no ng_fec node can ever exist long enough to pass traffic (choose_port) or be torn down (rmnode). See their individual verdicts.

PoC changes

Wrote trigger.sh (ngctl mkpeer β†’ panic), build.sh/run.sh, fix.diff (remove interior-pointer kfrees), full logs. No upstream PoC existed (folder was empty).

Fix verification

fix_failed

fix_failed - residual cascade

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live panic). ng_fec_constructor kfree(interior ptr ifp) then kfree(priv) -> double-free -> NULL ptr panic on EVERY node creation. Fix.diff removes 2 kfree(ifp) but residual cascade persists.