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

Kernel heap memory disclosure via ip_fw3_ctl_set_get unbounded bcopy from 4-byte field

Summary

ip_fw3_ctl_set_get copies sopt->sopt_valsize bytes from &ctx->sets but ctx->sets is single uint32_t(4 bytes) at tail of ~36-byte kmalloc struct ipfw3_context. sopt_valsize attacker-controlled up to 65536 non-root 32MB root via getsockopt never bounded to sizeof(ctx->sets). Overrun bytes fill correctly-sized sopt_val kernel buffer copyout back to caller leaking ~64KB adjacent kernel heap per call. &ctx->sets at offset 32 of 36-byte slab read runs off end of M_IPFW3 allocation into neighbouring heap. Requires raw IP socket NONET_RAW root or jail allow_raw_sockets. Repeatable KASLR-defeat heap-disclosure primitive.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2592 Β· 12 files
FileTypeDescriptionSize
poc.c trigger-source minimal leak trigger: raw socket + getsockopt(IP_FW_X,opcode=95) with large len 6.9 KB view raw
build.sh build-script cc -O2 -o poc poc.c 120 B view raw
run.sh run-script loads ipfw3 (safe default) and runs poc as root 451 B view raw
build.log build-log final successful build (BUILD_EXIT=0) 13 B view raw
run.log run-log baseline reproduction, 3 readlens, leaked kernel pointers 3.8 KB view raw
fix_run.log run-log patched-module re-run: 4 bytes back, 0 leaked 2.3 KB view raw
fix_build.log build-log ipfw3.ko module rebuild (MAKE_RC=0) 853 B view raw
leak_sample.txt leak-sample raw leaked bytes across runs incl. kernel text pointer 0xffffffff82600930 1.9 KB view raw
env.txt environment uname, cc, kldstat, patched ipfw3.ko sha256, sysctl 489 B view raw
panic.txt panic-signature UNRELATED rn_flush unload panic (recorded; not DF-2592) 817 B view raw
fix.diff suggested-fix clamp sopt_valsize to sizeof(ctx->sets) in ip_fw3_ctl_set_get 495 B view raw
VERDICT.md verdict full narrative: reproduced? how/why? fix validated? 6.5 KB ↓ raw
VERDICT.md verdict full narrative: reproduced? how/why? fix validated?
↓ download raw

DF-2592 β€” kernel heap memory disclosure via ip_fw3_ctl_set_get unbounded bcopy

Verdict

REPRODUCED on the unpatched 6.5-DEVELOPMENT #0 kernel (ipfw3.ko + ipfw3_basic.ko loaded). FIX VALIDATED on the same #0 kernel with a single-clamp rebuild of ipfw3.ko (sha256 f44f0a2a…). The leak is real: an attacker-controlled sopt->sopt_valsize drives an unbounded bcopy off the end of a 4-byte field, disclosing stale kernel heap (text/data pointers) back to userspace on every call.

The bug (confirmed line-by-line)

ip_fw3_ctl_set_get() in sys/net/ipfw3/ip_fw3_set.c:207:

int
ip_fw3_ctl_set_get(struct sockopt *sopt)
{
    struct ipfw3_context *ctx;
    ctx = fw3_ctx[mycpuid];
    bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize);   /* line 213 */
    return 0;
}

ctx->sets is a single uint32_t (4 bytes) β€” the last field of struct ipfw3_context (sys/net/ipfw3/ip_fw3.h:482, 5 fields = 36 bytes on amd64), allocated with kmalloc(LEN_FW3_CTX, M_IPFW3, M_WAITOK|M_ZERO) (ip_fw3.c:1410, lands in the kmalloc-64 bucket). sopt->sopt_valsize is the caller-supplied getsockopt length, never clamped to sizeof(ctx->sets). The bcopy therefore reads sopt_valsize - 4 bytes past the sets field β€” through the zeroed intra-chunk padding, then into neighbouring slab chunks / adjacent pages β€” and copies all of it into the reply buffer, which sys_getsockopt then copyouts to the caller (sys/kern/uipc_syscalls.c:1349).

Reach path (getsockopt, all in-kernel):

getsockopt(raw_ip_sock, IPPROTO_IP, IP_FW_X=49, buf, &len)   /* len attacker-controlled */
  sys_getsockopt        (uipc_syscalls.c:1303)   sopt_valsize=len; sopt_val=kmalloc(len)
  -> sogetopt -> rip_ctloutput  (raw_ip.c:327 SOPT_GET / case IP_FW_X:335)
  -> ip_fw3_sockopt             (ip_fw3_glue.c:51)
  -> ip_fw3_ctl_x               (ip_fw3.c:1039)  strips 4-byte ip_fw_x_header{opcode=95},
                                                  sopt_valsize -= 4, shifts sopt_val
  -> ip_fw3_ctl                 (ip_fw3.c:1070 case IP_FW_SET_GET=95)
  -> ip_fw3_ctl_set_sockopt     (ip_fw3_set.c:256)
  -> ip_fw3_ctl_set_get         (ip_fw3_set.c:207)  *** unbounded bcopy -> OOB read ***

