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

Unchecked uint16_t index into 10-element log_if_table β€” OOB read + kernel panic on matching packet

Summary

ip_fw3_log.c:121 the_if=log_if_table[id] where id is uint16_t parameter (:114). log_if_table is global struct ifnet*[LOG_IF_MAX=10] (:70 ip_fw3_log.h:39). NO bounds check id<LOG_IF_MAX anywhere. id flows from cmd->arg1 (uint16_t ip_fw3.h:127) set by ipfw3 CLI log N via strtoul NO validation (sbin/ipfw3/ipfw3basic.c:93). Kernel rule-add bcopy verbatim NO per-instruction arg1 validation (ip_fw3.c:655). For id>=10 OOB array read returns garbage pointer dereferenced at :122 the_if->if_bpf (if_bpf offset 64 struct ifnet) = page fault panic. id=10-13 reads lock state id=14 reads fake_eh 0x4242424241414141 non-canonical address. Trigger: ipfw3 add 1000 deny log 14 ip from any to any then ping any address. Requires SYSCAP_NONET_RAW (root) for rule install. Fix: if(id>=LOG_IF_MAX) return at top of ip_fw3_log.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0748 Β· 15 files
FileTypeDescriptionSize
trigger.sh trigger-source minimal root-only trigger: load ipfw3, set verbose, install log N rule, ping 2.0 KB view raw
run.sh trigger-source end-to-end repro: builds+loads harness kld, runs the trigger 2.3 KB view raw
build.sh build-script builds harness/df748_wire_log.ko 235 B view raw
harness/harness.c exploit-chain 30-line kld that wires ip_fw3_log_ptr = ip_fw3_log (registration upstream never finished) -> makes dead path live 2.3 KB view raw
harness/Makefile build-script builds df748_wire_log.ko 91 B ↓ download
harness/wire.sh build-script build+load the harness 1.3 KB view raw
fix.diff suggested-fix git-apply-able: if (id >= LOG_IF_MAX) return; at top of ip_fw3_log() 806 B view raw
panic.txt panic-signature Fatal trap 9 GPF in bpf_mtap+0x79 from baseline trigger 1.8 KB view raw
fix_build.log build-log patched ipfw3_basic.ko module build output (rc=0) 4.6 KB view raw
fix_run.log run-log trigger on patched module: no panic, guest stays up 1.1 KB view raw
env.txt environment uname, cc, kldstat, sysctls at test time 635 B view raw
VERDICT.md verdict full narrative: mechanism, primitive, no-escalation story, fix validation 9.8 KB ↓ raw
README.md readme quick repro + file index 2.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
README.md readme quick repro + file index
↓ download raw

DF-0748 β€” Unchecked uint16_t index into 10-element log_if_table[]

Status: REPRODUCED (panic via harness). Impact: latent panic / OOB-read primitive; root-only reachability; dead code on default kernel.

Quick repro (on the DragonFly guest as root)

sh /root/poc748/run.sh
# unpatched kernel: guest panics in bpf_mtap+0x79 (Fatal trap 9 GPF)
# patched kernel:  trigger runs to completion, guest stays up

Files in this folder

file what it is
trigger.sh minimal root-only trigger: load ipfw3, set verbose, install log N rule, ping
run.sh end-to-end repro: builds+loads the harness kld, then runs the trigger
harness/harness.c 30-line kld that wires ip_fw3_log_ptr = ip_fw3_log (the registration upstream never finished)
harness/Makefile builds df748_wire_log.ko
harness/wire.sh build+load the harness
fix.diff git-apply-able fix: if (id >= LOG_IF_MAX) return; at top of ip_fw3_log()
VERDICT.md full narrative (mechanism, primitive, why no escalation, fix validation)
panic.txt the panic signature captured from boot.log (proof of crash)
fix_build.log full build output of the patched ipfw3_basic.ko
fix_run.log full output of the trigger on the patched module (no panic)
env.txt guest uname, cc, kldstat, sysctls at test time

Read VERDICT.md for the full story.

In short: - The bug is real: ip_fw3_log.c:121 indexes log_if_table[10] with an unchecked uint16_t id. - The bug is unreachable from packet processing on a default kernel because ip_fw3_log_ptr (the only call-site gate) is initialised NULL at ip_fw3.c:134 and never assigned anywhere in sys/. - The harness module proves the primitive by wiring the pointer; the trigger then causes a deterministic Fatal trap 9 in bpf_mtap+0x79. - Even if the path were live, the bug is root-only: installing any ipfw3 rule requires SYSCAP_NONET_RAW (raw IP socket) per raw_ip.c:473. No unprivileged→root escalation exists. - The fix is a one-line bounds check; it is validated by a module-only rebuild (deterministic panic before, no panic after, 3/3 runs).

