ng_fec_shutdown infinite loop when member interface destroyed: dangling pointer + unkillable loop
Summary
ng_fec_shutdown(:1335-1339): while(!TAILQ_EMPTY) calls ng_fec_delport(priv,p->fec_if->if_xname). delport re-resolves via ifunit(:481), if interface destroyed returns ENOENT(:485) WITHOUT removing portlist entry -> TAILQ_FIRST returns same entry forever -> infinite loop. p->fec_if is dangling pointer to freed ifnet -> if_xname is UAF read. Privileged local user destroys member iface before shutdown -> hangs kernel. Fix: iterate+remove directly in shutdown, dont re-resolve.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0502 · 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| repro_attempt.sh | trigger-source | ngctl mkpeer attempt + documents the ng_ether NGM_MKPEER blocker | 1.5 KB | view raw |
| fix.diff | suggested-fix | direct TAILQ_REMOVE+kfree in ng_fec_shutdown (no ifunit re-resolve) | 882 B | view raw |
| VERDICT.md | verdict | full narrative | 1.9 KB | ↓ raw |
| README.md | readme | summary | 3.3 KB | ↓ raw |
| env.txt | environment | guest uname, modules, HW-gate note | 188 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-0502 — ng_fec_shutdown infinite loop when a member interface is destroyed
Verdict: INCONCLUSIVE at runtime — bug is unambiguous by inspection (certain); netgraph node-creation bootstrap is blocked on this guest
The bug (unambiguous, certain)
sys/netgraph7/ng_fec.c ng_fec_shutdown() (lines 1335-1339):
while (!TAILQ_EMPTY(&b->ng_fec_ports)) {
p = TAILQ_FIRST(&b->ng_fec_ports);
ng_fec_ether_cmdmulti(priv->ifp, p, 0);
ng_fec_delport(priv, p->fec_if->if_xname); /* re-resolves by name */
}
ng_fec_delport() (line 481) re-resolves the member via bifp = ifunit(iface).
If the member interface has already been destroyed, ifunit() returns NULL
and delport returns ENOENT (line 485) without removing the portlist entry
(TAILQ_REMOVE at :515 is skipped). So TAILQ_FIRST returns the same p
forever ⇒ infinite loop in kernel context (unkillable). Additionally
p->fec_if is a dangling pointer to the freed ifnet (no refcount is held —
new->fec_if = bifp at :450), so p->fec_if->if_xname is a use-after-free
read on every iteration.
This is deductively certain from the source: the loop's termination condition
(TAILQ_EMPTY) can never become true once a member is unresolvable.
Privilege boundary
Creating/destroying an ng_fec node and member interfaces requires root
(ngctl/kldload/ifconfig destroy). Root-only ⇒ root→kernel self-DoS /
hardening gap; no unpriv→root escalation.
Why not reproduced on this guest
The live trigger needs an ng_fec node to exist (with a member port that is
then destroyed before shutdown). Creating an ng_fec node requires mkpeer,
which on this DragonFly netgraph7 build is non-functional for interface-backed
node types:
sys/netgraph7/ether/ng_ether.c ng_ether_rcvmsg() (line 560-562):
default: /* any typecookie != NGM_ETHER_COOKIE */
error = EINVAL;
break;
ng_ether is the only hookable node family available on the guest (one per
ethernet/tap interface), and it eats generic control messages — including
NGM_MKPEER (NGM_GENERIC_COOKIE) — returning EINVAL without forwarding them
to the generic netgraph handler. Confirmed empirically: ngctl mkpeer tap0:
upper fec lower, ... echo x, ... tee x all fail with send msg: Invalid
argument, while ngctl connect tap0: tap1: upper lower succeeds. With no way
to instantiate an ng_fec node, the shutdown infinite loop cannot be exercised.
repro_attempt.sh records the exact commands and the blocker.
Fix (applies + compiles + boots)
fix.diff makes ng_fec_shutdown remove portlist entries directly instead of
re-resolving each member by name:
while ((p = TAILQ_FIRST(&b->ng_fec_ports)) != NULL) {
ng_fec_ether_cmdmulti(priv->ifp, p, 0);
TAILQ_REMOVE(&b->ng_fec_ports, p, fec_list);
kfree(p, M_NETGRAPH);
b->fec_ifcnt--;
}
This both breaks the infinite loop (entry is always removed) and avoids the
p->fec_if UAF read (no dereference of the dangling pointer). Built into the
combined single-fix kernel (#1); boots clean. fix_status = not_testable
(the trigger topology cannot be set up on this guest).
Files
repro_attempt.sh— the ngctl/ifconfig sequence + the observed EINVAL blockerfix.diff— direct-remove in shutdownREADME.md,VERDICT.md,manifest.json
DF-0502 detailed verdict
Verdict: INCONCLUSIVE at runtime — bug certain by inspection; netgraph bootstrap blocked on guest
Mechanism (the infinite loop, deductively certain)
sys/netgraph7/ng_fec.c ng_fec_shutdown (:1335-1339) loops
while(!TAILQ_EMPTY) { p=TAILQ_FIRST; ...; ng_fec_delport(priv, p->fec_if->if_xname); }.
ng_fec_delport (:481-485) re-resolves the member with ifunit(iface); if the
member interface was destroyed, ifunit returns NULL and delport returns
ENOENT without TAILQ_REMOVE (:515 skipped) ⇒ TAILQ_FIRST returns the
same entry every iteration ⇒ unbounded kernel loop. p->fec_if (set at :450
with no refcount) is dangling ⇒ p->fec_if->if_xname is a UAF read each loop.
Why no runtime reproduction
Trigger needs an ng_fec node. On this DragonFly netgraph7 build, ng_ether
(the only available hookable node family) drops generic control messages
including NGM_MKPEER — ng_ether_rcvmsg default: error = EINVAL
(sys/netgraph7/ether/ng_ether.c:560-562). Empirically verified: every
ngctl mkpeer tap0: <hook> <type> <hook> (echo/tee/fec) fails EINVAL, while
ngctl connect tap0: tap1: succeeds. No way to instantiate an ng_fec node ⇒
shutdown path cannot be exercised. (repro_attempt.sh records this.)
Privilege boundary
Root-only (ng_fec node creation/destroy + ifconfig destroy need root).
Root→kernel self-DoS / hardening gap; no unpriv→root escalation.
Fix (validated applies+compiles+boots)
fix.diff: iterate TAILQ_FIRST and TAILQ_REMOVE+kfree each entry
directly in ng_fec_shutdown (no ifunit re-resolve, no p->fec_if
dereference). Built in the combined single-fix kernel (#1); boots clean.
fix_status = not_testable (trigger topology unavailable on guest).
Why no chain
Root-only DoS (infinite loop); not a write/corruption primitive a non-root user can reach. No escalation chain.
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
—
Detail
Exploit chain
none
Evidence (decisive lines)
—
Verdict
Source-confirmed certain. ng_fec_shutdown infinite loop + UAF when member ifp destroyed. ng_ether_rcvmsg blocks MKPEER on this build.
No comments yet.