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

v1 netgraph ng_eiface (and ng_iface): node constructors call if_attach()/ether_ifattach() from netisr control path - deterministic panic (self-deadlock on non-INVARIANTS)

Summary

Every ng_eiface node creation panics the stock kernel: 'panic: try holding ifnet lock in netisr' from ifnet_lock <- if_attach <- ether_ifattach_bpf <- ng_eiface_constructor+0xf6 (first-hand capture, DWARF-confirmed). Netgraph control messages execute on a TD_TYPE_NETISR thread, but if_attach() takes the ifnet lock whose helper asserts td_type != TD_TYPE_NETISR and calls netmsg_service_sync() which cannot run from netisr - production kernels get a hang/self-deadlock instead. ng_iface carries the same latent calls (if_attach at :605; if_detach at :801 in rmnode - the shutdown path could never have worked either), demonstrated once DF-3485 is fixed. This corrects the DF-3482 lead: the eiface panic is NOT 'free NULL pointer'. Root-only; these stock modules are crash-on-sight and cannot ever have worked on kernels carrying the ifnet-lock netisr assertion. Fix validated (both modules; ifnet attach/detach moved into short-lived kernel threads with a spinlock ownership-transfer state machine): both mkpeer triggers succeed, clean shutdown, 22+ create/destroy cycles, zero panics.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3486 Β· 13 files
FileTypeDescriptionSize
trigger.sh β€” 494 B view raw
run.sh β€” 174 B view raw
build.sh β€” 223 B view raw
panic.txt β€” 1.9 KB view raw
panic.iface_path.txt β€” 1.2 KB view raw
run.log β€” 1.2 KB view raw
fix_build.log β€” 1.7 KB view raw
fix_run.log β€” 2.2 KB view raw
unpriv_check.log β€” 506 B view raw
env.txt β€” 620 B view raw
fix.diff β€” 8.6 KB view raw
VERDICT.md β€” 3.7 KB ↓ raw
verdict.json β€” 3.9 KB view raw
VERDICT.md
↓ download raw

DF-3486 VERDICT β€” REPRODUCED (panic, deterministic, root-only)

Bottom line

Every creation of an ng_eiface node panics the stock kernel: panic: try holding ifnet lock in netisr from ifnet_lock() ← if_attach() ← ether_ifattach_bpf() ← ng_eiface_constructor+0xf6. The constructors of the v1 netgraph interface nodes run in the netgraph control-message path, which executes on a TD_TYPE_NETISR thread; if_attach() is structurally forbidden there (ifnet lock KASSERT at sys/net/if.c:3787, plus netmsg_service_sync() at if.c:741 which would self-deadlock). ng_iface carries the same latent defect (ng_iface.c:605 if_attach, ng_iface.c:801 if_detach in rmnode) β€” hidden on the stock kernel only because DF-3485 panics earlier; demonstrated directly in panic.iface_path.txt.

How it was proven

  1. Live reproduction, stock modules, fresh boot: ngctl mkpeer . eiface e0 ether β†’ instant panic; full trace captured first-hand (panic.txt). Deterministic on every attempt.
  2. The DF-3482 lead's recorded message for this panic ("trying to free NULL pointer") was shown to be a truncation artifact: the actual console message and the two ifnet_lock frames were captured intact here.
  3. Offsets resolved via module DWARF: +0xf6 = return address of the call ether_ifattach at +0xf1 (ng_eiface.c:356).
  4. ng_iface variant reproduced with a DF-3485-only-fixed module (panic. iface_path.txt) β€” proving both files are affected and that ng_iface_rmnode's if_detach is a further instance.
  5. Fix validated (module rebuilds only): with fix.diff, both mkpeer triggers SUCCEED, ng0/ngeth0 appear in ifconfig, ngctl shutdown is clean, and 22+ rapid create/destroy cycles (exercising PENDING/RUNNING/DONE/DYING/DETACHING transitions) produced zero panics; the guest stayed up (fix_run.log).

