DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0668

Unbounded table id in all dispatch/ioctl handlers -> per-CPU heap OOB write, OOB read, and wild function-pointer call

Summary

Every table op indexed by user-controlled ioc_table->id (int) performs table_ctx+=id against per-CPU array ctx->table_ctx[IPFW_TABLES_MAX=32] with NO bounds check anywhere. table_create_dispatch:92-95 reads int id=ioc_table->id table_ctx+=id then writes type/count/name(32B) + rn_inithead writes 2 kernel ptrs at OOB offset. Same at delete:127 append:149 remove:205 flush:244 rename:265. Read paths show:377-379 test:431-433 equally broken. Show calls rnh->rnh_walktree (function ptr off48) through OOB pointer = wild call RCE with grooming. Dispatch tour netisr_forwardmsg_all replicates corruption every CPU. Attacker: root/SYSCAP_NONET_RAW setsockopt(IPPROTO_IP,IP_FW_X,opcode 73-81). Impact: deterministic heap corruption + wild function-ptr call heap-groomable priv-esc/RCE. Jail boundary broken jailed root allow.raw_sockets corrupts host kernel. Fix: if(id<0||id>=IPFW_TABLES_MAX) goto done in every handler.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0668 · 13 files
FileTypeDescriptionSize
ipfw3_table_oob.c trigger-source setsockopt IP_FW_X TABLE_CREATE with attacker id -> OOB 2.7 KB view raw
build.sh build-script cc ipfw3_table_oob.c 208 B view raw
run.sh run-script set default-accept, kldload ipfw3/basic, run binary 328 B view raw
build.log build-log fixed ipfw3_basic.ko module build 1.7 KB view raw
run.log run-log baseline: unpriv EPERM + root id=16384 success(0) + id=0x10000000 panic 1.3 KB view raw
fix_run.log run-log fixed: id=16384 EINVAL, id=0x10000000 EINVAL no panic, id=5 valid 0 736 B view raw
panic.txt panic-signature fatal trap 12 in table_create_dispatch+0x45 179 B view raw
fix.diff suggested-fix bounds check id in all 6 table dispatch functions 2.1 KB view raw
env.txt environment uname / kern.version / cc / module state 335 B view raw
VERDICT.md verdict full narrative, root-only reachability, primitive, fix validation 5.3 KB ↓ raw
README.md readme build/run/expected 1.4 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 build/run/expected
↓ download raw

DF-0668 PoC — ipfw3 table op OOB via unchecked ioc_table->id

Build (as any user; runtime needs root)

cd findings/poc/DF-0668
./build.sh        # -> ipfw3_table_oob

Run (as root — needs a raw IP socket + ipfw3 loaded)

./run.sh [id]     # default id=0x4000

run.sh sets net.filters_default_to_accept=1 first so that loading ipfw3 (whose default policy is DENY) does not cut off your ssh session.

Expected (bug present, unfixed ipfw3_basic)

  • As an unprivileged user: socket(SOCK_RAW)Operation not permitted (root-only).
  • As root:
  • id=16384setsockopt returned 0 (OOB accepted — a checked impl would EINVAL).
  • id=0x10000000 → kernel panic: Fatal trap 12: page fault while in kernel mode Stopped at table_create_dispatch+0x45: movl $0,0x30(%rbx)

Expected (after applying fix.diff to /usr/src/sys/net/ipfw3_basic/ip_fw3_table.c and

rebuilding/loading ipfw3_basic.ko) - id=16384 and id=0x10000000setsockopt returned -1 errno=22 (Invalid argument). - valid id=5setsockopt returned 0 (legit tables still work).

Notes

  • Loading ipfw3 enables packet filtering with default DENY; you MUST set net.filters_default_to_accept=1 before kldload ipfw3 or you lose network access.
  • Rebuilding just the module: cd /usr/src/sys/net/ipfw3_basic && make, then load ipfw3_basic.ko after kldload ipfw3.
VERDICT.md verdict full narrative, root-only reachability, primitive, fix validation
↓ download raw

DF-0668 — ipfw3 table op OOB via unchecked ioc_table->id

Verdict: REPRODUCED (panic in cited function + fix validated)

Every ipfw3 table dispatch handler indexes a per-CPU array ctx->table_ctx[IPFW_TABLES_MAX=32] with a user-controlled int id and no bounds check:

/* sys/net/ipfw3_basic/ip_fw3_table.c:84-95 (table_create_dispatch) */
ioc_table = tbmsg->ioc_table;
int id = ioc_table->id;            /* from user, unchecked */
table_ctx = ctx->table_ctx;
table_ctx += id;                   /* OOB pointer arithmetic */
table_ctx->type = ioc_table->type; /* write */
table_ctx->count = 0;              /* write */
strlcpy(table_ctx->name, ...);     /* write */
rn_inithead(&table_ctx->mask, ...);/* deposits a kernel pointer at OOB */
rn_inithead(&table_ctx->node, ...);/* deposits a kernel pointer at OOB */

