DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0739

Heap over-read / kernel memory disclosure in ip_fw3_ctl_set_get — bcopy sopt_valsize bytes from 4-byte ctx->sets

Summary

ip_fw3_set.c:213 bcopy(&ctx->sets,sopt->sopt_val,sopt->sopt_valsize) — NO bounds check. ctx->sets is uint32_t (4 bytes) LAST field of struct ipfw3_context (ip_fw3.h:489). sopt_valsize attacker-controlled up to 65536 (raw socket) or 32MiB (root). Any valsize>4 reads kernel heap past fw3_ctx[0] allocation returned verbatim to userspace via getsockopt copyout = KASLR bypass kernel address leak. Large valsize walks off mapped slab page panics kernel = DoS. Trigger: getsockopt(IPPROTO_IP,IP_FW_X=49) with x_header.opcode=IP_FW_SET_GET(95) large valsize. Same pattern as DF-0670 (ip_fw3_table sopt_valsize). Fix: if(sopt_valsize!=sizeof(uint32_t)) return EINVAL; bcopy 4 bytes only.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0739 · 14 files
FileTypeDescriptionSize
df0739.c trigger-source getsockopt(IPPROTO_IP, IP_FW_X, IP_FW_SET_GET) over-read trigger; hexdumps + counts leaked 0xffff... kernel pointers 4.1 KB view raw
build.sh build-script cc -O2 -o df0739 df0739.c 155 B view raw
run.sh run-script sets net.filters_default_to_accept=1, kldload ipfw3, runs ./df0739 [readlen] 664 B view raw
build.log build-log clean build as maxx (uid 1001) 64 B view raw
run.log run-log baseline decisive run on unpatched #0 ipfw3.ko (12 kernel pointers leaked) 1.2 KB view raw
leak_sample.txt leak-sample pointer-leak sample (8-12 KVA ptrs) + 3-run string-leak sample (csh/sftp-server/getty/ttyv*) 3.2 KB view raw
env.txt environment uname, cc version, sysctl, kldstat, ipfw3.ko sha256 488 B view raw
fix.diff suggested-fix clamp bcopy to sizeof(ctx->sets)=4; set sopt_valsize; git-apply-able (supersedes/matches finding proposal) 715 B view raw
fix_build.log build-log single-module rebuild of ipfw3.ko, rc=0 1.9 KB view raw
fix_run.log run-log patched-module re-run: returns exactly 4 bytes, no over-read 784 B view raw
VERDICT.md verdict full narrative: mechanism, reachability, before/after, fix validation 8.6 KB ↓ raw
README.md readme human-facing build/run/expected + reachability notes 2.1 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 build/run/expected + reachability notes
↓ download raw

DF-0739 — ip_fw3_ctl_set_get heap over-read / kernel memory disclosure

Severity: Medium (info leak) · Status: REPRODUCED, FIX VALIDATED Cited bug: sys/net/ipfw3/ip_fw3_set.c:213bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize) with no bounds check; ctx->sets is a 4-byte uint32_t, the LAST field of struct ipfw3_context.

Build

./build.sh      # cc -O2 -o df0739 df0739.c

Run (root only — see reachability)

./run.sh [readlen]      # default 256; tries readlen up to 65536

run.sh sets net.filters_default_to_accept=1 and kldload ipfw3 first (idempotent), so loading the firewall does not cut off ssh.

Expected