sopt_valsize is bounded only by SOMAXOPT_SIZE=65536 (non-root) / SOMAXOPT_SIZE0=32MB (root) at uipc_syscalls.c:1324 β€” so the attacker chooses the read length (minus the 4-byte IP_FW_X header).

Reproduction (unpatched #0 kernel)

Run as root after sysctl net.filters_default_to_accept=1; kldload ipfw3; kldload ipfw3_basic (the latter survives ssh while the firewall is up):

# /tmp/poc2592 4096
[*] getsockopt(IPPROTO_IP, IP_FW_X=49, opcode=IP_FW_SET_GET=95) readlen=4096
[+] raw socket opened fd=3
[*] getsockopt rc=0 errno=0 returned len=4092          <-- 4092 bytes back from a 4-byte field
[!] LEAK CONFIRMED: 477 nonzero bytes returned past the 4-byte sets field
offset 0x10: 80 98 0a 4f 00 f8 ff ff  -> 0xfffff8004f0a9880   (kernel direct-map pointer)
offset 0x18: 30 09 60 82 ff ff ff ff  -> 0xffffffff82600930   (inside ipfw3.ko text @ 0xffffffff82600000)

The leaked 0xffffffff82600930 is a kernel text pointer into the just-loaded ipfw3.ko module (kldstat base 0xffffffff82600000) β€” a definitive KASLR-defeat / kernel-base disclosure. Identical residue across runs (4096/2048/8192) reflects a deterministic heap layout on this boot; the values are genuine heap pointers, not a static/cosmetic buffer.

Privilege analysis (why this is a leak, not an escalation)

The only entry to ip_fw3_ctl_* is rip_ctloutput, the pr_ctloutput of the raw-IP protocol. Creating such a socket runs rip_attach (raw_ip.c:460) which gates on caps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW) (raw_ip.c:473) — i.e. root (or a jail with allow_raw_sockets, which still grants it only to jail-root). An unprivileged user cannot enter the bug path. Per the Phase-6 bright-line rule, root-only reachability is a valid hard blocker for escalation, but the finding class here is info leak, not memory corruption: there is no chain to develop. The realistic impact ceiling is a root→kernel heap disclosure / KASLR-defeat primitive (a hardening gap; root can already read kernel memory via /dev/mem, kmem, etc., so practical privilege gain is nil, but the uninitialised-kernel-memory exposure is real and worth fixing).

The fix

One clamp, at the root cause (ip_fw3_set.c:213):

/* DF-2592: ctx->sets is a single uint32_t; never copy more than that,
 * otherwise we read (and disclose) stale kernel heap past the field. */
if (sopt->sopt_valsize > sizeof(ctx->sets))
    sopt->sopt_valsize = sizeof(ctx->sets);
bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize);

Standalone git apply-able diff: fix.diff. Supersedes the generic "zero/M_ZERO the response" idea in the finding prompt β€” the real defect is an unbounded read length, not an uninitialised stack struct, so clamping the length (not zeroing the destination) is the correct minimal fix.

