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

NULL-dereference panic in ng_ether_input/input_orphan/output when IFP2NG(ifp) NULL

Summary

Three if_ethersubr callbacks ng_ether_input(:206) ng_ether_input_orphan(:222) ng_ether_output(:260) unconditionally dereference IFP2NG(ifp) as node->private no NULL check. Sibling ng_ether_detach correctly guards if(node==NULL)return (:326). ng_ether_attach can fail early without setting IFP2NG on ng_make_node_common failure(:289) or M_NOWAIT priv kmalloc failure(:296). ether_ifattach ignores failure interface comes up IFF_RUNNING no node. MOD_LOAD sets ng_ether_input_p unconditionally(:696) before per-interface attach loop(:700-704) leaving MP window. Memory pressure from unpriv fork/mmap bomb causes attach M_NOWAIT failure next packet panics. Local DoS only page 0 not mappable.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2587 Β· 12 files
FileTypeDescriptionSize
harness_mod.c trigger-source diagnostic kld: sets IFP2NG(vtnet0)=NULL (post-race/failed-attach state) under ifnet_lock 2.7 KB view raw
poc.c trigger-source userspace detach-race attempt (create/flood/destroy tap) β€” narrow window, did not panic 3.5 KB view raw
build.sh build-script cc -O0 -o poc poc.c 118 B view raw
run.sh run-script userspace race loop 237 B view raw
build.log build-log build outputs 85 B view raw
run.log run-log unpatched harness+ping -> panic at ng_ether_output+0x7 387 B view raw
fix_run.log fix-validation patched: harness+ping -> 2/2 ping success, no panic, guest up 540 B view raw
fix_results.txt fix-validation before/after contrast 540 B view raw
panic.txt panic-signature Fatal trap 12, fault va=0x28, ng_ether_output+0x7 movq 0x28(%rax) 162 B view raw
env.txt environment uname + cc + patched ng_ether.ko sha 325 B view raw
fix.diff suggested-fix add if(node==NULL) guard to ng_ether_input/input_orphan/output (matches detach :326) 1.3 KB view raw
VERDICT.md verdict full narrative 4.3 KB ↓ raw
VERDICT.md verdict full narrative
↓ download raw

DF-2587 β€” ng_ether NULL-deref in input/input_orphan/output callbacks

Verdict: REPRODUCED (NULL-deref panic); fix_status = fixed

Bug

The three if_ethersubr callbacks registered by ng_ether dereference IFP2NG(ifp) as node->private with no NULL check: - ng_ether_input (sys/netgraph/ether/ng_ether.c:206-207) - ng_ether_input_orphan (sys/netgraph/ether/ng_ether.c:222-223) - ng_ether_output (sys/netgraph/ether/ng_ether.c:260-261)

const node_p node = IFP2NG(ifp);
const priv_p priv = node->private;     /* NULL-deref if node == NULL */

The sibling ng_ether_detach (:321) does guard: if (node == NULL) return; (:326). IFP2NG(ifp) (((struct arpcom *)ifp)->ac_netgraph) is NULL on a live interface when: - ng_ether_attach fails before setting it (ng_make_node_common failure :289, or the M_NOWAIT priv kmalloc failure :296-301) β€” needs memory pressure; or - ng_ether_detach nulls it (:331) during interface teardown β€” a race with in-flight traffic.

Once ng_ether is kldload'd, ng_ether_input_p/ng_ether_output_p are set GLOBALLY (ng_ether.c:692-696), so every ethernet RX/TX packet on every interface is routed through these callbacks (if_ethersubr.c:389, :1273-1279, :1160-1177). An interface with IFP2NG==NULL therefore panics on the next packet.

Reproduction

The detach race and the attach-failure (memory-pressure) paths are hard to hit deterministically from userspace: ifconfig tapX destroy returns EBUSY while the tap fd is open (verified), so continuous flooding during detach is not possible, and netisr drains write-bursts before destroy nulls IFP2NG. ~4400 create/flood/destroy race cycles produced no panic (the window is narrow).

So the NULL-deref primitive is confirmed at the function level with a tiny diagnostic module (harness_mod.c) that puts a live interface into the exact post-race / failed-attach state β€” IFP2NG(vtnet0) := NULL β€” and then a normal packet exercises the unguarded deref. This is the same harness-style primitive confirmation used for race/memory-pressure-gated findings.

Unpatched #0 kernel: kldload ng_ether then kldload df2587_harness.ko (sets IFP2NG(vtnet0)=NULL) then ping -c1 10.0.2.2 (an outgoing packet β†’ ether_output β†’ ng_ether_output) β‡’ panic:

Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x28
instruction pointer    = 0x8:0xffffffff82600247
current process        = Idle
Stopped at ng_ether_output+0x7:  movq 0x28(%rax),%rax

0x28 is exactly the offset of private in struct ng_node; movq 0x28(%rax) with %rax==0 is the node->private load at ng_ether.c:261 with node==NULL. This is the precise line the finding cites.

(The harness itself must take ifnet_lock() around ifunit()/the write, since ifunit() asserts mtx_owned(&ifnet_mtx) β€” sys/net/if.c:1946. An early harness build that omitted the lock tripped panic: ifnet is not locked first; the final harness_mod.c locks correctly.)