Bug present (unpatched #0 ipfw3.ko, sha256 ca6ccfb9…)

getsockopt returned <readlen> bytes (requested <readlen+4> payload after 4-byte x_header)
Over-read past ctx->sets: <readlen-4> bytes (heap residue):
  ...kernel pointers (0xffff...) and adjacent-slab strings (getty/ttyv*/csh/...)...
candidate 64-bit kernel pointers (0xffff...): N>0

A 128-byte request leaks ~124 bytes of heap incl. up to 12 kernel pointers; a 1024-byte request leaks 1020 bytes.

Fixed (patched ipfw3.ko, sha256 241f263a…)

getsockopt returned 4 bytes (requested <readlen+4> payload after 4-byte x_header)
ctx->sets (first 4 bytes, the only legitimate field): ...
len <= 4: no over-read observed

Exactly the 4 valid bytes of ctx->sets; zero heap residue.

Reachability

  • ipfw3.ko must be loaded (kldload ipfw3, root).
  • IP_FW_X is handled only by rip_ctloutput (sys/netinet/raw_ip.c), reachable solely via SOCK_RAW, which needs SYSCAP_NONET_RAW (root).
  • An unprivileged user (maxx) gets EPERM on socket(SOCK_RAW,…) and ENOPROTOOPT on IP_FW_X over SOCK_DGRAM.

=> This is a root-reachable heap info leak, not an unpriv→root escalation.

Fix

fix.diff clamps the copy to sizeof(ctx->sets) (4 bytes) and sets sopt_valsize so the copyout returns only those 4 bytes. Validated on a rebuilt ipfw3.ko (module-scoped single-file fix; no kernel rebuild needed).

VERDICT.md verdict full narrative: mechanism, reachability, before/after, fix validation
↓ download raw

DF-0739 — ip_fw3_ctl_set_get heap over-read / kernel memory disclosure

Verdict: REPRODUCED (root-reachable heap over-read / kernel pointer + string leak). FIX VALIDATED.

The bug

sys/net/ipfw3/ip_fw3_set.c:206-215

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);   /* <-- BUG */
        return 0;
}

ctx->sets is a single uint32_t and is the LAST field of struct ipfw3_context (sys/net/ipfw3/ip_fw3.h:482-490):

struct ipfw3_context {
        struct ip_fw                  *rules;
        struct ip_fw                  *default_rule;
        struct ipfw3_state_context    *state_ctx;
        struct ipfw3_table_context    *table_ctx;
        uint32_t                       sets;     /* offset 32; struct ends at 36 */
};

sopt->sopt_valsize is attacker-controlled (capped at SOMAXOPT_SIZE = 65536 for non-root, SOMAXOPT_SIZE0 = 32 MiB for root — sys/kern/uipc_syscalls.c:1319-1330, sys/sys/socket.h:356-357). Any valsize > 4 makes the bcopy read past the fw3_ctx[cpu] allocation into adjacent kernel heap, and those bytes are returned verbatim to userspace by sys_getsockopt's copyout(sopt->sopt_val, uap->val, sopt->sopt_valsize) (sys/kern/uipc_syscalls.c:1349).

There is no bounds check anywhere on this path — confirmed by reading ip_fw3_ctl_set_get, ip_fw3_ctl_set_sockopt, ip_fw3_ctl, ip_fw3_ctl_x, ip_fw3_sockopt, and rip_ctloutput.

Trigger path (reachability)

  1. ipfw3.ko must be loaded (kldload ipfw3 — root-only). This is the acceptable operational precondition (a firewall admin loads ipfw3).
  2. The IP_FW_X (=49, sys/netinet/in.h:389) socket option is handled only by rip_ctloutput (sys/netinet/raw_ip.c:334 GET / :385 SET), which is the pr_ctloutput for every SOCK_RAW inet protocol (sys/netinet/in_proto.c — all rip_ctloutput entries are SOCK_RAW).
  3. Creating a raw IP socket requires caps_priv_check(SYSCAP_NONET_RAW) (sys/netinet/raw_ip.c:473), i.e. root. Confirmed empirically: maxx (uid 1001) gets EPERM on socket(AF_INET, SOCK_RAW, …), and a SOCK_DGRAM socket gets ENOPROTOOPT (errno 42) on IP_FW_X.

=> The live trigger is root-reachable. This is a root→kernel heap information leak. It is not an unprivileged→kernel escalation. The realistic impact ceiling is: a process holding SYSCAP_NONET_RAW (or uid 0) on a host where ipfw3 is loaded can disclose up to 64 KiB (32 MiB for root) of kernel heap per call, including kernel pointers and the contents of adjacent slab objects — bypassing any securelevel >0 kernel-memory read restriction. With sufficient calls / heap grooming this is a KASLR-defeat and a building block for further exploitation. No memory-corruption primitive is obtained, so no uid=0 escalation chain is derivable from this finding alone.

Data flow (getsockopt)

