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

Unbounded table-id index in MAC-table lookup matchers β€” heap OOB read before eh-NULL gate (same DF-0668 pattern)

Summary

check_mac_from_lookup :118-120 and check_mac_to_lookup :161-163: table_ctx=ctx->table_ctx; table_ctx+=cmd->arg1; rnh=table_ctx->node. cmd->arg1 is uint16_t (0-65535) from userland parser strtoul NO validation. table_ctx array allocated 32 entries IPFW_TABLES_MAX (ip_fw3_table.h:39 ip_fw3_table.c:570). NO bounds check arg1<32. OOB read at :120 BEFORE eh-NULL gate :124 so OOB fires on EVERY packet matching rule regardless of args.eh. Also missing table_ctx->type!=0 guard (IPv4 siblings ip_fw3_basic.c:314 have it layer2 omits). Same pattern as DF-0668. Trigger: ipfw3 add mac-from table 65535 any to any then ping any address. Currently eh-gated branch dormant (args.eh always NULL in ipfw3) but OOB read itself is live. Fix: if(cmd->arg1>=IPFW_TABLES_MAX) NOT_MATCH return + if(table_ctx->type==0||rnh==NULL) return.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0749 Β· 15 files
FileTypeDescriptionSize
trigger.c trigger-source C wrapper around the trigger (compiles cleanly) 4.1 KB view raw
trigger.sh trigger-source self-contained sh trigger 1.3 KB view raw
run.sh trigger-source exact run invocation used for both baseline and patched runs 784 B view raw
build.sh build-script exact cc build line 308 B view raw
build.log build-log final successful C build, full output 66 B view raw
run.log run-log decisive baseline run stdout (panic captured in panic.txt) 827 B view raw
fix_run.log run-log decisive patched-kernel run: rule added, no panic, guest stays up 598 B view raw
fix_build.log build-log single-fix kernel build log (excerpt; full log was 35568 lines on guest) 2.1 KB view raw
panic.txt panic-signature Fatal trap 12 / Stopped at check_mac_from_lookup+0x37 (twice, fresh boots) 2.8 KB view raw
env.txt environment uname, cc version, sysctl state, loaded modules 922 B view raw
fix.diff suggested-fix git-apply-able unified diff: bounds-check cmd->arg1 in both MAC lookup matchers 1.7 KB view raw
VERDICT.md verdict full narrative: mechanism, reachability, Phase-6 blocker, Phase-8 validation 6.4 KB ↓ raw
README.md readme human-facing reproduction guide 2.7 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 guide
↓ download raw

DF-0749 β€” Unbounded table-id index in MAC-table lookup matchers (ipfw3 layer2)

Status: REPRODUCED (panic / DoS, root-reachable).
Impact ceiling: kernel panic from root. Unprivileged escalation is impossible β€” kldload + ipfw3 add are both PRIV_ROOT. The primitive is a read-only OOB load; the loaded value is never dereferenced (eh-gate) and never copied to userspace (no leak).
Fix VALIDATED on a built-and-booted single-fix kernel.

Files in this evidence pack

file what it is
trigger.c C wrapper around the trigger (compiles; not strictly needed)
trigger.sh self-contained sh trigger
run.sh the exact run invocation (used for baseline + patched runs)
build.sh exact build command
build.log full compiler output of the final C build
run.log decisive baseline run (stdout; panic captured in panic.txt)
fix_run.log decisive patched-kernel run (clean, no panic)
fix_build.log single-fix kernel build log (excerpt; full on guest)
panic.txt kernel panic signature from serial boot.log (the proof)
env.txt guest uname, cc version, sysctl state
fix.diff git-apply-able unified diff fixing the bug
VERDICT.md full narrative with path:line citations
manifest.json machine-readable catalog

How to reproduce (unpatched kernel)

On the DragonFlyBSD master DEV guest as root:

sysctl net.filters_default_to_accept=1     # keep ssh reachable
kldload ipfw3
kldload ipfw3_basic
kldload ipfw3_layer2
ipfw3 flush
ipfw3 add 1000 allow ip from any to any mac-from table 65535
ping -c 1 127.0.0.1

Within ~1–2 s the kernel will panic with:

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffff...     (β‰ˆ 3.5 MB past table_ctx)
Stopped at      check_mac_from_lookup+0x37:     movq    (%rax),%rbx

Capture it from dfbsd-qemu/boot.log.

Expected on a fixed kernel

The same trigger completes; the rule is added (ipfw3 list shows it), but sending packets does not panic the kernel. The bounds-checked matcher returns IP_FW_NOT_MATCH for arg1 >= 32 and skips the OOB read entirely.

