Kernel panic via forged mbuf tag with invalid dir value β panic() on attacker-controllable data
Summary
ng_ipfw_rcvdata :251 default: panic("ng_ipfw_rcvdata: bad dir %u",ngit->dir). ngit->dir is 4-byte int in mbuf m_tag identified by cookie=NGM_IPFW_COOKIE type=0. Any privileged netgraph peer can prepend m_tag with that cookie and arbitrary dir value (e.g. 2). Result: instant kernel panic host reboot. No legitimate code path produces dir!=0/1 (ng_ipfw_input() writer has zero callers in tree) but tag is forgeable by netgraph peer. Privileged DoS should drop+log never panic. Fix: m_freem(m) return(EINVAL) + log(LOG_ERR).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0736 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| attack.c | trigger-source | in-kernel attack module: locates ipfw netgraph node, forges NGM_IPFW_COOKIE m_tag with dir=2, invokes node->nd_type->rcvdata(NULL, item) | 4.2 KB | view raw |
| setup.sh | build-setup | rebuilds netgraph.ko+ng_socket.ko from source; patches ng_ipfw.c (symlinks missing <netinet/ip_fw.h>, strips dangling MODULE_DEPEND on ipfw, defines missing ng_ipfw_input_p); builds + loads ng_ipfw.ko | 1.5 KB | view raw |
| build.sh | build-script | builds df736attack.ko against the live /usr/src tree | 522 B | view raw |
| run.sh | run-script | kldloads df736attack.ko -> panic on unfixed / clean return on fixed | 864 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff: replaces panic() at line 251 with log(LOG_ERR,...)+m_freem(m)+return(EINVAL) | 406 B | view raw |
| validate_fix.sh | fix-validation | applies fix.diff, rebuilds patched ng_ipfw.ko, re-runs the attack -> no panic | 2.6 KB | view raw |
| boot_panic.log | run-log | full untrimmed serial console capture of the panic run (dfbsd-qemu/boot.log copy) | 13.9 KB | view raw |
| panic.txt | panic-signature | panic signature: panic: ng_ipfw_rcvdata: bad dir 2 + stack trace | 13.9 KB | view raw |
| panic_sig.txt | panic-signature | trimmed panic signature excerpt | 629 B | view raw |
| fix_run_dmesg.txt | run-log | dmesg excerpt from the patched-kernel re-run: 'bad dir 2, dropping mbuf' + 'rcvdata returned 22' | 457 B | view raw |
| env.txt | environment | guest uname, kern.version, cc --version | 247 B | view raw |
| README.md | readme | human-facing reproduction instructions | 5.1 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability trace, fix, validation | 8.5 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-0736 β Trigger / Reproduction
Claim: panic("ng_ipfw_rcvdata: bad dir %u", ngit->dir) at
sys/netgraph7/ng_ipfw.c:251 is reachable when a netgraph peer delivers an
mbuf carrying a forged m_tag (cookie=NGM_IPFW_COOKIE, type=0) with an
invalid dir value (anything other than NG_IPFW_OUT=0 / NG_IPFW_IN=1).
Reachability model β privileged DoS only
| Path | Status |
|---|---|
ng_ipfw compiled into X86_64_GENERIC |
β not in default config (options netgraph7_ipfw is required) |
ng_ipfw.ko shipped in /boot/kernel |
β not built (missing from sys/netgraph7/Makefile SUBDIR; build also fails due to broken <netinet/ip_fw.h> include and undefined ng_ipfw_input_p symbol) |
In-kernel writer of the m_tag (ng_ipfw_input_p) |
β zero callers in the tree |
Userspace ng_socket forging the m_tag |
β ng_socket data-send path rejects the control mbuf and never adds an m_tag |
ng_socket control attach from unprivileged user |
β requires SYSCAP_RESTRICTEDROOT (root) |
| Forged-m_tag delivery from a custom kernel module | β
requires root (kldload) |
Realistic impact: privileged DoS (root β kernel panic) on a non-default configuration that requires manual source patching to even build. Not an unprivileged privesc.
Reproduction on the unpatched kernel
The bug lives in a non-default module that cannot be built as-is. The
setup.sh + attack.c pair performs the minimal patching needed to build
ng_ipfw.ko from source and triggers the panic.
# Host -> guest:
scp -F dfbsd-qemu/config findings/poc/DF-0736/setup.sh dfbsd:/root/setup.sh
scp -F dfbsd-qemu/config findings/poc/DF-0736/attack.c dfbsd:/root/df736attack/attack.c
scp -F dfbsd-qemu/config findings/poc/DF-0736/fix.diff dfbsd:/root/fix.diff # for the fix-validation pass
# Guest:
sh /root/setup.sh # rebuild + load netgraph, ng_socket, ng_ipfw
(cd /root/df736attack && make) # build attack.ko
kldload /root/df736attack/df736attack.ko # PANIC at sys/netgraph7/ng_ipfw.c:251
Expected output (serial console / dfbsd-qemu/boot.log):
df736: invoking ng_ipfw_rcvdata on node 0xfffff8004f1e8b50 with dir=2 -> expect panic at sys/netgraph7/ng_ipfw.c:251 panic: ng_ipfw_rcvdata: bad dir 2 cpuid = 4 Trace beginning at frame 0xfffff801189bd7a8 ng_ipfw_rcvdata() at ng_ipfw_rcvdata+0xc2 0xffffffff8260b112 df736_modevent() at df736_modevent+0x125 0xffffffff8260c125 module_register_init() at module_register_init+0x49 0xffffffff806232e9 linker_load_file.part.3() at linker_load_file.part.3+0x1b9 0xffffffff80624619 linker_load_module() at linker_load_module+0x116 0xffffffff80625e16 Debugger("panic") Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip) db>
The panic.txt file contains the full signature captured from
dfbsd-qemu/boot.log.
Fix-validation
Apply fix.diff to /usr/src/sys/netgraph7/ng_ipfw.c, rebuild the
standalone ng_ipfw.ko (same setup.sh pattern), and re-load the attack
module. On the fixed kernel:
df736: invoking ng_ipfw_rcvdata on node 0xfffff8004f1e8b50 with dir=2 -> expect panic at sys/netgraph7/ng_ipfw.c:251 ng_ipfw_rcvdata: bad dir 2, dropping mbuf df736: rcvdata returned 22 -- if you see this, the fix is in place
rcvdata returned 22 is EINVAL β the mbuf is freed via m_freem(m) and
the call returns cleanly. No panic.
Files
| File | Purpose |
|---|---|
attack.c |
In-kernel attack module: locates the ipfw netgraph node, builds an mbuf with a forged NGM_IPFW_COOKIE m_tag (dir=2), and invokes node->nd_type->rcvdata(NULL, item) to drive the panic path. |
setup.sh |
Rebuilds netgraph.ko + ng_socket.ko from source (so the ABI matches freshly-built modules), patches ng_ipfw.c (broken <netinet/ip_fw.h> include via symlink, strips the dangling MODULE_DEPEND(ng_ipfw, ipfw, ...), defines the missing ng_ipfw_input_p symbol), builds ng_ipfw.ko, and loads everything. |
validate_fix.sh |
Same as setup.sh but additionally applies fix.diff first; the attack then returns cleanly instead of panicking. |
fix.diff |
Standalone git apply-able unified diff: replaces panic() at line 251 with log(LOG_ERR,...) + m_freem(m) + return (EINVAL). |
panic.txt / panic_sig.txt |
Full panic signature from dfbsd-qemu/boot.log. |
fix_run_dmesg.txt |
dmesg excerpt from the patched-kernel re-run showing bad dir 2, dropping mbuf + rcvdata returned 22. |
boot_panic.log |
Full untrimmed serial console capture of the panic run. |
env.txt |
Guest uname -a, kern.version, cc --version. |
Why no uid=0 chain
There is no memory-corruption primitive here β the bug is a direct
panic() call on attacker-controllable data. Panic is the only effect;
no slab grooming, no function-pointer overwrite, no ucred forge. The
chain terminates at "kernel panics, host reboots" β a DoS, by definition
not an escalation.
Per the audit's threat model, the bug is also gated behind root-only operations (kldload + netgraph access) and requires a non-default kernel configuration. Reported impact: privileged DoS (root β kernel panic) / hardening opportunity.
DF-0736 β Verdict
Verdict
REPRODUCED (privileged DoS / hardening gap, not an unprivileged escalation).
Mechanism (trigger β primitive β effect)
static int
ng_ipfw_rcvdata(hook_p hook, item_p item)
{
struct ng_ipfw_tag *ngit;
struct mbuf *m;
NGI_GET_M(item, m);
NG_FREE_ITEM(item);
if ((ngit = (struct ng_ipfw_tag *)m_tag_locate(m, NGM_IPFW_COOKIE, 0,
NULL)) == NULL) {
NG_FREE_M(m);
return (EINVAL);
}
switch (ngit->dir) {
case NG_IPFW_OUT: /* 0 */
...
case NG_IPFW_IN: /* 1 */
ip_input(m);
return (0);
default:
panic("ng_ipfw_rcvdata: bad dir %u", ngit->dir); /* LINE 251 */
}
...
}
ngit->dir is a 4-byte int carried inside the mbuf's m_tag identified
by (cookie=NGM_IPFW_COOKIE=1105988990, type=0). Any value other than
0/1 falls into the default: arm and triggers panic(), taking down the
kernel.
Reachability trace (cited path:line)
| Step | Path |
|---|---|
| 1. ng_ipfw netgraph node must be live | sys/netgraph7/ng_ipfw.c:106-119 (ng_ipfw_mod_event MOD_LOAD β ng_make_node_common + ng_name_node(fw_node, "ipfw")) |
| 2. An mbuf carrying the forged m_tag arrives on any ipfw hook | sys/netgraph7/ng_ipfw.c:220 (ng_ipfw_rcvdata) |
| 3. The m_tag is located via cookie+type lookup | sys/netgraph7/ng_ipfw.c:228 (m_tag_locate(m, NGM_IPFW_COOKIE, 0, NULL)) |
4. ngit->dir is read from the m_tag body |
sys/netgraph7/ng_ipfw.c:234 (switch (ngit->dir)) |
| 5. Invalid dir β panic | sys/netgraph7/ng_ipfw.c:251 |
Who can produce the forged m_tag?
ng_ipfw_input()atsys/netgraph7/ng_ipfw.c:259is the only in-tree writer of the m_tag (m_tag_alloc(NGM_IPFW_COOKIE, ...)at line 286,ngit->dir = dirat line 292). It runs only via theng_ipfw_input_pfunction pointer.grep -rn ng_ipfw_input_p sys/returns zero dereferences outsidesys/netgraph7/ng_ipfw.{c,h}itself. No kernel subsystem ever calls it; the writer is dead code.- The userspace
ng_socketdata-send path (sys/netgraph7/socket/ng_socket.c:412 ngd_send) explicitly rejects thecontrolmbuf at line 424 (if (control != NULL) error = EINVAL) and never attaches an m_tag to the data mbuf, so unprivileged (and even root) userspace cannot forge the tag viasendmsg(2). ng_socketcontrol attach requiresSYSCAP_RESTRICTEDROOT(sys/netgraph7/socket/ng_socket.c:182), so unprivileged users cannot even reach the netgraph topology.- The only remaining producer of the forged tag is an in-kernel netgraph
peer module β which requires
kldload(root).
Build / load barriers
ng_ipfwis not inX86_64_GENERIC(sys/config/X86_64_GENERICcontains nooptions netgraph7_ipfw).ng_ipfw.kois not shipped: it's missing from theSUBDIR=list ofsys/netgraph7/Makefile.- Building
ng_ipfw.kofrom source fails as-is because: #include <netinet/ip_fw.h>(sys/netgraph7/ng_ipfw.c:44) β that path does not exist on DragonFly; the header is atsys/net/ipfw/ip_fw.h.MODULE_DEPEND(ng_ipfw, ipfw, 2, 2, 2)(sys/netgraph7/ng_ipfw.c:83) β requires theipfw2firewall module to be loaded, which on default installs blocks all network traffic on load.ng_ipfw_input_pis declaredexterninsys/netgraph7/ng_ipfw.h:36but defined nowhere in the tree (grep -rn 'ng_ipfw_input_t \*ng_ipfw_input_p' sys/is empty), so the module fails to link without an explicit definition added.
These three issues together strongly suggest ng_ipfw is broken/dead
code that has not been touched since the FreeBSD import in 2008 (per the
$DragonFly: src/sys/netgraph7/ng_ipfw.h,v 1.2 2008/06/26 23:05:35 dillon Exp $
tag).
Reproduction
The included attack.c is a tiny in-kernel module that, on kldload,
locates the live ipfw netgraph node (ng_name2noderef(NULL, "ipfw")),
packages an mbuf carrying a minimal IPv4 header + a forged
NGM_IPFW_COOKIE m_tag with dir=2, and invokes
node->nd_type->rcvdata(NULL, item) β driving the exact same code path
netgraph's item dispatcher would take when an mbuf arrives on a hook of
the ipfw node. On the unfixed kernel this triggers:
panic: ng_ipfw_rcvdata: bad dir 2
cpuid = 4
ng_ipfw_rcvdata() at ng_ipfw_rcvdata+0xc2 0xffffffff8260b112
df736_modevent() at df736_modevent+0x125 0xffffffff8260c125
module_register_init() at module_register_init+0x49 0xffffffff806232e9
linker_load_file.part.3() at linker_load_file.part.3+0x1b9 0xffffffff80624619
linker_load_module() at linker_load_module+0x116 0xffffffff80625e16
Debugger("panic")
Full signature captured in panic.txt; full serial console in
boot_panic.log.
Why no escalation chain
The primitive is a direct panic() call on attacker-controllable data.
There is no memory-corruption primitive (no OOB write, no UAF, no
type-confusion, no function-pointer overwrite). The kernel does not
return control to the attacker after the panic β it reboots. By
definition this is a DoS, not an escalation, and there is no chain to
develop.
Per the audit's threat model this is also a privileged DoS:
- ng_socket control attach requires SYSCAP_RESTRICTEDROOT
- kldload ng_ipfw.ko requires root
- The ng_ipfw.ko module is not shipped and is non-trivial to build
Fix
fix.diff replaces the panic() with the correct "drop + log + return
EINVAL" pattern recommended in the finding summary:
--- a/sys/netgraph7/ng_ipfw.c
+++ b/sys/netgraph7/ng_ipfw.c
@@ -248,7 +248,10 @@
case NG_IPFW_IN:
ip_input(m);
return (0);
default:
- panic("ng_ipfw_rcvdata: bad dir %u", ngit->dir);
+ log(LOG_ERR, "ng_ipfw_rcvdata: bad dir %u, dropping mbuf\n",
+ ngit->dir);
+ m_freem(m);
+ return (EINVAL);
}
git apply --check passes on a clean checkout.
Fix-validation
| Step | Result |
|---|---|
Apply fix.diff to /usr/src/sys/netgraph7/ng_ipfw.c |
Hunk #1 succeeded at 248. |
Build standalone patched ng_ipfw.ko (after the same source-fixing setup as the baseline) |
OK, panic symbol absent from the resulting .ko (PANIC GONE) |
Re-load the same df736attack.ko that triggered the baseline panic |
kldload returned 0; guest stayed up |
dmesg on the patched kernel |
ng_ipfw_rcvdata: bad dir 2, dropping mbuf + df736: rcvdata returned 22 (EINVAL) β no panic |
Before/after contrast (both runs from the same df736attack.ko driving
the same forged mbuf into the same ipfw netgraph node):
=== BEFORE (unpatched ng_ipfw.ko) ===
panic: ng_ipfw_rcvdata: bad dir 2
ng_ipfw_rcvdata() at ng_ipfw_rcvdata+0xc2 0xffffffff8260b112
df736_modevent() at df736_modevent+0x125 0xffffffff8260c125
Debugger("panic") <-- kernel halt
=== AFTER (patched ng_ipfw.ko) ===
ng_ipfw_rcvdata: bad dir 2, dropping mbuf
df736: rcvdata returned 22 -- if you see this, the fix is in place
(load_exit=0, guest responsive, no panic)
Fix closes the bug. (fix_status: fixed.)
PoC changes vs. the original PoC
The finding had no PoC on disk (only the DB summary). This evidence pack was authored from scratch:
attack.cβ minimal in-kernel trigger that locates theipfwnode, forges the m_tag withdir=2, and invokes the rcvdata callback directly vianode->nd_type->rcvdata(NULL, item). Avoids the static-only netgraph hook-creation APIs.setup.shβ performs the three pre-existing source fixes (netinet/ip_fw.hsymlink, strips the brokenMODULE_DEPEND(ng_ipfw, ipfw, ...), defines the missingng_ipfw_input_psymbol) song_ipfw.kocan actually be built and loaded standalone; also rebuildsnetgraph.ko+ng_socket.kofrom source to match the freshly-built modules' ABI.validate_fix.shβ same setup but appliesfix.difffirst.fix.diffβ replacespanic()at line 251 withlog + m_freem + return EINVAL. Supersedes the finding-markdown proposal (the finding's## Recommended fixtext in the DB summary is one line "Fix: m_freem(m) return(EINVAL) + log(LOG_ERR)"; this diff is the git-apply-able, line-accurate realization of that proposal and adds the missingm_freem(m)ordering).
Final impact
| Field | Value |
|---|---|
| Reproduced | yes (kernel panic) |
| Impact | panic (privileged DoS β root-only, non-default config) |
| Confidence | certain |
| Unprivileged privesc | no (the panic is the entire effect; reachability is root-only) |
| Realistic severity | Low / hardening (matches the finding's rating) |
Fix verification
fixedVALIDATED: baseline panic at ng_ipfw_rcvdata+0xc2; patched log+m_freem+EINVAL, guest responsive x1.
BEFORE: panic: ng_ipfw_rcvdata: bad dir 2, kernel halted. AFTER: 'ng_ipfw_rcvdata: bad dir 2, dropping mbuf' + 'rcvdata returned 22', guest responsive.
Confirmed kernel references
Detail
Exploit chain
none -- direct panic() call on attacker data, not corruption. Kernel halts and reboots. No memory corruption, no escalation.
Evidence (decisive lines)
BASELINE: panic: ng_ipfw_rcvdata: bad dir 2 / ng_ipfw_rcvdata+0xc2 / Debugger(panic) / kernel halted. PATCHED: ng_ipfw_rcvdata: bad dir 2, dropping mbuf / returned 22 / guest responsive.
PoC changes
Authored from scratch. attack.c (in-kernel trigger forging m_tag), setup.sh (rebuilds netgraph7 modules + patches ng_ipfw.c build issues), build.sh, run.sh, fix.diff (panic->log+m_freem+return EINVAL), VERDICT.md, manifest.json.
Verified recommended fix
Replace panic() at ng_ipfw.c:251 with: log(LOG_ERR,...); m_freem(m); return(EINVAL);. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0736/fix.diff.
Verdict
REPRODUCED. panic at ng_ipfw.c:251 fires when mbuf with forged m_tag (dir=2) reaches ng_ipfw_rcvdata. Confirmed by in-kernel attack module that forges m_tag and invokes node->nd_type->rcvdata directly. Root-only, netgraph7 opt-in (ng_ipfw not in GENERIC, ng_ipfw.ko not shipped, source has 3 preexisting build issues). Privileged DoS / hardening gap.
No comments yet.