Fix

fix.diff adds if (node == NULL) return; to all three callbacks β€” the same guard ng_ether_detach already has (:326). For ng_ether_input_orphan the NULL path also m_freem(m) (it owns the mbuf); ng_ether_output returns 0 (let the packet continue, matching the "upper hook not connected" path).

Fix validation

Built the patched ng_ether.ko (make in sys/netgraph/ether, rc=0), installed to /boot/kernel/ng_ether.ko (sha e8f14d5f…), reloaded. Re-ran the SAME harness + ping:

PATCHED: kldload ng_ether; kldload df2587_harness.ko (IFP2NG(vtnet0)=NULL)
         ping -c2 10.0.2.2: 2/2 packets, 0.0% loss, PING_EXIT=0, guest UP

No panic. The NULL check returns 0 in ng_ether_output, the packet passes through normally. Same workload that panicked the unpatched module now succeeds. fix_status = fixed.

Impact

Kernel NULL-deref β†’ panic (DoS). Reachable when an ethernet interface has IFP2NG==NULL while traffic flows: via ng_ether_attach failure (memory pressure) or a teardown race (interface destroyed with in-flight packets). Requires ng_ether to be loaded (root kldload) AND either condition. No memory-corruption primitive is attacker-shaped (it is a NULL deref, not a controllable write) β€” pure DoS. Low severity is appropriate (module must be loaded; trigger needs a race or memory pressure).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. Built patched ng_ether.ko (make in sys/netgraph/ether, rc=0; NULL check present in all three callbacks), installed to /boot/kernel/ng_ether.ko, reloaded. Re-ran SAME harness + ping: unpatched panicked at ng_ether_output+0x7 (NULL deref, fault va=0x28); patched returned 0 from ng_ether_output (packet passed through) β€” ping 2/2 success, guest UP, no panic. NULL check closes the bug.

baseline (original ng_ether.ko): harness(IFP2NG=NULL)+ping -> panic 'ng_ether_output+0x7 movq 0x28(%rax),%rax'. patched ng_ether.ko (e8f14d5f...): same harness+ping -> '64 bytes from 10.0.2.2 ... 2 packets transmitted, 2 received, 0.0% loss', PING_EXIT=0, guest up.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 with patched ng_ether.ko (sha256 e8f14d5fbb08f7b355b4fe14bf01fc0e7a6d5b3b37c89e93167c861d359353cf; rebuilt standalone via make in sys/netgraph/ether)

Confirmed kernel references

Detail

Exploit chain

none β€” NULL dereference (panic/DoS), not controllable write; no escalation primitive.

Evidence (decisive lines)

Unpatched: kldload ng_ether + kldload df2587_harness.ko (IFP2NG(vtnet0)=NULL) + ping -c1 10.0.2.2 -> Fatal trap 12 page fault, fault virtual address=0x28, instruction pointer=0x8:0xffffffff82600247, current process=Idle, 'Stopped at ng_ether_output+0x7: movq 0x28(%rax),%rax'. Patched: same setup -> ping 2/2 success, guest UP, no panic.

PoC changes

Authored harness_mod.c (diagnostic kld: sets IFP2NG(vtnet0)=NULL under ifnet_lock, then any packet triggers the deref) because live detach-race too narrow on this guest (EBUSY-on-open-fd). poc.c is userspace detach-race attempt (create/flood/destroy tap) that did not hit. fix.diff adds if(node==NULL){return/return 0;} to all three callbacks, matching detach guard (:326); input_orphan's NULL path also m_freem(m) since it owns the mbuf.

Verified recommended fix

Add 'if (node == NULL) return;' (return 0 for ng_ether_output; m_freem+return for ng_ether_input_orphan which owns the mbuf) to all three callbacks in sys/netgraph/ether/ng_ether.c β€” same guard ng_ether_detach already has at :326. Matches finding's 'NULL check' proposal. Full git-apply-able diff in findings/poc/DF-2587/fix.diff.

Verdict

REPRODUCED (NULL-deref panic). ng_ether_input (:206-207), ng_ether_input_orphan (:222-223) and ng_ether_output (:260-261) all do 'const node_p node = IFP2NG(ifp); const priv_p priv = node->private;' with NO NULL check, unlike ng_ether_detach which guards 'if(node==NULL)return;' (:326). IFP2NG(ifp) is NULL on a live interface via ng_ether_attach failure (:289/:296, needs memory pressure) or a detach race (:331, interface destroyed with in-flight traffic). Once ng_ether is kldload'd, ng_ether_input_p/output_p set GLOBALLY (:692-696) so every ethernet RX/TX packet routes through these callbacks. Detach race narrow on this guest (ifconfig tapX destroy returns EBUSY while tap fd open, so continuous flooding during detach impossible; ~4400 create/flood/destroy cycles produced no panic), so NULL-deref primitive confirmed at function level with harness_mod.c which sets IFP2NG(vtnet0)=NULL (exact post-race/failed-attach state, under ifnet_lock) and then normal ping exercises it. Unpatched: panic 'Stopped at ng_ether_output+0x7: movq 0x28(%rax),%rax' (fault va=0x28 = offset of 'private' in ng_node; node==NULL) β€” exact line finding cites.