user: getsockopt(s, IPPROTO_IP, IP_FW_X=49, buf[=x_header{opcode=IP_FW_SET_GET=95}+room], &len)
sys_getsockopt                      sys/kern/uipc_syscalls.c:1303
  sopt.sopt_valsize = len           (1319)  capped SOMAXOPT_SIZE[0]
  sopt.sopt_val = kmalloc(len,M_TEMP)        (1332)
  copyin(uap->val, sopt.sopt_val, len)       (1336)
  kern_getsockopt -> sogetopt -> rip_ctloutput
rip_ctloutput  case IP_FW_X (SOPT_GET)       sys/netinet/raw_ip.c:334
  -> ip_fw3_sockopt                          sys/net/ipfw3/ip_fw3_glue.c:51
     -> ip_fw_ctl_x_ptr = ip_fw3_ctl_x       sys/net/ipfw3/ip_fw3.c:1038
        sopt_valsize -= sizeof(ip_fw_x_header) /*4*/   (1044)
        bcopy(++x_header, sopt_val, sopt_valsize)      (1045)  /* shift past hdr */
        sopt_name = IP_FW_SET_GET                      (1043)
        -> ip_fw3_ctl case IP_FW_SET_GET               (1070)
           -> ip_fw3_ctl_set_sockopt                   (256)
              -> ip_fw3_ctl_set_get(sopt)              (206)
                 bcopy(&ctx->sets, sopt_val, sopt_valsize)  *** OVER-READ ***
  copyout(sopt_val, uap->val, sopt_valsize)            (1349)  *** LEAK TO USER ***