Build / run

./build.sh        # compiles trigger.c (optional; the shell trigger is sufficient)
./run.sh          # invokes the trigger (run as root on the guest)

See VERDICT.md for the full mechanism, reachability, and Phase-8 fix validation.

VERDICT.md verdict full narrative: mechanism, reachability, Phase-6 blocker, Phase-8 validation
↓ download raw

DF-0749 β€” Verdict

Verdict: REPRODUCED (panic / DoS, root-reachable). Fix VALIDATED on a built-and-booted single-fix kernel.

The bug (confirmed by source trace + live panic)

check_mac_from_lookup (sys/net/ipfw3_layer2/ip_fw3_layer2.c:109-132) and check_mac_to_lookup (sys/net/ipfw3_layer2/ip_fw3_layer2.c:152-175) index the per-CPU ctx->table_ctx[] array (32 entries, IPFW_TABLES_MAX, sys/net/ipfw3_basic/ip_fw3_table.h:39; allocated at sys/net/ipfw3_basic/ip_fw3_table.c:570) with cmd->arg1 and no bounds check:

table_ctx = ctx->table_ctx;
table_ctx += cmd->arg1;        /* line 119 / 162: NO check that arg1 < 32 */
rnh = table_ctx->node;          /* line 120 / 163: OOB load */
...
if ((*args)->eh != NULL) { ... /* eh-gate fires AFTER the OOB load */ }

cmd->arg1 is a uint16_t populated from userland by the ipfw3 rule parser with no validation:

/* lib/libipfw3/layer2/ipfw3_layer2.c:153 */
(*cmd)->arg1 = strtoul(**av, NULL, 10);

So ipfw3 add ... mac-from table 65535 (or mac-to table 65535) plants arg1 = 65535 directly into the rule. The matcher then runs on every IP packet traversing the rule and performs table_ctx += 65535 (offset 65535 Γ— sizeof(struct ipfw3_table_context) = 65535 Γ— 56 = 3,669,960 bytes β‰ˆ 3.5 MB past the array), followed by an 8-byte load at that OOB address.

Crucially, the OOB load at line 120/163 executes BEFORE the (*args)->eh != NULL gate at line 124/167, so the gate does not protect against it β€” contradicting any "eh is always NULL so it's dormant" hand-wave. The load itself is the bug; it just happens to be benign only when the OOB address is mapped. With arg1=65535 the OOB offset is unmapped β†’ page fault.

Reproduction