The identical unchecked table_ctx += ioc_tbl->id exists in table_delete_dispatch (:127), table_append_dispatch (:149), table_remove_dispatch (:205), table_flush_dispatch (:244), table_rename_dispatch (:265). ctx->table_ctx is kmalloc(32*sizeof(struct ipfw3_table_context)=1792, M_IPFW3_TABLE) (ip_fw3_table.c:570); any id >= 32 walks past it. IPFW_TABLES_MAX=32 is defined but never used to validate id anywhere.

Reachability — ROOT-ONLY (valid hard blocker for uid=0)

The sink is reached via setsockopt(IPPROTO_IP, IP_FW_X, ...) on a raw IP socket:

setsockopt(IPPROTO_IP, IP_FW_X=49, [x_header{opcode=IP_FW_TABLE_CREATE=73}]
                                    [struct ipfw_ioc_table{id,type,...}])
  -> raw_ip.c rip_ctloutput -> ip_fw3_sockopt -> ip_fw3_ctl (IP_FW_X)
  -> ip_fw3_ctl_x (strip x_header, sopt_name=73) -> ip_fw3_ctl (IP_FW_TABLE_CREATE)
  -> ip_fw3_ctl_table_ptr -> ip_fw3_ctl_table_create -> table_create_dispatch [per CPU]

Creating a raw IP socket requires caps_priv_check(SYSCAP_NONET_RAW) (sys/netinet/raw_ip.c:473). An unprivileged user gets EPERM:

$ ./ipfw3_table_oob 33            # as maxx (uid 1001)
socket(SOCK_RAW) failed: Operation not permitted (expected for unpriv)

So the write is reachable only from an already-root context. This is a root→kernel hardening gap (a root user with ipfw3 access can corrupt arbitrary kernel memory), not an unprivileged local privilege escalation. There is no unprivileged→uid=0 chain because the privilege boundary (raw-socket capability) is not crossed.

Primitive characterization (measured on this guest)

property value
allocation overflowed kmalloc(1792, M_IPFW3_TABLE) per CPU (ctx->table_ctx)
write offset id * sizeof(struct ipfw3_table_context) = id * 56 bytes (fully attacker-controlled via id)
fields written type (attacker int), count (0), name[32] (attacker bytes), + 2 radix_node_head kernel pointers (rn_inithead)
small OOB (id=33..N) silent corruption of adjacent M_IPFW3_TABLE slab / kmalloc arena
large OOB (id=0x10000000) page fault, panic in table_create_dispatch+0x45 (movl $0,0x30(%rbx) = table_ctx->count = 0)

Decisive baseline evidence (id=0x10000000, root):

Fatal trap 12: page fault while in kernel mode
Stopped at      table_create_dispatch+0x45:     movl    $0,0x30(%rbx)

Escalation ceiling

None for an unprivileged user (root-only reachability — see above). For a root caller the primitive is a fully-controlled kernel heap write (chosen 56-byte-stride offset + controlled type/name bytes + kernel pointer deposits), trivially gameable for root→kernel code execution, but root→kernel is already game-over. The realistic finding value is a privilege-boundary / robustness bug: ipfw3 must never trust ioc_table->id, and any future unprivileged ipfw3 control path (e.g. a delegated socket, jail, or setuid helper) would turn this into local kernel RCE.

PoC changes

  • Wrote ipfw3_table_oob.c: opens a raw IP socket, issues setsockopt(IPPROTO_IP, IP_FW_X, {x_header opcode=TABLE_CREATE} + {ioc_table id,type}). id is argv[1] (default 0x4000). Build/run via build.sh/run.sh (run.sh loads ipfw3 with net.filters_default_to_accept=1 so ssh survives the firewall).

Fix (fix.diff)

Add an id < 0 || id >= IPFW_TABLES_MAX bounds check (return EINVAL, forward the netmsg and return) to all six table dispatch functions (create/delete/append/ remove/flush/rename). Validated on a freshly-built ipfw3_basic.ko:

  • baseline: id=16384setsockopt returns 0 (OOB accepted/silent); id=0x10000000panic in table_create_dispatch.
  • fixed: id=16384 and id=0x10000000EINVAL (no panic); valid id=50 (legit tables still work).

The finding markdown had no per-finding proposal to supersede; this fix targets the root cause (missing bounds check at every table_ctx += id).

Kernel references (verified)

Fix verification

fixed

validated

see evidence pack

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Verdict

REPRODUCED. ipfw3 table id OOB: table_ctx[id] no bounds check, id=0x10000000 -> panic table_create_dispatch+0x45. Root-only (raw socket).