Reproduction (observed on the unpatched #0 kernel)

./df0739 128 (root, ipfw3 loaded):

getsockopt returned 128 bytes (requested 132 payload after 4-byte x_header)
ctx->sets (first 4 bytes, the only legitimate field):
  0000: 00 00 00 00
Over-read past ctx->sets: 124 bytes (heap residue):
  0000: 00 00 00 00 00 00 00 00 00 00 00 00 80 26 0a 4f
  0010: 00 f8 ff ff 30 09 60 82 ff ff ff ff 00 00 00 00
  0030: 00 00 00 00 20 21 0a 4f 00 f8 ff ff 20 0b 60 82
  0040: ff ff ff ff 00 00 00 00 00 00 00 00 02 00 00 00
  ...
non-zero bytes in over-read region: 60 / 124
candidate 64-bit kernel pointers (0xffff...): 12

Decoded leaked 64-bit kernel pointers include 0xffffff80_4f0a2680, 0xffffff82_82600930, 0xffffff80_4f0a2120, 0xffffff82_82600b20 — genuine KVA addresses. Across runs the over-read also disclosed the contents of adjacent slab objects holding process/path strings: csh, -c, /usr/libexec/sftp-server, /usr/libexec/getty, ttyv0, ttyv3, ttyv5. The leak content varies with slab layout (different bytes across reset/reload), confirming it is live kernel heap, not a fixed buffer.

Unprivileged reachability (maxx, uid 1001) — confirmed not reachable:

socket(SOCK_RAW) failed: Operation not permitted (need SYSCAP_NONET_RAW / root)
DGRAM getsockopt(IP_FW_X) rc=-1 errno=42 Protocol not available

Exploit chain

None — this is a pure heap over-read / information disclosure, not a write/corruption primitive. No uid=0 escalation is derivable. Impact ceiling: kernel heap pointer + string disclosure (KASLR-defeat / securelevel read-bypass building block), up to 64 KiB per call (non-root raw) / 32 MiB (root). The leak is read-only; the realistic worst case is information disclosure that aids a separate write-class bug.

Fix (fix.diff — VALIDATED)

Clamp the copy to the 4 valid bytes of ctx->sets and set sopt_valsize accordingly so copyout only returns those 4 bytes:

 if (sopt->sopt_valsize < sizeof(ctx->sets))
         return (EINVAL);
 sopt->sopt_valsize = sizeof(ctx->sets);
 bcopy(&ctx->sets, sopt->sopt_val, sizeof(ctx->sets));

This matches the finding proposal (bound the bcopy to sizeof(uint32_t)).

Phase 8 — fix validation (module-scoped, single file in ipfw3.ko)

The fix lives entirely in sys/net/ipfw3/ip_fw3_set.c, which compiles into the loadable ipfw3.ko (not the base kernel). So a single-module rebuild + reload is the correct validation, faster than a full kernel rebuild.

  • Baseline (/boot/kernel/ipfw3.ko sha256 ca6ccfb9…, unpatched #0 kernel 6.5-DEVELOPMENT): ./df0739 128 returns 128 bytes — 124-byte heap over-read with 12 kernel pointers + adjacent-slab strings. ./df0739 1024 returns 1024 bytes — 1020-byte over-read. (run.log, leak_sample.txt)
  • Applied fix.diff to /usr/src/sys/net/ipfw3/ip_fw3_set.c (patch -p1Hunk #1 succeeded at 210).
  • Built the module: cd /usr/src/sys/net/ipfw3 && make obj && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERICrc=0 (fix_build.log).
  • Installed: cp /usr/obj/usr/src/sys/net/ipfw3/ipfw3.ko /boot/kernel/ipfw3.ko (sha256 241f263a…). kldload ipfw3 loads the fixed module.
  • Patched re-run: ./df0739 128 and ./df0739 1024 both return exactly 4 bytes (ctx->sets), len <= 4: no over-read observed, deterministically across 4 runs. (fix_run.log)

Result: fixed. The over-read is gone on the patched module and present on the unpatched baseline — clean before/after.

PoC changes

The finding's poc_path was registered but the folder/markdown were not yet materialized on disk, so the trigger PoC (df0739.c), build.sh, run.sh, fix.diff, and all logs were authored fresh by this verification run. The PoC exercises the exact cited path: getsockopt(IPPROTO_IP, IP_FW_X=49) with an ip_fw_x_header{opcode=IP_FW_SET_GET=95} and an oversized valsize, hex-dumps the over-read region, and counts leaked kernel pointers.

Notes

  • ipfw3 is loaded by default with default to deny, which cuts ssh off the moment it loads. The run.sh sets net.filters_default_to_accept=1 first so the firewall allows ssh while the module is loaded. This sysctl is read by ipfw3 at load time (sys/net/ipfw3/ip_fw3.c:1425).
  • Reachability for an unprivileged user was specifically tested and denied (SYSCAP_NONET_RAW required). Honest impact: root-reachable heap info leak, not unpriv→root. The Medium severity in the finding is appropriate for an info-leak / KASLR-defeat building block reachable once ipfw3 is loaded.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. fix.diff applies clean (patch -p1 'Hunk #1 succeeded at 210'); single-module rebuild of ipfw3.ko rc=0 (make obj && make KERNBUILDDIR=...); installed over /boot/kernel/ipfw3.ko and reloaded. BASELINE unpatched module leaked 124 bytes incl. 12 kernel pointers on a 128B getsockopt (and 1020 bytes on a 1024B getsockopt). PATCHED module returns exactly 4 bytes (ctx->sets) with zero over-read, deterministically across 4 runs (128B x3 + 1024B x1). The over-read is closed => fix_status=fixed.

BEFORE (unpatched ipfw3.ko ca6ccfb9...): ./df0739 128 -> 'returned 128 bytes', 'Over-read past ctx->sets: 124 bytes (heap residue)', 'candidate 64-bit kernel pointers (0xffff...): 12'; ./df0739 1024 -> 'returned 1024 bytes', 1020-byte over-read. AFTER (fixed ipfw3.ko 241f263a...): ./df0739 128 -> 'returned 4 bytes', 'len <= 4: no over-read observed'; ./df0739 1024 -> 'returned 4 bytes', 'no over-read observed' (4/4 runs).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (same #0 base kernel; fix is module-scoped to ipfw3.ko -- rebuilt single module from patched sys/net/ipfw3/ip_fw3_set.c, sha256 ca6ccfb9... -> 241f263a..., reloaded via kldload)

Confirmed kernel references

Detail

Exploit chain

none -- this is a pure heap over-read / information disclosure (read past a 4-byte field), not a write/UAF/double-free/type-confusion primitive, so no memory-corruption escalation chain is derivable. Impact ceiling characterized: per-call disclosure of up to 64KiB (non-root raw socket) / 32MiB (root) of kernel heap, including kernel pointers (observed 12 x 0xffff... KVA in a 128-byte read) and contents of adjacent slab objects (process/path strings); repeated calls / heap grooming yields a KASLR-defeat and a building block that would aid a separate write-class bug. No corruption => no uid=0 path. File containing the trigger: findings/poc/DF-0739/df0739.c.

Evidence (decisive lines)

BASELINE (unpatched #0 ipfw3.ko, sha256 ca6ccfb9...), ./df0739 128:
  getsockopt returned 128 bytes (requested 132 payload after 4-byte x_header)
  Over-read past ctx->sets: 124 bytes (heap residue):
  0000: 00 00 00 00 00 00 00 00 00 00 00 00 80 26 0a 4f
  0010: 00 f8 ff ff 30 09 60 82 ff ff ff ff 00 00 00 00
  0030: 00 00 00 00 20 21 0a 4f 00 f8 ff ff 20 0b 60 82
  0040: ff ff ff ff ...
  candidate 64-bit kernel pointers (0xffff...): 12
PATCHED (fixed module, sha256 241f263a...), ./df0739 128:
  getsockopt returned 4 bytes (requested 132 payload after 4-byte x_header)
  ctx->sets (first 4 bytes, the only legitimate field): 0000: 00 00 00 00
  len <= 4: no over-read observed
Unpriv reach: maxx socket(SOCK_RAW)=EPERM; DGRAM IP_FW_X=ENOPROTOOPT(42).

PoC changes

poc_path was registered in audit.db but the folder/markdown were not materialized on disk, so this verification authored everything fresh: df0739.c (trigger: getsockopt(IPPROTO_IP,IP_FW_X=49) with ip_fw_x_header.opcode=IP_FW_SET_GET=95 + oversized valsize, hexdumps over-read, counts 0xffff... kernel pointers), build.sh, run.sh (sets net.filters_default_to_accept=1 then kldload ipfw3 so loading the default-deny firewall does not cut ssh), fix.diff (clamp bcopy to sizeof(ctx->sets)), and VERDICT.md/README.md/manifest.json plus full build/run/fix_build/fix_run logs and leak_sample.txt.

Verified recommended fix

In ip_fw3_ctl_set_get (sys/net/ipfw3/ip_fw3_set.c:206-215), bound the copy to the 4 valid bytes: add if (sopt->sopt_valsize < sizeof(ctx->sets)) return (EINVAL); sopt->sopt_valsize = sizeof(ctx->sets); then bcopy(&ctx->sets, sopt->sopt_val, sizeof(ctx->sets)); so the getsockopt copyout (uipc_syscalls.c:1349) returns only the legitimate uint32_t. Matches the finding proposal (bound the bcopy to sizeof(uint32_t)); the full git-apply-able diff is in findings/poc/DF-0739/fix.diff.

Verdict

REPRODUCED. The bug at sys/net/ipfw3/ip_fw3_set.c:213 is real and confirmed: ip_fw3_ctl_set_get() does bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize) with NO bounds check, but ctx->sets is a single uint32_t (the LAST field of struct ipfw3_context, ip_fw3.h:482-490) and sopt->sopt_valsize is attacker-controlled (capped SOMAXOPT_SIZE=65536 non-root / 32MiB root, uipc_syscalls.c:1319-1330). Any valsize>4 reads kernel heap past fw3_ctx[cpu] and sys_getsockopt's copyout (uipc_syscalls.c:1349) returns those bytes to userspace. Confirmed on the unpatched #0 kernel: a 128-byte getsockopt(IPPROTO_IP,IP_FW_X=49) with ip_fw_x_header.opcode=IP_FW_SET_GET=95 leaked 124 bytes of heap containing 12 candidate 0xffff... kernel pointers (e.g. 0xffffff80_4f0a2680, 0xffffff82_82600930) plus adjacent-slab strings (csh, /usr/libexec/sftp-server, /usr/libexec/getty, ttyv0/ttyv3). Reachability traced: IP_FW_X is handled only by rip_ctloutput (raw_ip.c:334) which is the pr_ctloutput for SOCK_RAW only (in_proto.c), and SOCK_RAW requires caps_priv_check(SYSCAP_NONET_RAW) (raw_ip.c:473) = root; maxx (uid 1001) gets EPERM on socket(SOCK_RAW) and ENOPROTOOPT(42) on IP_FW_X over SOCK_DGRAM. So this is a root-reachable kernel heap info leak (KASLR-defeat / securelevel kernel-read bypass building block), NOT an unpriv->root escalation. ipfw3.ko must be loaded by root (acceptable operational precondition).