Twice, on a fresh vm.sh reset with-src (6.5-DEVELOPMENT #0, unpatched audit-source kernel), as root:

sysctl net.filters_default_to_accept=1   # keep ssh alive
kldload ipfw3; kldload ipfw3_basic; kldload ipfw3_layer2
ipfw3 flush
ipfw3 add 1000 allow ip from any to any mac-from table 65535
ping -c 1 127.0.0.1

Result (both runs, identical signature modulo fault VA / cpuid):

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff80118b1f148   (run 1) / 0xfffff80118ad0a48   (run 2)
fault code            = supervisor read data, page not present
instruction pointer   = 0x8:0xffffffff826de167   (in ipfw3_layer2.ko)
Stopped at      check_mac_from_lookup+0x37:     movq    (%rax),%rbx

check_mac_from_lookup+0x37 disassembles to the movq (%rax), %rbx that implements rnh = table_ctx->node (line 120). %rax holds the OOB address; the supervisor read of an unmapped page vector-12's β†’ kernel panic.

Reachability / Phase-6 escalation analysis (HARD BLOCKER β€” valid)

The primitive is an OOB read (load of table_ctx->node into a local rnh). Reaching it requires all of:

  1. kldload ipfw3 + kldload ipfw3_basic + kldload ipfw3_layer2 β€” root only (securelevel/kld privilege check).
  2. ipfw3 add ... β€” issues setsockopt(IPPROTO_IP, IP_FW_X, ...) which is gated by PRIV_ROOT / SYSCAP_NONET_RAW.
  3. A packet traversing the rule (any IP packet, including loopback).

This is root β†’ kernel, which the bright-line rule explicitly lists as a valid hard blocker: "reachable only from an already-root context (kldload / wheel-only ioctl …), so there is no privilege boundary to cross (rootβ†’kernel is game-over by definition)." There is no unprivileged path to the matcher β€” neither kldload nor IP_FW_X is callable by uid!=0.

Even setting reachability aside, the primitive itself is read-only: the OOB value lands in a local struct radix_node_head *rnh which is only dereferenced inside the eh != NULL branch (always false on the ipfw3 packet path), and is never copied to userspace. So there is no attacker-observable info leak either; the only observable effect is the page-fault panic (DoS).

Per Phase 6, two independent valid hard blockers apply (root-only reachability AND read-only primitive). No escalation chain is attempted because none is possible. The realistic impact ceiling is kernel panic (DoS) at root.

Fix

findings/poc/DF-0749/fix.diff β€” bounds-check cmd->arg1 against IPFW_TABLES_MAX before the table_ctx += cmd->arg1 index, in both check_mac_from_lookup and check_mac_to_lookup. Also adds the table_ctx->type == 0 || rnh == NULL guard (matching the IPv4 sibling check_from_lookup at ip_fw3_basic.c:314 shape) as defense-in-depth against using an uninitialized table. The bounds check is the load-bearing fix; the type guard prevents a separate latent issue (using a table slot that was never created). Supersedes any pre-verification proposal by placing the check at the earliest possible return point (before the OOB index), not after.

Fix validation (Phase 8)

Built a single-fix kernel + module on the guest: make -j6 nativekernel KERNCONF=X86_64_GENERIC (warm obj, ~7 min, rc=0, -Werror clean). Installed kernel.stripped β†’ /boot/kernel/kernel and ipfw3_layer2.ko β†’ /boot/kernel/ipfw3_layer2.ko, rebooted. kern.version bumped #0 (Jul 2) β†’ #1 (Jul 9).

Re-ran the identical trigger on the patched kernel:

kernel rule added packets sent guest after boot.log
#0 unpatched yes 1 ping down Fatal trap 12 … Stopped at check_mac_from_lookup+0x37
#1 patched yes many pings up only ipfw3 module layer2 loaded, no trap

The previously-observed page fault is gone on the patched kernel, and legitimate in-bounds usage (mac-from table 0, mac-from table 31) still works correctly. Fix status: fixed.

Conclusion

  • Status: reproduced (panic, deterministic, twice on fresh boot).
  • Impact: panic (root-reachable DoS; primitive is read-only, unprivileged path does not exist).
  • Severity as filed: Medium β€” agrees with the demonstrated impact ceiling.
  • Fix: fix.diff, validated by built-and-booted single-fix kernel.

Same pattern as DF-0668 (sys/net/ipfw3_basic/ip_fw3_table.c dispatch handlers); same root cause class (unvalidated cmd->arg1/ioc_table->id used as array index). The IPv4 sibling check_from_lookup at ip_fw3_basic.c:311-320 has the same unbounded table_ctx += cmd->arg1 at line 312 and should receive the same fix (covered by DF-0668's scope).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. Built a single-fix kernel + module (make -j6 nativekernel KERNCONF=X86_64_GENERIC, warm obj, rc=0, -Werror clean; ip_fw3_layer2.c recompiled and ipfw3_layer2.ko relinked), installed kernel.stripped -> /boot/kernel/kernel and ipfw3_layer2.ko -> /boot/kernel/ipfw3_layer2.ko, rebooted (kern.version #0 Jul 2 -> #1 Jul 9). On the #0 baseline the same trigger (ipfw3 add 1000 allow ip from any to any mac-from table 65535 + ping) deterministically page-faults inside check_mac_from_lookup+0x37 within ~1-2s. On the #1 patched kernel the SAME rule is installed (verified via ipfw3 list) and pings complete normally; the guest stays UP and boot.log shows no Fatal trap, no Stopped-at. In-bounds rules (mac-from table 0, table 31) still work. Fix closes the bug.

BASELINE #0: rule `01000 allow mac-from table 65535` added -> Fatal trap 12: page fault while in kernel mode / fault VA 0xfffff80118ad0a48 / supervisor read data, page not present / Stopped at check_mac_from_lookup+0x37: movq (%rax),%rbx / db>   (twice on fresh boots). PATCHED #1: same trigger -> STILL_ALIVE id=65535 RUN_EXIT=0 / ipfw3 list shows `01000 allow mac-from table 65535` / ping -c 2 127.0.0.1: 2 packets transmitted, 2 received, 0% loss / vm.sh status: up / boot.log shows only `ipfw3 module layer2 loaded`, no Fatal trap. Before/after contrast confirms fix.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 08:17:53 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (ipfw3_layer2.ko sha256 584567ab56a23a630db86127791f8f285903660be092c9ed767e1fbfd5f0f1b8)

Confirmed kernel references

Detail

Exploit chain

none -- TWO valid Phase-6 hard blockers apply, so no escalation is possible. (1) ROOT-ONLY REACHABILITY: triggering requires kldload ipfw3/ipfw3_basic/ipfw3_layer2 (root-only) AND ipfw3 add via setsockopt(IPPROTO_IP,IP_FW_X) gated by PRIV_ROOT/SYSCAP_NONET_RAW (root-only); there is no unprivileged path to the matcher. (2) READ-ONLY PRIMITIVE: the OOB value lands in a local struct radix_node_head *rnh that is only dereferenced inside the eh != NULL branch (always false on the ipfw3 IP path) and is never copied to userspace -- no info leak, no write, no function-pointer corruption. The only observable effect is the page-fault panic. Same root-cause class as DF-0668 (unvalidated cmd->arg1/ioc_table->id as array index); the IPv4 sibling check_from_lookup at sys/net/ipfw3_basic/ip_fw3_basic.c:311-320 has the identical unbounded table_ctx += cmd->arg1 at :312 and should receive the same fix. No escalation chain file was authored because none is possible.

Evidence (decisive lines)

BASELINE (#0): rule `01000 allow mac-from table 65535` added, then within ~1-2s: Fatal trap 12: page fault while in kernel mode / fault VA=0xfffff80118ad0a48 / supervisor read data, page not present / instruction pointer=0x8:0xffffffff826de167 (in ipfw3_layer2.ko) / Stopped at check_mac_from_lookup+0x37: movq (%rax),%rbx / db>   (reproduced twice, identical signature modulo fault VA + cpuid). PATCHED (#1): same trigger, rule added, many pings sent, guest stays UP; boot.log shows only 'ipfw3 module layer2 loaded' -- no Fatal trap, no Stopped-at. In-bounds rules (mac-from table 0, table 31) still work correctly.

PoC changes

Wrote the evidence pack from scratch (no prior DF-0749 folder existed). Created trigger.c (C wrapper, compiles cleanly), trigger.sh and run.sh (self-contained sh triggers that set filters_default_to_accept=1, kldload ipfw3/ipfw3_basic/ipfw3_layer2, ipfw3 flush, add allow ip from any to any mac-from table 65535, ping), build.sh, VERDICT.md, README.md, fix.diff, manifest.json, plus panic.txt/run.log/fix_run.log/fix_build.log/env.txt.

Verified recommended fix

In sys/net/ipfw3_layer2/ip_fw3_layer2.c, in BOTH check_mac_from_lookup and check_mac_to_lookup, add if (cmd->arg1 >= IPFW_TABLES_MAX) { *cmd_ctl = IP_FW_CTL_NO; *cmd_val = IP_FW_NOT_MATCH; return; } BEFORE the table_ctx += cmd->arg1 index (lines 119 and 162). Defense-in-depth: also add if (table_ctx->type == 0 || rnh == NULL) return; after the index so an uninitialized/uncreated table slot is rejected (matching the IPv4 sibling shape at ip_fw3_basic.c:314). fix.diff in findings/poc/DF-0749/ implements exactly this; supersedes the finding markdown's pre-verification proposal by placing the bounds check at the earliest return point (before the OOB index) rather than relying on the eh-gate. The same unbounded index at sys/net/ipfw3_basic/ip_fw3_basic.c:312 and :405 (IPv4 siblings) should be patched in DF-0668's scope.

Verdict

REPRODUCED as a deterministic kernel panic on the unpatched 6.5-DEVELOPMENT #0 (with-src) kernel, twice on fresh vm.sh reset boots. check_mac_from_lookup (sys/net/ipfw3_layer2/ip_fw3_layer2.c:118-120) and check_mac_to_lookup (:161-163) index ctx->table_ctx[cmd->arg1] with NO bounds check; cmd->arg1 is a uint16_t written straight from userland strtoul by the ipfw3 rule parser (lib/libipfw3/layer2/ipfw3_layer2.c:153,177). The OOB load rnh = table_ctx->node; fires on EVERY packet matching the rule (it executes BEFORE the eh-NULL gate at :124/:167, contradicting any 'dormant' hand-wave). With arg1=65535 the OOB offset is 65535*56 = 3,669,960 bytes (~3.5MB) past the 32-entry array (IPFW_TABLES_MAX, sys/net/ipfw3_basic/ip_fw3_table.h:39; allocated sys/net/ipfw3_basic/ip_fw3_table.c:570) -> supervisor read of unmapped page -> page fault. Signature: 'Fatal trap 12: page fault while in kernel mode / supervisor read data, page not present / Stopped at check_mac_from_lookup+0x37: movq (%rax),%rbx' (the movq is the table_ctx->node load after table_ctx += cmd->arg1).