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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3502 Β· 10 files
FileTypeDescriptionSize
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_input deref), 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

  1. ng_fec_addport stores the node pointer in the member's arpcom.ac_netgraph (IFP2NG), but ng_fec_delport and ng_fec_rmnode never clear it. After teardown (kfree(priv), ng_unref frees the node) the member keeps a dangling pointer.
  2. ng_fec_rmnode clears the global ng_ether_input_p unconditionally β€” 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 point ether_input_frame β†’ ng_fec_input runs for every ethernet frame and dereferences IFP2NG() 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_p when the last fec node goes away (armed counter) and only if the global still points at ng_fec_input.

Validated in-guest: identical sequence (shutdown β†’ echo reuse β†’ create β†’ ifconfig up β†’ ping) completes with no panic, 3/3 pings, guest healthy.

VERDICT.md
↓ download raw

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

fixed
↓ fix.diffper-fix-DF-3502

Confirmed kernel references

Detail

Exploit chain

create fec node -> add_iface (IFP2NG= node) -> shutdown node (member left with dangling IFP2NG; shared global cleared) -> create any new fec node (global re-armed) -> first inbound frame on the member: ng_fec_input dereferences freed node -> NULL+0x3f8 read -> kernel panic

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).