DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0502

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0502 · 7 files
FileTypeDescriptionSize
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
README.md readme summary
↓ download 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 blocker
  • fix.diff — direct-remove in shutdown
  • README.md, VERDICT.md, manifest.json
VERDICT.md verdict full narrative
↓ download raw

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_MKPEERng_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_testable

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