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)
PoC verification
Evidence pack
findings/poc/DF-0739 · 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:213 —
bcopy(&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.komust be loaded (kldload ipfw3, root).IP_FW_Xis handled only byrip_ctloutput(sys/netinet/raw_ip.c), reachable solely viaSOCK_RAW, which needsSYSCAP_NONET_RAW(root).- An unprivileged user (maxx) gets
EPERMonsocket(SOCK_RAW,…)andENOPROTOOPTonIP_FW_XoverSOCK_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).
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)
ipfw3.komust be loaded (kldload ipfw3— root-only). This is the acceptable operational precondition (a firewall admin loads ipfw3).- The
IP_FW_X(=49,sys/netinet/in.h:389) socket option is handled only byrip_ctloutput(sys/netinet/raw_ip.c:334GET /:385SET), which is thepr_ctloutputfor everySOCK_RAWinet protocol (sys/netinet/in_proto.c— allrip_ctloutputentries areSOCK_RAW). - 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) getsEPERMonsocket(AF_INET, SOCK_RAW, …), and aSOCK_DGRAMsocket getsENOPROTOOPT(errno 42) onIP_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.kosha256ca6ccfb9…, unpatched#0kernel 6.5-DEVELOPMENT):./df0739 128returns 128 bytes — 124-byte heap over-read with 12 kernel pointers + adjacent-slab strings../df0739 1024returns 1024 bytes — 1020-byte over-read. (run.log, leak_sample.txt) - Applied
fix.diffto/usr/src/sys/net/ipfw3/ip_fw3_set.c(patch -p1→Hunk #1 succeeded at 210). - Built the module:
cd /usr/src/sys/net/ipfw3 && make obj && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC→rc=0(fix_build.log). - Installed:
cp /usr/obj/usr/src/sys/net/ipfw3/ipfw3.ko /boot/kernel/ipfw3.ko(sha256241f263a…).kldload ipfw3loads the fixed module. - Patched re-run:
./df0739 128and./df0739 1024both 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 setsnet.filters_default_to_accept=1first 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_RAWrequired). 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
fixedVALIDATED. 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).
Confirmed kernel references
- sys/net/ipfw3/ip_fw3_set.c:213
- sys/net/ipfw3/ip_fw3_set.c:206
- sys/net/ipfw3/ip_fw3.h:482
- sys/net/ipfw3/ip_fw3.h:489
- sys/net/ipfw3/ip_fw3.h:366
- sys/net/ipfw3/ip_fw3.h:423
- sys/net/ipfw3/ip_fw3.c:1038
- sys/net/ipfw3/ip_fw3.c:1043
- sys/net/ipfw3/ip_fw3.c:1070
- sys/net/ipfw3/ip_fw3_glue.c:51
- sys/netinet/raw_ip.c:334
- sys/netinet/raw_ip.c:473
- sys/kern/uipc_syscalls.c:1319
- sys/kern/uipc_syscalls.c:1349
- sys/sys/socket.h:356
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).
No comments yet.