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)
PoC verification
Evidence pack
findings/poc/DF-0749 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 addare bothPRIV_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.
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:
kldload ipfw3+kldload ipfw3_basic+kldload ipfw3_layer2β root only (securelevel/kld privilege check).ipfw3 add ...β issuessetsockopt(IPPROTO_IP, IP_FW_X, ...)which is gated byPRIV_ROOT/SYSCAP_NONET_RAW.- 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
fixedVALIDATED. 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.
Confirmed kernel references
- sys/net/ipfw3_layer2/ip_fw3_layer2.c:118
- sys/net/ipfw3_layer2/ip_fw3_layer2.c:119
- sys/net/ipfw3_layer2/ip_fw3_layer2.c:120
- sys/net/ipfw3_layer2/ip_fw3_layer2.c:161
- sys/net/ipfw3_layer2/ip_fw3_layer2.c:162
- sys/net/ipfw3_layer2/ip_fw3_layer2.c:163
- sys/net/ipfw3_basic/ip_fw3_table.h:39
- sys/net/ipfw3_basic/ip_fw3_table.c:570
- lib/libipfw3/layer2/ipfw3_layer2.c:153
- lib/libipfw3/layer2/ipfw3_layer2.c:177
- sys/net/ipfw3_basic/ip_fw3_basic.c:312
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).
No comments yet.