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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0736 Β· 15 files
FileTypeDescriptionSize
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
README.md readme human-facing reproduction instructions
↓ download 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.

VERDICT.md verdict full narrative: mechanism, reachability trace, fix, validation
↓ download raw

DF-0736 β€” Verdict

Verdict

REPRODUCED (privileged DoS / hardening gap, not an unprivileged escalation).

Mechanism (trigger β†’ primitive β†’ effect)

sys/netgraph7/ng_ipfw.c:251:

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() at sys/netgraph7/ng_ipfw.c:259 is the only in-tree writer of the m_tag (m_tag_alloc(NGM_IPFW_COOKIE, ...) at line 286, ngit->dir = dir at line 292). It runs only via the ng_ipfw_input_p function pointer.
  • grep -rn ng_ipfw_input_p sys/ returns zero dereferences outside sys/netgraph7/ng_ipfw.{c,h} itself. No kernel subsystem ever calls it; the writer is dead code.
  • The userspace ng_socket data-send path (sys/netgraph7/socket/ng_socket.c:412 ngd_send) explicitly rejects the control mbuf 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 via sendmsg(2).
  • ng_socket control attach requires SYSCAP_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_ipfw is not in X86_64_GENERIC (sys/config/X86_64_GENERIC contains no options netgraph7_ipfw).
  • ng_ipfw.ko is not shipped: it's missing from the SUBDIR= list of sys/netgraph7/Makefile.
  • Building ng_ipfw.ko from 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 at sys/net/ipfw/ip_fw.h.
  • MODULE_DEPEND(ng_ipfw, ipfw, 2, 2, 2) (sys/netgraph7/ng_ipfw.c:83) β€” requires the ipfw2 firewall module to be loaded, which on default installs blocks all network traffic on load.
  • ng_ipfw_input_p is declared extern in sys/netgraph7/ng_ipfw.h:36 but 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 the ipfw node, forges the m_tag with dir=2, and invokes the rcvdata callback directly via node->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.h symlink, strips the broken MODULE_DEPEND(ng_ipfw, ipfw, ...), defines the missing ng_ipfw_input_p symbol) so ng_ipfw.ko can actually be built and loaded standalone; also rebuilds netgraph.ko + ng_socket.ko from source to match the freshly-built modules' ABI.
  • validate_fix.sh β€” same setup but applies fix.diff first.
  • fix.diff β€” replaces panic() at line 251 with log + m_freem + return EINVAL. Supersedes the finding-markdown proposal (the finding's ## Recommended fix text 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 missing m_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; validated via standalone patched ng_ipfw.ko)

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.