VERDICT.md verdict full narrative: mechanism, primitive, no-escalation story, fix validation
↓ download raw

DF-0748 β€” Unchecked uint16_t index into 10-element log_if_table[] β€” OOB access

Verdict: REPRODUCED (panic / memory-corruption primitive proven via harness; root-only reachability; dead code on default kernel). Fix VALIDATED (single-module rebuild; deterministic before/after).

Severity as filed: Medium. Verified realistic impact: latent panic / OOB-read primitive in ipfw3 logging; reachable only by root, and only when (a) the ipfw3/ipfw3_basic modules are loaded, (b) net.inet.ip.fw3.verbose=1, and (c) ip_fw3_log_ptr is wired (which no current code does).


The bug, confirmed in source

The function ip_fw3_log() at sys/net/ipfw3_basic/ip_fw3_log.c:114 takes a uint16_t id parameter and uses it to index a 10-element array with no bounds check:

// sys/net/ipfw3_basic/ip_fw3_log.c
70:  struct ifnet *log_if_table[LOG_IF_MAX];   // LOG_IF_MAX = 10  (ip_fw3_log.h:39)
...
114: void ip_fw3_log(struct mbuf *m, struct ether_header *eh, uint16_t id)
115: {
116:     struct ifnet *the_if = NULL;
...
121:     the_if = log_if_table[id];                       // <-- OOB if id >= 10
122:     if (the_if == NULL || the_if->if_bpf == NULL) {  // <-- deref of OOB-read ptr
            ...
128:     bpf_mtap_hdr(the_if->if_bpf, ...);              // <-- deref of if_bpf

The id flows from cmd->arg1 (uint16_t, sys/net/ipfw3/ip_fw3.h:127) set by the ipfw3 CLI's log N parser without any validation:

// sbin/ipfw3/ipfw3basic.c:93,110
(*cmd)->arg1 = strtoul(**av, NULL, 10);   // no N < 10 check anywhere

The kernel rule-add path bcopy's this verbatim into the in-kernel rule (sys/net/ipfw3/ip_fw3.c:655), and check_accept/check_deny (ip_fw3.c:253,264) hand cmd->arg1 straight to ip_fw3_log_ptr(...) with no per-instruction validation.

Reproduction / primitive proof

Trigger chain (root-only): 1. sysctl net.filters_default_to_accept=1 (so loading ipfw3 doesn't drop SSH) 2. kldload ipfw3 ; kldload ipfw3_basic 3. sysctl net.inet.ip.fw3.verbose=1 (the gate at ip_fw3_log.c:118) 4. install the harness kld df748_wire_log.ko (sets ip_fw3_log_ptr = ip_fw3_log) 5. ipfw3 add 1000 deny log 12 icmp from 127.0.0.1 to 127.0.0.1 6. ping -c1 127.0.0.1 β†’ kernel panic

Observed panic (deterministic, 1/1 runs):

Fatal trap 9: general protection fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer = 0x8:0xffffffff8072a759
current process = Idle
kernel: type 9 trap, code=0
Stopped at  bpf_mtap+0x79:  movl 0x68(%rbx),%eax
db>

The chain is: check_deny (ip_fw3.c:264) β†’ ip_fw3_log_ptr(m, eh, 12) β†’ ip_fw3_log (ip_fw3_log.c:114) β†’ the_if = log_if_table[12] (:121, OOB read) β†’ the_if = 0xffffffff8264f080 (the address of sysctl__net_inet_ip_fw3_basic_children) β†’ the_if->if_bpf (offset 16) = 0xc000200200000100 (non-canonical) β†’ bpf_mtap_hdr(0xc000200200000100, ...) β†’ bpf_mtap deref β†’ trap 9 GPF.

The runtime symbol addresses (kgdb on /dev/mem) confirm the layout: - log_if_table @ 0xffffffff8264fba0 (10 entries, ends at 0xffffffff8264fbf0) - id=12 reads at 0xffffffff8264fba0 + 8*12 = 0xffffffff8264fc00 = the sysctl__net_inet_ip_fw3_basic_children slot - offset +16 of that = 0xc000200200000100 β†’ non-canonical

Why a harness is needed (the dead-code caveat)

ip_fw3_log() is dead code on a default kernel. Its only call sites (ip_fw3.c:253 and :264) are gated by ip_fw3_log_ptr != NULL, but:

$ grep -rn ip_fw3_log_ptr sys/
sys/net/ipfw3/ip_fw3.c:134:ip_fw_log_t *ip_fw3_log_ptr = NULL;   /* initialised NULL */
sys/net/ipfw3/ip_fw3.c:252:    if (cmd->arg3 && ip_fw3_log_ptr != NULL) {
sys/net/ipfw3/ip_fw3.c:253:        ip_fw3_log_ptr((*args)->m, (*args)->eh, cmd->arg1);
sys/net/ipfw3/ip_fw3.c:263:    if (cmd->arg3 && ip_fw3_log_ptr != NULL) {
sys/net/ipfw3/ip_fw3.c:264:        ip_fw3_log_ptr((*args)->m, (*args)->eh, cmd->arg1);

Five references, all in ip_fw3.c: one declaration (init = NULL), two NULL-guarded reads around the call. No writer anywhere in sys/. So the buggy function cannot be reached from packet processing on a stock kernel.

The 30-line harness module (harness/harness.c) does the registration the upstream feature never finished (ip_fw3_log_ptr = &ip_fw3_log) and is explicit about the dependency in its MODULE_DEPEND. Once loaded, the existing packet-processing path becomes live and the OOB read fires.

This places the finding in Phase-4 case (d): the vulnerable code path is dead at runtime; prove the primitive at the object/harness level, and note the live trigger conditions that would make it real. The harness is that proof. The live trigger condition is "any future commit that wires ip_fw3_log_ptr (which is the obvious next step for the unfinished ipfw3 logging feature)."

Privilege / reachability (the no-escalation story)

This bug has no unprivileged->root escalation path, for two independent reasons (either suffices; both hold):

  1. Root-only trigger. Installing any ipfw3 rule requires the IP_FW_X setsockopt, which is dispatched in rip_ctloutput (sys/netinet/raw_ip.c:386). Raw IP sockets are gated by caps_priv_check(p_ucred, SYSCAP_NONET_RAW) at raw_ip.c:473. SYSCAP_NONET_RAW is root-only on a default DragonFly install. No unprivileged user can install a log N rule.

  2. Default-dead path. Even an attacker who could install a rule cannot reach ip_fw3_log() on a default kernel because ip_fw3_log_ptr is never assigned.

Per the bright-line rule, this is a valid hard blocker for escalation: "The write/primitive is reachable only from an already-root context... root→kernel is game-over by definition." Combined with the dead-code blocker, there is nothing to escalate from. So:

  • Escalation attempts: 0 β€” there is no unprivileged primitive to escalate from. (Spending 15–30 grooming attempts would be pointless: the trigger itself already requires root.)
  • Realistic impact ceiling: panic / DoS / latent OOB-read primitive (could in principle leak kernel memory to a bpf tap on the forged ifp, but no bpf listener is normally attached to the corrupted fake ifp, so crash dominates).
  • Recommended triage: defense-in-depth. The bounds check is correct and cheap; upstream should add it (the harness exists because someone clearly intends to wire this code path soon).

Fix (validated)

fix.diff adds a single bounds check at the top of ip_fw3_log():

+    if (id >= LOG_IF_MAX) {
+        return;
+    }

This is the smallest correct fix: it closes the OOB read at the sink (ip_fw3_log.c:121) regardless of how id arrives. (A defense-in-depth companion would also clamp at rule-install in ip_fw3.c / sbin/ipfw3/ipfw3basic.c, but the sink-side guard is the root-cause fix.)

Validation (Phase 8): module-only rebuild of ipfw3_basic.ko (the bug lives entirely in module C; no kernel rebuild needed). Same trigger on:

  • unpatched baseline (/boot/kernel/ipfw3_basic.ko, sha256 3c3af7cf...): panic in bpf_mtap+0x79, guest down.
  • patched module (rebuilt from fixed source, sha256 7d0c4ae2...): trigger runs to completion, rule still matches (ping is denied), no panic, guest stays up. Reproduced 3/3 times.

The kern.version string is unchanged (6.5-DEVELOPMENT #0) because the kernel image itself is not touched β€” only the loaded module. The before/after contrast is the panic-vs-no-panic behavior of the same trigger.

PoC changes (what's in this folder vs. what the finding proposed)

The finding markdown does not exist on disk (only the DB row exists). The DB summary proposed: trigger ipfw3 add 1000 deny log 14 ip from any to any then ping. That proposed PoC has two errors the runner corrected:

  1. log 14 does not reliably panic. At runtime fake_eh is loaded at a lower address than log_if_table (0xffffffff8264f040 vs 0xffffffff8264fba0), so id=14 reads BSS padding = NULL β†’ silent early-return. The runner measured the actual live layout (kgdb) and chose id=12, which reads the sysctl__net_inet_ip_fw3_basic_children slot whose +16 field is a non-canonical 0xc000200200000100 β†’ reliable trap 9.
  2. log N from any to any would deny SSH and break the test harness. The runner narrowed the match to icmp from 127.0.0.1 to 127.0.0.1.
  3. The trigger alone does nothing on a default kernel β€” ip_fw3_log_ptr is never assigned. The runner built a 30-line harness kld (harness/harness.c) to wire the pointer, making the path live and the OOB read reachable. This proves the primitive at the object level per Phase-4 case (d); it is not part of any escalation chain (kldload is root-only by definition).

How to reproduce

scp -r findings/poc/DF-0748/. dfbsd:/root/poc748/    # root shell on guest
ssh dfbsd
sh /root/poc748/run.sh                                # panics on unpatched, exits 0 on patched
# panic signature lands in dfbsd-qemu/boot.log

Kernel references confirmed during verification

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: the SAME trigger (harness kld + ipfw3 stack + verbose=1 + rule deny log 12 icmp from 127.0.0.1 to 127.0.0.1 + ping) panics on the unpatched baseline ipfw3_basic.ko (Fatal trap 9 in bpf_mtap+0x79, guest down) and does NOT panic on the patched module (rule still matches, ping denied with 100% packet loss, guest stays up; reproduced 3/3 times). The one-line if (id >= LOG_IF_MAX) return; guard at the top of ip_fw3_log() short-circuits before the OOB read at ip_fw3_log.c:121, deterministically closing the bug.

BEFORE (unpatched /boot/kernel/ipfw3_basic.ko sha256 3c3af7cf...): `Fatal trap 9: general protection fault while in kernel mode / cpuid = 0 / current process = Idle / kernel: type 9 trap, code=0 / Stopped at bpf_mtap+0x79: movl 0x68(%rbx),%eax / db>` (guest DOWN). AFTER (patched module sha256 7d0c4ae2...): `01000 0 0 deny log 12 icmp from 127.0.0.1 to 127.0.0.1 / ... / ping: sendto: Permission denied / 1 packets transmitted, 0 packets received, 100.0% packet loss / [!] GUEST_UP_NO_PANIC (FIXED kernel; baseline would have panicked here)` (guest UP, 3/3).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kern.version unchanged -- module-only rebuild; patched artifact is /boot/kernel/ipfw3_basic.ko sha256 7d0c4ae2426a19298b820ea41fe2a0b56158cbc29ed6cd27e90c07aa27a49b7f vs baseline 3c3af7cf53174a43b09a4d8cd4d921fbc32838b4e2524ab21e043a93b39cf6ef)

Confirmed kernel references

Detail

Exploit chain

NO escalation chain developed -- blocked by TWO valid hard blockers (Phase 6): (1) the primitive is reachable only from an already-root context (rule install requires SYSCAP_NONET_RAW at raw_ip.c:473; root->kernel is game-over by definition), and (2) the buggy function ip_fw3_log() is dead code on a default kernel because ip_fw3_log_ptr is initialised NULL at ip_fw3.c:134 and never assigned anywhere in sys/. The primitive is PROVEN at the object level via a 30-line harness kld (harness/harness.c) that wires ip_fw3_log_ptr = ip_fw3_log (the registration the upstream feature never finished). With the harness loaded + rule deny log 12 icmp from 127.0.0.1 to 127.0.0.1 + ping, the OOB read at ip_fw3_log.c:121 fires deterministically and panics the kernel in bpf_mtap+0x79 (Fatal trap 9 GPF). Next iteration that would make this a LIVE bug on stock kernels: any upstream commit that assigns ip_fw3_log_ptr (the obvious next step for the unfinished ipfw3 logging feature). No slab grooming / victim-object / uid=0 chain was attempted because there is no unprivileged primitive to escalate from. Files: harness/harness.c (chain wiring), trigger.sh (minimal trigger), run.sh (end-to-end).

Evidence (decisive lines)

BASELINE (unpatched ipfw3_basic.ko, sha256 3c3af7cf...): trigger installs `deny log 12 icmp from 127.0.0.1 to 127.0.0.1` after loading ipfw3 + harness kld; ping fires; kernel panics -- boot.log shows: `Fatal trap 9: general protection fault while in kernel mode / cpuid = 0 / current process = Idle / kernel: type 9 trap, code=0 / Stopped at bpf_mtap+0x79: movl 0x68(%rbx),%eax / db>`. PATCHED module (rebuilt with fix.diff, sha256 7d0c4ae2...): same trigger runs to completion -- `ping: sendto: Permission denied` (deny rule still matches), 100% packet loss, then `[!] GUEST_UP_NO_PANIC (FIXED kernel; baseline would have panicked here)`, guest stays up; reproduced 3/3 times.

PoC changes

Created findings/poc/DF-0748/ from scratch (no markdown or PoC existed -- only the DB row). Wrote trigger.sh (root-only ipfw3 rule-install + ping), run.sh (end-to-end with harness), harness/harness.c + Makefile + wire.sh (30-line kld that wires ip_fw3_log_ptr = ip_fw3_log to make the dead path live), fix.diff (one-line bounds check), VERDICT.md, README.md, manifest.json. Corrected three errors in the DB-proposed PoC: (1) log 14 does not reliably panic -- runtime kgdb showed fake_eh is at a LOWER address than log_if_table, so id=14 reads BSS padding = NULL -> silent early return; runner measured live layout and chose id=12 which reads the sysctl__net_inet_ip_fw3_basic_children slot whose +16 field is non-canonical 0xc000200200000100 -> reliable trap 9; (2) narrowed match from ip from any to any to icmp from 127.0.0.1 to 127.0.0.1 so SSH survives; (3) discovered ip_fw3_log_ptr is never assigned in sys/ -> built harness kld to prove the primitive.

Verified recommended fix

Add if (id >= LOG_IF_MAX) { return; } at the top of ip_fw3_log() in sys/net/ipfw3_basic/ip_fw3_log.c (right after the function prologue), closing the OOB read at line 121 regardless of how id arrives. The full git-apply-able diff is in findings/poc/DF-0748/fix.diff. This is a NEW fix (the finding markdown does not exist on disk; only the DB row exists with a brief description, so there is no prior proposal to supersede or match). A defense-in-depth companion would also clamp at rule-install in ip_fw3.c / sbin/ipfw3/ipfw3basic.c, but the sink-side guard at ip_fw3_log() is the root-cause fix.

Verdict

REPRODUCED (panic / OOB-read primitive proven via harness module; root-only reachability; dead code on default kernel). The bug is real and confirmed at sys/net/ipfw3_basic/ip_fw3_log.c:121 where the_if = log_if_table[id] indexes a 10-element array (LOG_IF_MAX=10, ip_fw3_log.h:39) with an unchecked uint16_t id (ip_fw3.h:127) that flows from the ipfw3 CLI log N parser (sbin/ipfw3/ipfw3basic.c:93,110) with no validation, and the kernel rule-add path bcopy's it verbatim (ip_fw3.c:655). With id=12 the OOB read returns the address of sysctl__net_inet_ip_fw3_basic_children (0xffffffff8264f080), whose +16 field is the non-canonical value 0xc000200200000100 -> bpf_mtap deref -> Fatal trap 9 GPF at bpf_mtap+0x79, guest down (deterministic, captured in panic.txt). Two independent hard blockers prevent escalation: (1) ip_fw3_log() is DEAD CODE on a default kernel -- its only call sites (ip_fw3.c:253,264) are gated by ip_fw3_log_ptr != NULL, but ip_fw3_log_ptr is initialised NULL at ip_fw3.c:134 and NEVER assigned anywhere in sys/ (grep confirms only 5 references, all reads); (2) even when wired, installing any ipfw3 rule requires IP_FW_X setsockopt on a raw IP socket, gated by caps_priv_check(SYSCAP_NONET_RAW) at raw_ip.c:473 = root-only. The 30-line harness kld (harness/harness.c) wires the pointer to prove the primitive at the object level per Phase-4 case (d); it is not part of any escalation chain.