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)
PoC verification
Evidence pack
findings/poc/DF-0529 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_NETGRAPHslab allocation (sizeof(struct ng_fec_private)β large, contains the fullstruct 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 (
makeinsys/netgraph/fecβ RC=0,ng_fec.koproduced); - was installed hash-verified into
/boot/kernel/ng_fec.kowith 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_failedfix_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.
No comments yet.