delport/rmnode never clear the member's ac_netgraph (IFP2NG); rmnode clears the shared ng_ether_input_p unconditionally -> dangling node pointer detonates in ng_fec_input on the next RX frame after any later fec node re-arms the global: reproduced page-fault panic at NULL+0x3f8 in interrupt context
Summary
addport stores priv->node in the member's ac_netgraph but delport and rmnode never clear it. rmnode additionally clears the SHARED global ng_ether_input_p unconditionally - blinding any other live fec bundle and ng_ether(4) which stores its hook in the same global. After a teardown, any later fec constructor re-arms the global and ether_input_frame then routes every ethernet frame through ng_fec_input, which dereferences IFP2NG of previously-enslaved members -> freed node -> NULL+offsetof(fec_bundle)=0x3f8 read. REPRODUCED exactly: freed chunk reused by an echo node at the identical address; first inbound frame after 'ifconfig vtnet0 up; ping' -> Fatal trap 12, fault VA 0x3f8, 'Stopped at ng_fec_input+0x30', RX context (Idle). Detonation needs NO privilege once the dangling state exists (admin tears down a bundle; the next fec node creation anywhere + first inbound frame on the ex-member panics). Secondary: multi-instance RX blackout via the unconditional global clear. Fix validated (conditional clear on delport + armed-counter guard on the global): identical sequence SURVIVES, 3/3 pings, guest healthy.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3502 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| run.log | β | 3.2 KB | view raw | |
| run.sh | β | 90 B | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| enablers.sh | β | 1.2 KB | view raw | |
| fixes.sh | β | 2.8 KB | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| verdict.json | β | 3.3 KB | view raw | |
| manifest.json | β | 750 B | view raw |
DF-3502 β delport/rmnode never clear the member's ac_netgraph (IFP2NG); rmnode clears the shared ng_ether_input_p unconditionally β RX-frame UAF/NULL-deref panic landmine + multi-instance RX blackout
- File:
sys/netgraph/fec/ng_fec.c(v1 netgraph, LIVE) - Lines: 379 (set
ac->ac_netgraph = priv->node), 416-471 (delport: no clear), 739-783 (ng_fec_inputderef), 1132-1134 (constructor arms the shared global), 1233-1234 (rmnode clears it unconditionally) - Severity: Medium (kernel UAF/NULL-deref panic triggered by any inbound ethernet frame after a recreate cycle; secondary: total RX blackout for coexisting fec bundles and ng_ether(4), which shares the global)
Defects
ng_fec_addportstores the node pointer in the member'sarpcom.ac_netgraph(IFP2NG), butng_fec_delportandng_fec_rmnodenever clear it. After teardown (kfree(priv),ng_unreffrees the node) the member keeps a dangling pointer.ng_fec_rmnodeclears the globalng_ether_input_punconditionally β even when other fec nodes (or ng_ether, which uses the same global) are still using it. Any later fec constructor re-arms it (if (ng_ether_input_p == NULL)), at which pointether_input_frameβng_fec_inputruns for every ethernet frame and dereferencesIFP2NG()of previously-enslaved members β freed node βnode->private(NULL or reused) β NULL+offset read (measured offset 0x3f8 =offsetof(struct ng_fec_private, fec_bundle)) or wild list walk β kernel panic in RX/interrupt context.
Reproduction (run.sh)
sysctl -n debug.fecdrive=create # fec node f0
sysctl -n debug.fecdrive="add vtnet0" # ac_netgraph(vtnet0) = f0-node
# detached (teardown downs vtnet0, ssh dies):
sysctl -n debug.fecdrive=shutdown # f0 freed; global cleared;
# ac_netgraph dangles
sysctl -n debug.fecdrive=echo # echo node REUSES f0's chunk
# (same address, private=NULL)
sysctl -n debug.fecdrive=create # new fec node re-arms global
ifconfig vtnet0 up; ping 10.0.2.2 # first inbound frame:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x3f8
current process = Idle (RX/interrupt context)
Stopped at ng_fec_input+0x30: movq 0x3f8(%rbx),%rax
The detonation needs no privilege once the dangling state exists: any
inbound frame on the member (external traffic, or any local user's traffic
that elicits a response). Deterministic across chunk-reuse outcomes: no
reuse β private reads NULL β fault at 0x3f8; reuse by a private-less
node (echo) β same; reuse by a node with a live private β foreign bundle
walk (wild deref or silent blackhole).
Fix (validated)
- delport:
if (ac->ac_netgraph == (void *)priv->node) ac->ac_netgraph = NULL; - rmnode: only clear
ng_ether_input_pwhen the last fec node goes away (armed counter) and only if the global still points atng_fec_input.
Validated in-guest: identical sequence (shutdown β echo reuse β create β ifconfig up β ping) completes with no panic, 3/3 pings, guest healthy.
DF-3502 VERDICT β reproduced (panic; deterministic RX landmine)
What was run
Guest: DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS), vtnet0 virtio NIC. ng_fec.ko from HEAD source + lab enablers E1/E2/E3 (see ../DF-3500; none touch the defect β ac_netgraph lifetime and ng_ether_input_p handling are stock). Node operations driven from thread context by fecdrive.ko.
Decisive run (run.log, panic.txt, 2026-09-13 23:19 UTC):
FECDRIVE: ng_make_node(fec) = 0 node=0xfffff80117ab2840 # f0
FECDRIVE: add 'vtnet0' = 0
# ng_fec.c:379: ac_netgraph(vtnet0) = f0-node; global armed
FECDRIVE: rmnode done
# delport frees the port entry; rmnode kfree(priv), node->private
# = NULL, ng_unref frees the node chunk, and ng_fec.c:1233-1234
# clears the shared ng_ether_input_p. delport NEVER clears
# ac_netgraph -> IFP2NG(vtnet0) dangles at 0xfffff80117ab2840.
FECDRIVE: ng_make_node(echo) = 0 node=0xfffff80117ab2840
# the freed chunk is immediately reused by an echo node
# (->private == NULL)
FECDRIVE: ng_make_node(fec) = 0 node=0xfffff80117ab28a0
# second fec node re-arms ng_ether_input_p = ng_fec_input
# ifconfig vtnet0 up; ping -c 3 10.0.2.2
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x3f8
current process = Idle # interrupt / RX context
Stopped at ng_fec_input+0x30: movq 0x3f8(%rbx),%rax
Mechanism (path:line)
ether_input_frame (sys/net/if_ethersubr.c:1273-1279) calls the armed
ng_ether_input_p for every ethernet frame; ng_fec_input (ng_fec.c:752)
reads node = IFP2NG(vtnet0) β the freed/reused chunk; priv =
node->private == NULL; priv->fec_bundle at offsetof 0x3f8 is read ->
page fault at 0x3f8. With no chunk reuse the same NULL is read from the
freed chunk (rmnode wrote NULL at ng_fec.c:1239); with reuse by a node
holding a live private, ng_fec_input instead TAILQ-walks a foreign bundle
(wild deref / silent frame blackhole).
Trigger model
Creating the dangling state needs the netgraph control socket (root on stock DFly β e.g. an admin tearing down a fec bundle). Detonation needs no privilege: the next fec node creation anywhere on the box re-arms the global, and the next inbound frame on the previously-enslaved member panics the kernel (external traffic or any local user's traffic that elicits a response). The member is administratively down right after teardown, so in practice the landmine fires when the NIC is next brought up (standard admin recovery) and traffic flows.
Secondary facet
ng_fec_rmnode clears the SHARED global unconditionally (ng_fec.c:1233- 1234): with two live fec bundles, tearing down either one clears ng_ether_input_p for both (and for ng_ether(4), which stores ng_ether_input in the same global, sys/netgraph/ether/ng_ether.c:695) β permanent RX blackhole with all links "up".
Fix validation
Candidate fix (delport clears ac_netgraph when it still points at this node + rmnode clears the global only when the last fec node goes away and only if it still points at ng_fec_input) applied on top of the enablers: identical sequence (shutdown -> echo chunk reuse -> create -> ifconfig up -> ping) completes with no panic, 3/3 pings, guest healthy (df3502fix.log excerpt in VERDICT.md of the run: "SURVIVED"). fix_status: fixed.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedConfirmed kernel references
Detail
Exploit chain
create fec node -> add_iface
Evidence (decisive lines)
panic.txt (Fatal trap 12, fault VA 0x3f8, Stopped at ng_fec_input+0x30, current process Idle); run.log (step-by-step with node addresses 0xfffff80117ab2840 freed then reused by echo node at the same address); run.sh; fixes.sh + fix.diff
PoC changes
driven from thread context via fecdrive.ko (stock csock path blocked by DF-3499/DF-3500); lab enablers E1/E2/E3 documented in enablers.sh β none touch the defect
Verified recommended fix
delport: clear ac->ac_netgraph when it equals priv->node; rmnode: clear ng_ether_input_p only when the last fec node is destroyed and the global still points at ng_fec_input
Verdict
ng_fec_delport/ng_fec_rmnode never clear the member's ac_netgraph (IFP2NG) while addport stores the node pointer there, and rmnode clears the shared ng_ether_input_p global unconditionally. After any fec teardown the member keeps a dangling node pointer; the next fec node creation re-arms the global, and the next inbound ethernet frame on that member runs ng_fec_input over freed memory: reproduced deterministically as a page fault at address 0x3f8 (priv==NULL, offsetof(fec_bundle)=0x3f8) in RX/interrupt context (Idle process), with the freed node chunk provably reused by an echo node at the identical address. Secondary facet: rmnode's unconditional clear blinds every other live fec bundle and ng_ether(4), which shares the same global. Fixed and validated: with delport clearing IFP2NG and the global clear made reference-counted, the identical detonation sequence survives (3/3 pings, no panic).
No comments yet.