Reachability / severity

Root-only (ng_socket.c:172 caps_priv_check(SYSCAP_RESTRICTEDROOT); EPERM verified for uid 1001). Impact: deterministic kernel panic β€” on INVARIANTS kernels; on production kernels a netisr self-deadlock (hang) instead. No memory corruption. Severity Low. These v1 modules are effectively unusable crash-on-sight as shipped (they cannot have worked on any kernel containing the ifnet_lock netisr assertion), i.e. this is long-standing rot in the legacy v1 netgraph interface nodes, not a regression.

Fix notes / residual

  • The fix moves ifnet attach/detach into short-lived kernel threads with an ownership-transfer state machine (spinlock-protected). Validated for create / list / ifconfig / connect-hook / shutdown / rapid cycling.
  • Not exercised by ngctl: ng_eiface_rmnode's DONE-state path (ngctl's node-ID addressing did not resolve for the unnamed eiface node); that path is the unchanged original down-block, now merely gated on attach_state == DONE, and its state machine is identical in structure to ng_iface's validated one.
  • Data through hooks racing an in-flight attach (rcvdata before if_attach completes) is a pre-existing semantic gap in these modules; the fix does not widen it (the node is hook-connectable only after the constructor returns, and ifnet fields are initialized before the thread starts).
  • Upstream may prefer outright retirement of the v1 iface/eiface modules (netgraph7 provides equivalents); the fix demonstrates the defect is module-local and repairable.

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
baseline reproduced→ patch + rebuild →patched clean

Both fixed modules loaded on the stock kernel: ng_iface and ng_eiface mkpeer triggers that panicked instantly now SUCCEED (ng0/ngeth0 created and visible in ifconfig/ngctl), ngctl shutdown is clean (deferred if_detach thread), and 22+ rapid create/destroy cycles exercising all attach-state transitions produced zero panics with the guest staying up.

fix_run.log; fix_build.log; panic.txt vs fix_run.log (baseline panic -> success)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (stock kernel; fix applied as module rebuilds only)

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (full first-hand stock trace); panic.iface_path.txt (ng_iface variant proving both files affected); fix_run.log (fixed lifecycle: IFACE-CREATED/EIFACE-CREATED, ifconfig ng0/ngeth0, clean shutdown, race stress 0 panics); fix_build.log; unpriv_check.log (EPERM uid 1001)

PoC changes

none needed β€” plain ngctl reproduces on the stock kernel; trigger is 3 lines

Verified recommended fix

Defer the ifnet side of v1 iface/eiface node lifecycle to short-lived kernel threads (spinlock ownership-transfer state machine), never calling if_attach()/if_detach() from the netisr control path β€” see fix.diff.

Verdict

Deterministic kernel panic on every ng_eiface node creation: 'panic: try holding ifnet lock in netisr' from ifnet_lock() <- if_attach() <- ether_ifattach_bpf() <- ng_eiface_constructor+0xf6 (ng_eiface.c:356). Netgraph control messages run on a TD_TYPE_NETISR thread; if_attach() cannot run there (ifnet lock KASSERT sys/net/if.c:3787 and netmsg_service_sync() at if.c:741 which would self-deadlock β€” so on non-INVARIANTS kernels this is a hang rather than a panic). ng_iface carries the same latent calls (ng_iface.c:605 if_attach, ng_iface.c:801 if_detach in ng_iface_rmnode), demonstrated directly once DF-3485 is fixed. Root-only trigger (ng_socket.c:172). This also corrects the DF-3482 lead: the eiface panic is NOT 'trying to free NULL pointer' β€” that was inferred from a truncated console window. fix.diff moves the ifnet attach/detach into short-lived kernel threads with a spinlock ownership-transfer state machine; validated: both triggers succeed, ng0/ngeth0 appear in ifconfig, clean shutdown, 22+ rapid create/destroy cycles, zero panics.