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)
PoC verification
Evidence pack
findings/poc/DF-2587 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedVALIDATED. 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.
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.
No comments yet.