ip_input/ip_output called from netgraph thread context: ASSERT_NETISR_NCPUS panic (INVARIANTS) and PFIL list UAF (production)
Summary
ng_ipfw_rcvdata :245 ip_output(m) :248 ip_input(m) called from netgraph worker thread (netgraph N created ng_base.c:2787-2789). Netgraph thread msgports NOT in netreglist (only netmsg_service_port_init ports are netisr.c:258-280). INVARIANTS: ASSERT_NETISR_NCPUS(mycpuid) at ip_input.c:460 ip_output.c:185 trips immediately = deterministic panic. Production: pfil_run_hooks ip_input.c:631 ip_output.c:529 iterates pfil list synced by netmsg_service_sync (pfil.c:238,346) which only walks netreglist NOT netgraph ports. Concurrent pfil_add_hook/remove_hook (pfil.c:244-247,351-354) frees old list while netgraph thread still in TAILQ_FOREACH = UAF on pfil_list_t and struct packet_filter_hook. Sibling ng_ip_input.c:122-125 correct pattern: netisr_queue(NETISR_IP,m) never calls ip_input directly. LATENT: optional netgraph7_ipfw not in default kernel. Trigger: root ngctl connect to ipfw node + m_tag NGM_IPFW_COOKIE dir=IN/OUT. Fix: use netisr_queue dispatch to real netisr thread.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0735 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df735_trigger.c | trigger-source | sysctl trigger: ip_input(m) direct -- mirrors ng_ipfw.c:248, PANICS | 2.8 KB | view raw |
| df735_fixed.c | trigger-source | sysctl trigger: netisr_queue(NETISR_IP, m) -- mirrors fix.diff + ng_ip_input.c:125, NO panic | 2.0 KB | view raw |
| ng_df735_poc.c | trigger-source | faithful netgraph node mirroring ng_ipfw_rcvdata line-for-line (builds; netgraph7 ABI mismatch blocks kldload) | 4.9 KB | view raw |
| ng_df735_inject.c | trigger-source | userspace PF_NETGRAPH injector for ng_df735_poc | 4.2 KB | view raw |
| Makefile.ng_ipfw | build-script | Makefile template for the real (unbuildable) ng_ipfw.ko | 723 B | β download |
| trigger_df735.sh | build-script | alternative trigger via real ng_ipfw.ko + ipfw ngtee divert | 3.0 KB | view raw |
| run_df735.sh | build-script | alternative trigger via faithful netgraph node + ng_socket | 6.0 KB | view raw |
| build.sh | build-script | builds df735_trigger.ko, df735_fixed.ko, ng_df735_poc.ko | 1.3 KB | view raw |
| run.sh | run-script | Phase A (fixed, no panic) + Phase B (buggy, PANIC) on the same kernel | 1.8 KB | view raw |
| build.log | build-log | full compiler output of the final three-module build (gcc 8.3) | 2.7 KB | view raw |
| run.log | run-log | Phase B decisive run + serial console panic signature | 409 B | view raw |
| fix_run.log | run-log | Phase A (fixed) run -- 5 triggers, no panic, guest stays up | 609 B | view raw |
| panic.txt | panic-signature | panic: thread ... is not within netisr_ncpus 6 / ip_input+0x3de | 243 B | view raw |
| env.txt | environment | uname, cc version, kldstat, sysctls | 483 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff: route ng_ipfw rcvdata through netisr (matches ng_ip_input.c + dummynet pattern) | 2.8 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability (latent), fix validation | 10.5 KB | β raw |
| README.md | readme | reproduction instructions and file index | 3.2 KB | β 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-0735 β PoC and evidence pack
Finding: ip_input/ip_output called from netgraph thread context
(sys/netgraph7/ng_ipfw.c:245,248). The rcvdata callback calls
ip_output(m) / ip_input(m) directly from the netgraph worker thread,
which is not a netisr thread, so the ASSERT_NETISR_NCPUS(mycpuid) at
ip_input.c:460 / ip_output.c:185 fires on default-GENERIC (INVARIANTS on)
and the kernel panics.
Verdict: REPRODUCED at source + harness level. The cited path is
latent β ng_ipfw.c doesn't compile on this tree (netinet/ip_fw.h
does not exist), has no in-kernel caller of its ng_ipfw_input_p function
pointer, and is neither in the default kernel nor in the netgraph7 module
SUBDIR. The harness demonstrates the assertion mechanism that the cited
call would trip if it were ever reached.
Impact: panic / DoS, root-only reachability, no escalation primitive.
How to reproduce (on the DragonFly guest as root)
# 1. Build the three harness modules.
./build.sh
# 2. Run the two-phase demonstration (Phase A: fixed, no panic;
# Phase B: buggy, PANIC -> guest wedges in DDB).
./run.sh
After Phase B the guest is wedged in DDB; reset it from the host with
./dfbsd-qemu/vm.sh reset with-src. The panic signature is in
dfbsd-qemu/boot.log and in panic.txt.
Files
| file | purpose |
|---|---|
df735_trigger.c |
sysctl trigger module: ip_input(m) direct (mirrors ng_ipfw.c:248) β PANICS |
df735_fixed.c |
sysctl trigger module: netisr_queue(NETISR_IP, m) (mirrors the fix + ng_ip_input.c:125) β no panic |
ng_df735_poc.c |
faithful netgraph node harness (mirrors ng_ipfw_rcvdata) β builds; netgraph7 ABI mismatch prevents kldload |
ng_df735_inject.c |
userspace PF_NETGRAPH injector (for ng_df735_poc) |
Makefile.ng_ipfw |
Makefile template for building the real (unbuildable) ng_ipfw.ko from source |
trigger_df735.sh |
alternative trigger via the real (unbuildable) ng_ipfw.ko + ipfw ngtee divert |
run_df735.sh |
alternative trigger via the faithful netgraph node |
build.sh |
builds all three harness modules |
run.sh |
runs Phase A (fixed, no panic) + Phase B (buggy, PANIC) |
build.log |
full compiler output of the final build |
run.log |
full output of the decisive Phase B run (with panic signature) |
fix_run.log |
full output of the Phase A (fixed) run (no panic) |
panic.txt |
panic signature excerpt from dfbsd-qemu/boot.log |
env.txt |
guest environment (uname, cc, kldstat, sysctls) |
fix.diff |
git-apply-able unified diff against sys/netgraph7/ng_ipfw.c |
VERDICT.md |
full narrative verdict (mechanism, reachability, fix validation) |
manifest.json |
machine-readable artifact catalog for the static-site renderer |
DF-0735 β ip_input/ip_output called from netgraph thread context
Verdict: REPRODUCED at the source level + harness level (latent / dead-code path).
Impact: panic / DoS β but only reachable via root (kldload of an out-of-tree module) on a configuration that is not built by default. No unprivileged-reachability, no escalation primitive.
Fix: VALIDATED via harness β the fix pattern (route via netisr_queue) eliminates the panic.
1. The claim, restated
sys/netgraph7/ng_ipfw.c calls the IP layer directly from the netgraph worker
thread:
ng_ipfw.c:245βreturn ip_output(m, NULL, NULL, IP_FORWARDING, NULL, NULL);ng_ipfw.c:248βip_input(m);
These calls run inside ng_ipfw_rcvdata() (the netgraph rcvdata callback at
ng_ipfw.c:220). Because the hook is set with NG_HOOK_FORCE_QUEUE(hook) in
ng_ipfw_connect (ng_ipfw.c:182-187), data sent to the hook is queued
asynchronously and processed by the netgraph worker thread created at
sys/netgraph7/netgraph/ng_base.c:2787-2789:
lwkt_create(ngthread, NULL, &td, NULL, 0, i, "netgraph %d", i);
ng_msgport[i] = &td->td_msgport;
That thread is a plain lwkt thread. It is never registered on
netreglist β only ports initialized via netmsg_service_port_init are
(sys/net/netisr.c:258-280, with the TAILQ_INSERT_TAIL(&netreglist, ...)
at line 279). netmsg_service_sync() (the pfil list lifetime barrier at
sys/net/pfil.c:238,346) only walks netreglist, so it does not cover
the netgraph thread.
The first thing ip_input() and ip_output() do is:
sys/netinet/ip_input.c:460βASSERT_NETISR_NCPUS(mycpuid);sys/netinet/ip_output.c:185βASSERT_NETISR_NCPUS(mycpuid);
ASSERT_NETISR_NCPUS(n) (defined at sys/net/netisr2.h:136-139) is a
KASSERT(IN_NETISR_NCPUS(n), ...), i.e. it is compiled in whenever
INVARIANTS is on. The default X86_64_GENERIC config ships
options INVARIANTS (sys/config/X86_64_GENERIC:56), so the assertion is
live on every default DragonFly kernel.
Therefore: the cited call site, executed from the netgraph worker thread,
trips the KASSERT and panics with
panic: thread <ptr> cpuN is not within netisr_ncpus <N>.
The sibling node sys/netgraph7/ng_ip_input.c shows the correct pattern:
NGI_GET_M(item, m);
NG_FREE_ITEM(item);
m->m_flags &= ~M_HASH;
netisr_queue(NETISR_IP, m); /* ng_ip_input.c:125 -- routes to netisr */
2. Reachability assessment (why this is latent)
ng_ipfw.c is dead code on any current DragonFly kernel:
sys/conf/filesgates it onoptional netgraph7_ipfw, which is not insys/config/X86_64_GENERIC, so it is not in the default kernel.- It is not in the
SUBDIRlist ofsys/netgraph7/Makefile, so it is not built as a loadable.koeither. There is nong_ipfw.koanywhere on the running guest (/boot/kernel/and the source tree both lack it). - The file
#includes<netinet/ip_fw.h>(ng_ipfw.c:44), a header that does not exist undersys/netinet/on this tree (the onlyip_fw.hlives atsys/net/ipfw/ip_fw.hwith a different API). Trying to build the file standalone fails immediately withfatal error: netinet/ip_fw.h: No such file or directory. - Even if those issues were fixed, no in-kernel caller ever invokes
ng_ipfw_input_p(the function pointer registered atng_ipfw.c:118inMOD_LOAD). It is referenced only insideng_ipfw.candng_ipfw.hthemselves. Neithersys/net/ipfw/ip_fw2.cnorsys/net/ipfw3/*mention it. So even with the module loaded, no packet path would ever reachng_ipfw_rcvdata.
This is therefore a latent / dead-code finding. The mechanism is real at the source level; the cited path is unreachable on any running DragonFly kernel without substantial additional (root-only) scaffolding.
3. Demonstration
Because the cited code cannot be compiled or reached, the proof uses a
deterministic code-level harness that mirrors ng_ipfw.c:248 exactly
(calling ip_input(m) directly from a non-netisr thread). This is the
acceptable path called out in the assignment.
Two trigger modules were built (both compile cleanly with the guest gcc 8.3):
| module | what its trigger does | matches | expected |
|---|---|---|---|
df735_trigger.ko |
sysctl handler calls ip_input(m) directly from the invoking thread |
mirrors ng_ipfw.c:248 direct call |
PANIC |
df735_fixed.ko |
sysctl handler calls netisr_queue(NETISR_IP, m) instead |
mirrors the fix.diff pattern (and ng_ip_input.c:125) |
NO panic |
ng_df735_poc.ko |
real netgraph node whose rcvdata calls ip_input(m) |
faithful copy of ng_ipfw_rcvdata |
builds; netgraph7 ABI mismatch prevents kldload |
The sysctl-handler thread, the netgraph worker thread, and a kthread are all
non-netisr threads, so the ASSERT_NETISR_NCPUS behavior is identical
across them. The trigger module is therefore a faithful demonstration of the
cited mechanism.
3a. BASELINE β buggy module panics
Run on the unpatched audit-source kernel
(DragonFly 6.5-DEVELOPMENT #0, with-src snapshot):
# kldload /root/df735/df735_trigger.ko # sysctl -n debug.df735_trigger=1 0 (kernel panics, guest enters DDB)
Serial console (dfbsd-qemu/boot.log):
panic: thread 0xfffff800905fd480 cpu1 is not within netisr_ncpus 6 cpuid = 1 ip_input() at ip_input+0x3de 0xffffffff807b0dae ip_input() at ip_input+0x3de 0xffffffff807b0dae Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip) db>
The panic message is the exact expansion of
KASSERT(IN_NETISR_NCPUS(n), ("thread %p cpu%d is not within netisr_ncpus %d",
...)) from sys/net/netisr2.h:138, and the backtrace shows the panic
originates inside ip_input β confirming the cited ip_input.c:460
assertion. (ip_input+0x3de is the post-KASSERT-fault panic return path.)
3b. FIXED module β no panic
Run on the same unpatched kernel (the fix pattern is in the module, not the kernel):
# kldload /root/df735/df735_fixed.ko # for i in 1 2 3 4 5; do sysctl -n debug.df735_fixed=1; done 0 -> 0 0 -> 0 0 -> 0 0 -> 0 0 -> 0 # uptime 2:55AM up 4 mins, 0 users, load averages: 0.01, 0.02, 0.00
Five triggers, no panic, guest stays up. The mbuf is dispatched to the
NETISR_IP thread; ip_input runs there, where ASSERT_NETISR_NCPUS
holds.
4. Exploit chain / escalation
There is no escalation chain β this is not a memory-corruption primitive.
The bug produces a deterministic kernel panic / DoS, gated behind root-only
setup (kldload of an out-of-tree module to even reach the cited path). The
production (INVARIANTS-OFF) concern raised in the finding β that
pfil_run_hooks in ip_input/ip_output iterates a list whose lifetime is
synced via netmsg_service_sync (which does not cover the netgraph
thread), so concurrent pfil_add_hook/pfil_remove_hook could free the list
out from under the netgraph thread β is real in theory but only reachable
on a configuration that:
(a) compiles and loads ng_ipfw (impossible today without fixing the
netinet/ip_fw.h include),
(b) wires ng_ipfw_input_p into ipfw2/ipfw3 (no such caller exists),
(c) runs an attacker-controlled concurrent pfil hooks add/remove storm.
None of those hold on any shipping DragonFly kernel. There is no unprivileged attack surface.
5. fix.diff
fix.diff patches sys/netgraph7/ng_ipfw.c to route through netisr instead
of calling ip_input/ip_output directly:
- adds
#include <net/netisr.h>and<net/netmsg2.h>, - adds
ng_ipfw_ip_output_dispatch()netmsg handler (mirrors dummynet'sip_dn_ip_outputatsys/net/dummynet/ip_dummynet_glue.c:270), - in
ng_ipfw_rcvdata(), theNG_IPFW_OUTcase dispatches the mbuf to a netisr port vialwkt_sendmsg(netisr_cpuport(mycpuid), ...)and callsip_outputfrom there (mirrors dummynet'sip_dn_queuepattern atsys/net/dummynet/ip_dummynet_glue.c:82-99), - the
NG_IPFW_INcase callsnetisr_queue(NETISR_IP, m), matching the siblingsys/netgraph7/ng_ip_input.c:125pattern exactly.
The diff applies cleanly with patch -p1 --forward (all 4 hunks succeed).
It is minimal and targeted: the only behavior change is where ip_input /
ip_output are called (which thread), not what they do.
Fix validation (Phase 8)
The default kernel does not link ng_ipfw.c, so the standard "single-fix
kernel + reboot" Phase 8 path does not apply (no kernel objects change). The
fix is validated at the harness level instead, by demonstrating that the
fix pattern (route via netisr) eliminates the panic the buggy pattern
(direct call) produces, on the same kernel:
| Phase | Module | Outcome |
|---|---|---|
| baseline (buggy mechanism) | df735_trigger.ko |
PANIC panic: thread ... is not within netisr_ncpus 6 at ip_input+0x3de |
| patched (fix mechanism) | df735_fixed.ko |
NO panic, 5x triggers, guest stays up |
This is a clean before/after contrast on the same 6.5-DEVELOPMENT #0
audit-source kernel. fix_status = fixed.
6. Honest impact summary
| dimension | value |
|---|---|
| bug class | wrong-thread call β assertion panic (INVARIANTS) / lock-context race (production) |
| reachability | latent: cited code is dead on this tree (won't compile, no caller, not in default kernel or module SUBDIR) |
| preconditions | root must kldload an out-of-tree module (and even then, no caller exists to feed packets in) |
| unprivβroot chain? | No β no memory-corruption primitive, no unprivileged reachability. |
| realistic impact | source-level latent bug; would become a real root-triggerable DoS if the netgraph7_ipfw path were ever wired up |
| confidence | certain (source-level mechanism verified + harness panic confirmed) |
The finding is real as a code-quality / latent-bug issue. It is not an exploitable vulnerability on any shipping DragonFly configuration.
Fix verification
fixedVALIDATED via harness on the unpatched 6.5-DEVELOPMENT #0 audit-source kernel: the buggy mechanism (df735_trigger.ko, mirrors sys/netgraph7/ng_ipfw.c:248 direct ip_input(m) call) panics every time with 'thread
BASELINE (buggy mechanism, mirrors ng_ipfw.c:248):
kldload df735_trigger.ko; sysctl -n debug.df735_trigger=1
-> panic: thread 0xfffff800905fd480 cpu1 is not within netisr_ncpus 6
ip_input() at ip_input+0x3de 0xffffffff807b0dae
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db> (guest wedges; status=down)
FIXED (mirrors fix.diff NG_IPFW_IN branch + ng_ip_input.c:125):
kldload df735_fixed.ko; for i in 1 2 3 4 5; do sysctl -n debug.df735_fixed=1; done
-> 0 -> 0 (x5)
-> uptime: 2:55AM up 4 mins, 0 users, load averages: 0.01, 0.02, 0.00
-> status: up (no panic)
Confirmed kernel references
- sys/netgraph7/ng_ipfw.c:245
- sys/netgraph7/ng_ipfw.c:248
- sys/netgraph7/ng_ipfw.c:182
- sys/netgraph7/ng_ipfw.c:220
- sys/netgraph7/netgraph/ng_base.c:2787
- sys/netgraph7/ng_ip_input.c:125
- sys/netinet/ip_input.c:460
- sys/netinet/ip_output.c:185
- sys/net/netisr2.h:136
- sys/net/netisr.c:258
- sys/net/netisr.c:292
- sys/net/pfil.c:110
- sys/net/pfil.c:238
- sys/net/pfil.c:346
- sys/net/dummynet/ip_dummynet_glue.c:82
- sys/net/dummynet/ip_dummynet_glue.c:270
- sys/config/X86_64_GENERIC:56
- sys/conf/files:1712
Detail
Exploit chain
none (not a memory-corruption primitive). This is a deterministic kernel panic / DoS via wrong-thread assertion, gated behind root-only module loading to even reach the cited path. No unprivileged reachability, no privilege-boundary crossing, no escalation chain derivable. The production (INVARIANTS-OFF) concern raised in the finding -- pfil_run_hooks (sys/net/pfil.c:110) iterating a pfil list whose lifetime is synced via netmsg_service_sync (pfil.c:238,346), a sync that does NOT cover the netgraph thread, so concurrent pfil_add_hook/pfil_remove_hook could free the list out from under the netgraph thread -- is real in theory but unreachable today because no caller ever invokes ng_ipfw_input_p.
Evidence (decisive lines)
Phase B baseline (buggy mechanism, mirrors ng_ipfw.c:248):
kldload /root/df735/df735_trigger.ko
sysctl -n debug.df735_trigger=1
-> panic: thread 0xfffff800905fd480 cpu1 is not within netisr_ncpus 6
cpuid = 1
ip_input() at ip_input+0x3de 0xffffffff807b0dae
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>
(guest wedges in DDB; status=down)
Phase A fixed mechanism (mirrors fix.diff + ng_ip_input.c:125):
kldload /root/df735/df735_fixed.ko
for i in 1 2 3 4 5; do sysctl -n debug.df735_fixed=1; done
-> 0 -> 0 (x5)
-> uptime: 2:55AM up 4 mins, 0 users, load averages: 0.01, 0.02, 0.00
-> guest stays up; status=up
(Both phases run on the same unpatched 6.5-DEVELOPMENT #0 audit-source kernel.)
PoC changes
Authored the entire evidence pack from scratch (findings/poc/DF-0735/ did not exist when I started). Wrote: df735_trigger.c (sysctl module mirroring ng_ipfw.c:248 -- calls ip_input(m) directly from a non-netisr thread; PANICS), df735_fixed.c (sysctl module using the fix pattern -- netisr_queue(NETISR_IP, m); no panic), ng_df735_poc.c (faithful netgraph node mirroring ng_ipfw_rcvdata line-for-line; builds cleanly, but netgraph7 ABI version mismatch blocks kldload on this kernel), ng_df735_inject.c (PF_NETGRAPH userspace injector), Makefile.ng_ipfw (template for the real -- unbuildable -- ng_ipfw.ko), build.sh + run.sh (two-phase reproduce), VERDICT.md, README.md, manifest.json, fix.diff. Fixed a header typo (netinet/in_system.h -> netinet/in_systm.h) and added an explicit extern void ip_input(struct mbuf *); because DragonFly's ip_input() is not declared in any public header.
Verified recommended fix
fix.diff routes ng_ipfw_rcvdata's NG_IPFW_IN case through netisr_queue(NETISR_IP, m) (matching the sibling sys/netgraph7/ng_ip_input.c:125 exactly), and the NG_IPFW_OUT case through a netmsg_packet dispatched to netisr_cpuport(mycpuid) with a new ng_ipfw_ip_output_dispatch() handler that calls ip_output() from the netisr thread (mirroring dummynet's ip_dn_queue/ip_dn_ip_output pattern at sys/net/dummynet/ip_dummynet_glue.c:82-99,270). Adds
Verdict
REPRODUCED at source + harness level (LATENT/dead-code path). The cited call site at sys/netgraph7/ng_ipfw.c:245 (ip_output(m)) and :248 (ip_input(m)) is real: ng_ipfw_rcvdata runs in the netgraph worker thread created at sys/netgraph7/netgraph/ng_base.c:2787-2789, which is NOT registered on netreglist (only ports initialized via netmsg_service_port_init at sys/net/netisr.c:258-280 are), so it is not a netisr thread. ip_input.c:460 and ip_output.c:185 both open with ASSERT_NETISR_NCPUS(mycpuid) (defined at sys/net/netisr2.h:136, a KASSERT compiled in whenever INVARIANTS is on -- and X86_64_GENERIC ships options INVARIANTS at sys/config/X86_64_GENERIC:56). A harness module (df735_trigger.ko) that mirrors ng_ipfw.c:248 by calling ip_input(m) directly from a non-netisr thread panics deterministically: 'panic: thread 0xfffff800905fd480 cpu1 is not within netisr_ncpus 6' at ip_input+0x3de, confirmed in dfbsd-qemu/boot.log and panic.txt. The mechanism is identical whether the caller is a sysctl-handler thread, a kthread, or the netgraph worker thread -- all are non-netisr, all trip the same KASSERT. The cited path itself is LATENT on this tree: ng_ipfw.c is gated on optional netgraph7_ipfw (sys/conf/files:1712), absent from sys/netgraph7/Makefile SUBDIR, #includes ng_ipfw_input_p function pointer (set at ng_ipfw.c:118) is never referenced by any other .c/.h outside ng_ipfw.{c,h} -- so the rcvdata path is unreachable on any shipping DragonFly kernel.
No comments yet.