Fix validation (rebuilt ipfw3.ko, reloaded, re-ran same PoC)

  • Before (unpatched #0 kernel + #0 module, sha ca6ccfb9…): getsockopt(len=4096) β†’ returns len=4092, 477 nonzero bytes past the 4-byte sets field, including kernel text pointer 0xffffffff82600930.
  • After (#0 kernel + rebuilt single-fix ipfw3.ko, sha f44f0a2a…): getsockopt(len=4096) β†’ returns len=4, 0 nonzero bytes past offset 4 (only the valid ctx->sets value is copied out). Repeated at readlen 4096/8192 β€” deterministic.
  • Rebuilt via cd /usr/src/sys/net/ipfw3 && make KERNBUILDDIR=…/X86_64_GENERIC (module-only; MAKE_RC=0), installed to /boot/kernel/ipfw3.ko, clean reboot to load (kldunload of the active firewall triggers a separate rn_flush panic β€” see panic.txt β€” unrelated to DF-2592).

fix_status = fixed (clean before/after, deterministic across readlen and runs).

Files

  • poc.c β€” minimal leak trigger (raw socket + getsockopt(IP_FW_X) with opcode 95).
  • build.sh / run.sh β€” exact build & run (run as root after loading ipfw3).
  • run.log β€” baseline reproduction (3 readlens, leaked kernel pointers).
  • fix_run.log β€” patched-module re-run (4 bytes back, 0 leaked).
  • fix_build.log β€” module rebuild output (MAKE_RC=0).
  • leak_sample.txt β€” raw leaked bytes across runs (kernel text/data pointers).
  • env.txt β€” guest uname / cc / kld / sha / sysctl state.
  • panic.txt β€” the unrelated rn_flush unload panic (recorded for completeness).
  • fix.diff β€” the standalone git-apply-able clamp fix.
  • manifest.json β€” artifact catalog.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: ./poc 4096 leaked 477 nonzero bytes (incl. kernel text ptr 0xffffffff82600930) on unpatched ipfw3.ko baseline (ca6ccfb9...) and leaks ZERO on single-fix rebuilt ipfw3.ko (f44f0a2a...) β€” getsockopt now returns len=4 (only valid ctx->sets field), deterministic across readlen 4096/8192. Clamp closes the bug.

baseline (unpatched ipfw3.ko): getsockopt returned len=4092; LEAK CONFIRMED 477 nonzero bytes past 4-byte sets field; hexdump 80980a4f00f8ffff 30096082ffffffff (kernel ptrs). patched (ipfw3.ko f44f0a2a...): getsockopt returned len=4; got=4 bytes; nonzero total=0, after[4]=0 -> no nonzero bytes past offset 4.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; single-fix applied to ipfw3.ko MODULE only β€” rebuilt via cd /usr/src/sys/net/ipfw3 && make KERNBUILDDIR=.../X86_64_GENERIC, sha256 f44f0a2aa38c4a4a0f581b1133f34a736c3e19eb55ed715703c41d297daf3ce5, clean-reboot loaded)

Confirmed kernel references

Detail

Exploit chain

none β€” pure kernel-heap INFO LEAK, not memory-corruption primitive, no escalation chain to develop. Leak path reachable only through raw-IP socket whose creation gates on caps_priv_check(SYSCAP_NONET_RAW) (raw_ip.c:473) = root; unprivileged user cannot enter, so root-only reachability valid hard blocker. Realistic impact ceiling: root->kernel heap disclosure / KASLR-defeat primitive (hardening gap: root can already read kernel memory via /dev/mem, so practical privilege gain nil, but uninitialized-kernel-memory exposure real).

Evidence (decisive lines)

baseline (unpatched #0): getsockopt(IPPROTO_IP,IP_FW_X=49,opcode=95,len=4096) rc=0 returned len=4092; got=4092 bytes back; nonzero total=477, after[4]=477, after[36]=462; ctx->sets=0x00000000; LEAK CONFIRMED. hexdump offset 0x10: 80 98 0a 4f 00 f8 ff ff -> 0xfffff8004f0a9880 (kmem ptr); offset 0x18: 30 09 60 82 ff ff ff ff -> 0xffffffff82600930 (ipfw3.ko text). Identical residue at readlen 4096/2048/8192. patched module: getsockopt returned len=4, got=4, nonzero after[4]=0 -> leak gone.

PoC changes

Authored new poc.c (dir empty): opens raw IP socket, churns heap (pipes/sockets) to populate slab residue, issues getsockopt(IPPROTO_IP, IP_FW_X=49) with 4-byte ip_fw_x_header{opcode=95} + large payload, scans returned bytes past 4-byte ctx->sets field for non-zero kernel-pointer leaks with hexdump. build.sh, run.sh, fix.diff (clamp sopt_valsize to sizeof(ctx->sets)).

Verified recommended fix

In ip_fw3_ctl_set_get (sys/net/ipfw3/ip_fw3_set.c:213) clamp sopt_valsize to sizeof(ctx->sets) before bcopy: 'if (sopt->sopt_valsize > sizeof(ctx->sets)) sopt->sopt_valsize = sizeof(ctx->sets);'. Supersedes 'zero/M_ZERO the response' idea β€” real defect is unbounded READ length (not uninitialized stack struct), so clamping length (not zeroing destination) is correct minimal fix. Full git-apply-able diff in findings/poc/DF-2592/fix.diff.

Verdict

REPRODUCED. ip_fw3_ctl_set_get (sys/net/ipfw3/ip_fw3_set.c:207-215) does bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize) where ctx->sets is a single uint32_t (4 bytes, last field of 36-byte kmalloc-64 struct ipfw3_context) and sopt_valsize is the attacker-controlled getsockopt length, never clamped to sizeof(ctx->sets). Confirmed on unpatched #0 kernel (ipfw3.ko+ipfw3_basic.ko loaded): getsockopt(IPPROTO_IP, IP_FW_X=49, opcode=IP_FW_SET_GET=95, len=4096) -> rip_ctloutput -> ip_fw3_ctl_x -> ip_fw3_ctl_set_get -> OOB bcopy reads 4088 bytes past the 4-byte field. Reply copyout'd back, disclosing stale kernel heap including kernel text pointer 0xffffffff82600930 (inside ipfw3.ko @ base 0xffffffff82600000) and direct-map pointer 0xfffff8004f0a9880. 477 nonzero leaked bytes per call at readlen=4096; attacker controls read length up to 65536 (non-root)/32MB (root).