ipfw3 sockopt path bypasses securelevel>=3 firewall-mutability gate β missing check present in all sibling glue layers
Summary
ip_fw3_glue.c:50-62 ip_fw3_sockopt dispatches every IP_FW_X setsockopt to ip_fw_ctl_x_ptr WITHOUT securelevel>=3 check. ip_fw2_glue.c:65-69 HAS the check: if(sopt_name==IP_FW_ADD||(SOPT_SET&&!=IP_FW_RESETLOG)) if(securelevel>=3) return EPERM. ip_dummynet_glue.c:161 ip_dummynet3_glue.c:161 ip6_fw.c:1119 pf_ioctl.c:992 ALL have the check. ipfw3 omits it. Result: at securelevel 3 root can still add/del/flush/set/nat/dummynet/table/sync reconfigure ipfw3 rules β defeats securelevel 3 firewall immutability guarantee. Post-compromise root attacker (exact threat securelevel 3 contains) can flush rulebase insert allow-all rules disable sets alter NAT to re-open attack surface. Defense-in-depth boundary bypass not new priv-esc (root required) hence Medium not High. Fix: if(sopt_dir==SOPT_SET&&sopt_name==IP_FW_X) if(securelevel>=3) return EPERM.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0761 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0761.c | trigger-source | minimal C trigger: raw-socket setsockopt(IP_FW_X, FLUSH) probe with BYPASS/GATED/INCONCLUSIVE marker | 5.2 KB | view raw |
| build.sh | build-script | cc -O0 -g -o df0761 df0761.c | 392 B | view raw |
| run.sh | run-script | orchestrates: flip filters_default_to_accept=1 -> kldload ipfw3 -> securelevel=3 -> run probe | 2.1 KB | view raw |
| fix.diff | suggested-fix | one-hunk git-apply-able fix: add securelevel>=3 gate to ip_fw3_sockopt (matches dummynet3 sibling form) | 636 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, evidence, fix, validation | 8.0 KB | β raw |
| README.md | readme | human-facing reproduce instructions + expected output | 3.3 KB | β raw |
| build.log | build-log | baseline build (cc, exit 0) | 97 B | view raw |
| run.log | run-log | baseline run on #0 kernel: BYPASS CONFIRMED (setsockopt rc=0 at securelevel=3) | 1.2 KB | view raw |
| fix_build.log | build-log | single-fix kernel nativekernel build (NK_DONE rc=0), full output | 5.6 MB | β download |
| fix_run.log | run-log | patched #1 kernel run: GATED/FIXED (setsockopt rc=-1 EPERM at securelevel=3), run 1 | 1.2 KB | view raw |
| fix_run.2.log | run-log | patched #1 kernel run 2: GATED/FIXED (determinism) | 1.2 KB | view raw |
| env.txt | environment | guest uname, cc version, default sysctls | 948 B | view raw |
| boot_tail.txt | boot-log | serial console tail (no panic β clean policy test) | 116 B | view 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-0761 β ipfw3 sockopt path bypasses securelevel>=3 firewall-mutability gate
Severity: Medium Β· Class: privileged policy/logic bypass (NOT memory corruption)
Verified: REPRODUCED on unpatched #0; FIX VALIDATED on single-fix #1.
Claim
sys/net/ipfw3/ip_fw3_glue.c:50-62 β ip_fw3_sockopt() dispatches every IP_FW_X
setsockopt to ip_fw_ctl_x_ptr() WITHOUT the securelevel >= 3 firewall-mutability
gate that all sibling glue layers enforce (ip_fw2_glue.c:65-69,
ip_dummynet3_glue.c:159-163, pf_ioctl.c:992). Result: at securelevel 3 a root
process can still flush/add/del/set/nat/dummynet/table/sync-reconfigure the ipfw3
ruleset, defeating the securelevel-3 firewall-immutability guarantee.
How to reproduce (full procedure)
The test runs as root (the threat model is "root at securelevel>=3"; the bypass is
privileged-only). Run on a freshly-reset with-src guest (securelevel=-1, ipfw3 not
loaded β securelevel is irreversible in a running kernel, so each run needs a fresh guest).
./build.sh && ./run.sh
run.sh does:
1. sysctl net.filters_default_to_accept=1 β so loading ipfw3 (which defaults to DENY,
ip_fw3.c:1468) does not black-hole our ssh session. This does NOT affect the test.
2. kldload ipfw3.
3. sysctl kern.securelevel=3 β raise the firewall-immutability gate.
4. ./df0761 β issues setsockopt(IPPROTO_IP, IP_FW_X, {opcode=IP_FW_FLUSH}) and prints
the result marker.
Expected output
Bug present (unpatched #0 kernel):
[bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=0 errno=0 (OK) === RESULT: BYPASS CONFIRMED ===
Bug fixed (patched #1 kernel, with fix.diff applied):
[bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=-1 errno=1 (Operation not permitted) === RESULT: GATED (FIXED) ===
Files
| File | Purpose |
|---|---|
df0761.c |
minimal C trigger: raw-socket setsockopt(IP_FW_X, FLUSH) probe |
build.sh |
cc -O0 -g -o df0761 df0761.c |
run.sh |
orchestrates: load ipfw3 β raise securelevel β run probe |
VERDICT.md |
full narrative: mechanism, evidence, fix, validation |
fix.diff |
git-apply-able one-hunk fix adding the securelevel>=3 gate |
build.log |
baseline build output |
run.log |
baseline run (BYPASS CONFIRMED on #0) |
fix_build.log |
single-fix kernel build (NK_DONE rc=0) |
fix_run.log |
patched-kernel run (GATED/FIXED on #1, run 1) |
fix_run.2.log |
patched-kernel run (GATED/FIXED on #1, run 2 β determinism) |
env.txt |
guest environment (uname, cc, sysctls) |
manifest.json |
machine-readable artifact catalog |
Notes / caveats
- ipfw3 is not loaded by default on the
with-srcguest;run.shloads it. - ipfw3 defaults to DENY on load;
run.shflipsfilters_default_to_accept=1first so loading does not cut ssh. This is purely a test-harness concern and does not touch the securelevel-bypass code path. securelevelis monotonic in a running kernel (sys/kern/kern_mib.c:257-258); each test run requires a freshvm.sh reset with-src.- The sanity-GET line in the PoC output (
opcode=54) also returns EPERM on the patched kernel because the PoC issues it viasetsockopt()(SOPT_SET); the fix correctly gates all SOPT_SET. The realgetsockopt()(SOPT_GET) path is NOT blocked by the fix.
DF-0761 β VERDICT
Status: REPRODUCED (policy/logic bypass) + FIX VALIDATED
Impact: policy-bypass β securelevel>=3 firewall-immutability gate defeated (privileged-only)
Confidence: certain
Severity: Medium (matches finding)
Verdict (one line)
The ipfw3 IP_FW_X setsockopt dispatch path (sys/net/ipfw3/ip_fw3_glue.c:50-62) omits the
securelevel >= 3 firewall-mutability gate that ALL sibling glue layers enforce, so a root
process can keep flushing/altering the ipfw3 ruleset after the system has gone immutable β
confirmed on the unpatched #0 kernel (setsockopt(IP_FW_X, FLUSH) β rc=0 at
kern.securelevel=3) and closed on the single-fix #1 kernel (β EPERM).
Mechanism (trigger β effect, every hop cited)
-
Trigger (userspace, root). A root process opens a raw socket (
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) and issuessetsockopt(IPPROTO_IP, IP_FW_X, &xhdr, sizeof(xhdr))wherexhdr.opcodeselects the operation (IP_FW_FLUSH=52,IP_FW_ADD=50,IP_FW_DEL=51, etc., persys/net/ipfw3/ip_fw3.h:372-377).IP_FW_Xitself is49(sys/netinet/in.h:389). -
Dispatch.
rip_ctloutput(sys/netinet/raw_ip.c:385-387) routesIP_FW_XSOPT_SET calls toip_fw3_sockopt()(sys/netipfw3/ip_fw3_glue.c:50-62). -
The bug β missing gate.
ip_fw3_sockopt()checks onlyIPFW3_LOADEDand unconditionally callsip_fw_ctl_x_ptr(sopt)(line 58). There is nosecurelevel >= 3check anywhere in the function. Contrast with every sibling: -sys/net/ipfw/ip_fw2_glue.c:65-69βip_fw_sockoptHAS the gate:if (sopt->sopt_name == IP_FW_ADD || (sopt->sopt_dir == SOPT_SET && sopt->sopt_name != IP_FW_RESETLOG)) { if (securelevel >= 3) return EPERM; }-sys/net/dummynet3/ip_dummynet3_glue.c:159-163βip_dn_sockoptHAS the gate:if (sopt->sopt_dir == SOPT_SET) { if (securelevel >= 3) return EPERM; }-sys/net/pf/pf_ioctl.c:992β pf ioctl path HASif (securelevel > 1)gate. -
Effect. At
kern.securelevel >= 3(the "really-really secure" / network-immutable level, settable viasysctl kern.securelevel=3and irreversible in a running kernel persys/kern/kern_mib.c:257-258), a root process can still FLUSH / ADD / DEL / SET-TOGGLE / NAT / dummynet / table / sync-reconfigure the ipfw3 ruleset β defeating the exact guarantee securelevel 3 exists to provide (containment of a compromised root).
Threat model. This is a privileged-only policy/logic bypass β NOT memory corruption.
The setsockopt needs SYSCAP_NONET_RAW (root). There is no unprivileged escalation
chain; the impact is a defeated securelevel boundary for a post-compromise root attacker,
which is precisely the scenario securelevel 3 is designed to contain (a root attacker who
must NOT be able to alter the firewall ruleset, flush it to re-open attack surface, or
re-NAT to exfiltrate). Hence Medium, not High.
Exploit chain
none (non-corruption class). This is a privileged policy-bypass. The "primitive" is the
ungated setsockopt itself; the impact ceiling is "root at securelevel>=3 can mutate the
ipfw3 firewall arbitrarily", characterized above. No heap grooming, victim object, or
uid=0 conversion applies.
Reproduction evidence (decisive lines)
Baseline β unpatched #0 kernel (6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026):
=== [3] raise kern.securelevel to 3 (network-immutable gate) === kern.securelevel: -1 -> 3 kern.securelevel: 3 === [4] attempt IP_FW_X FLUSH at securelevel=3 (the bypass probe) === [env] kern.securelevel = 3 [bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=0 errno=0 (OK) === RESULT: BYPASS CONFIRMED === setsockopt(IP_FW_X, FLUSH) succeeded at securelevel=3 -> ipfw3 securelevel>=3 gate is MISSING (DF-0761 reproduced)
Patched β single-fix #1 kernel (6.5-DEVELOPMENT #1, Thu Jul 9 12:10:06 UTC 2026):
=== [3] raise kern.securelevel to 3 (network-immutable gate) === kern.securelevel: -1 -> 3 kern.securelevel: 3 === [4] attempt IP_FW_X FLUSH at securelevel=3 (the bypass probe) === [env] kern.securelevel = 3 [bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=-1 errno=1 (Operation not permitted) === RESULT: GATED (FIXED) === setsockopt(IP_FW_X, FLUSH) returned EPERM at securelevel=3 -> securelevel>=3 gate is present (bug closed)
(Second patched-kernel run, fix_run.2.log, is byte-identical at the decisive line β the
fix is deterministic.)
Full untrimmed logs: run.log (baseline), fix_run.log + fix_run.2.log (patched),
fix_build.log (single-fix kernel build, NK_DONE rc=0).
PoC changes
Authored from scratch β no prior PoC existed in findings/poc/DF-0761/ (the finding was
filed but the initial scaffolding had not been seeded). Files written:
- df0761.c β minimal C trigger: opens a raw socket, issues setsockopt(IP_FW_X, {opcode=FLUSH}),
reports rc/errno, and prints a BYPASS CONFIRMED / GATED (FIXED) / INCONCLUSIVE marker.
Constants (IP_FW_X=49, opcodes from ip_fw3.h:372-377) are defined in-file so it builds on
a stock master-DEV install with no kernel headers in /usr/include.
- build.sh β cc -O0 -g -o df0761 df0761.c.
- run.sh β orchestrates the test: flips net.filters_default_to_accept=1 (so loading ipfw3
does not black-hole ssh β ipfw3 defaults to DENY on load, ip_fw3.c:1468), kldload ipfw3,
raises kern.securelevel to 3, then runs ./df0761.
Two PoC bugs fixed during iteration:
1. First run hung ssh β ipfw3's default-deny on kldload cut the control channel before the
test could run. Fixed by setting net.filters_default_to_accept=1 before kldload (this
does not affect the securelevel-bypass test, which exercises the dispatch path, not the
default policy).
2. PoC's internal sysctl read of kern.securelevel used the wrong KERN_SECURELVL
constant (6 β 9, per sys/sys/sysctl.h:505); fixed so the PoC's printed securelevel
matches the kernel's own sysctl kern.securelevel.
Recommended fix
fix.diff adds the missing gate to ip_fw3_sockopt(), matching the dummynet3 sibling
form (sys/net/dummynet3/ip_dummynet3_glue.c:159-163):
int
ip_fw3_sockopt(struct sockopt *sopt)
{
int error;
ASSERT_NETISR0;
/* Disallow firewall mutations in really-really secure mode, matching
* the gate present in every sibling glue layer ... SOPT_GET remains
* allowed, consistent with ip_fw2_glue.c and ip_dummynet3_glue.c. */
if (sopt->sopt_dir == SOPT_SET && securelevel >= 3)
return EPERM;
if (IPFW3_LOADED)
error = ip_fw_ctl_x_ptr(sopt);
else
error = ENOPROTOOPT;
return (error);
}
This matches the finding's proposed fix (if(sopt_dir==SOPT_SET&&sopt_name==IP_FW_X)
if(securelevel>=3) return EPERM) β for ipfw3, sopt_name is always IP_FW_X at this
point (the sub-opcode is inside the payload), so checking sopt_dir==SOPT_SET alone is
equivalent and simpler. It is slightly stricter than the ipfw2 sibling (which carves out
IP_FW_RESETLOG), but ipfw3 has no equivalent carve-out requirement and the dummynet3
sibling uses exactly this all-SET-blocked form. A maintainer wanting the RESETLOG
carve-out can parse the ip_fw_x_header.opcode later; the minimal correct fix is this one.
Fix validation (Phase 8)
- Baseline
#0: PoC βBYPASS CONFIRMED(setsockopt rc=0 at securelevel=3). β bug present. - Applied
fix.diffto in-guest/usr/srcviapatch -p1(hunk succeeded at line 54). - Built single-fix kernel
make -j6 nativekernel KERNCONF=X86_64_GENERICβNK_DONE rc=0(warm obj, ~13 min full rebuild because mkdep regenerated;.c-only change). - Installed
kernel.strippedβ/boot/kernel/kernel,kernel.debugβ/boot/kernel/kernel.debug, clean reboot βkern.versionnow#1: Thu Jul 9 12:10:06 UTC 2026(was#0: Thu Jul 2 ...). - Patched
#1: PoC βGATED (FIXED)(setsockopt rc=-1 errno=EPERM at securelevel=3). Deterministic across 2 runs. fix_status: fixed β bad behavior gone on patched, present on baseline. Clean before/after.- Reset to
with-srcbaseline at end.
Fix verification
fixedVALIDATED the fix: ./run.sh on unpatched 6.5-DEVELOPMENT #0 baseline returned '=== RESULT: BYPASS CONFIRMED ===' ([bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=0 at kern.securelevel=3) -- bug present. Applied fix.diff (patch -p1, hunk succeeded at line 54), built single-fix kernel (make -j6 nativekernel, NK_DONE rc=0), installed kernel.stripped->/boot/kernel/kernel, rebooted to #1 (Thu Jul 9 12:10:06 UTC 2026). Re-running ./run.sh on #1 returned '=== RESULT: GATED (FIXED) ===' ([bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=-1 errno=1/EPERM at kern.securelevel=3) -- bug closed. Deterministic across 2 runs (fix_run.log, fix_run.2.log). Guest reset to with-src baseline at end.
BEFORE (#0 baseline, run.log): kern.securelevel: -1 -> 3 | [bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=0 errno=0 (OK) | === RESULT: BYPASS CONFIRMED ===. AFTER (#1 single-fix, fix_run.log): kern.securelevel: -1 -> 3 | [bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=-1 errno=1 (Operation not permitted) | === RESULT: GATED (FIXED) ===. Build: === NK_DONE rc=0 === (Thu Jul 9 12:13:57 UTC 2026).
Confirmed kernel references
- sys/net/ipfw3/ip_fw3_glue.c:50
- sys/net/ipfw3/ip_fw3_glue.c:58
- sys/net/ipfw/ip_fw2_glue.c:65
- sys/net/ipfw/ip_fw2_glue.c:67
- sys/net/dummynet3/ip_dummynet3_glue.c:160
- sys/net/dummynet3/ip_dummynet3_glue.c:161
- sys/netinet/raw_ip.c:385
- sys/netinet/raw_ip.c:386
- sys/netinet/in.h:389
- sys/net/ipfw3/ip_fw3.h:372
- sys/kern/kern_mib.c:257
- sys/sys/sysctl.h:505
Detail
Exploit chain
none (non-corruption class). This is a privileged-only policy/logic bypass -- the setsockopt needs SYSCAP_NONET_RAW/root, so there is no unprivileged escalation chain and no heap/victim/uid=0 conversion applies. The impact ceiling is 'root at securelevel>=3 can FLUSH/ADD/DEL/SET-TOGGLE/NAT/dummynet/table/sync-reconfigure the ipfw3 ruleset' -- defeating the securelevel-3 firewall-immutability guarantee that exists precisely to contain a post-compromise root attacker (re-open attack surface, exfil via NAT, etc.).
Evidence (decisive lines)
BASELINE #0 (run.log): kern.securelevel: -1 -> 3 ; [bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=0 errno=0 (OK) ; === RESULT: BYPASS CONFIRMED === setsockopt(IP_FW_X, FLUSH) succeeded at securelevel=3 -> ipfw3 securelevel>=3 gate is MISSING. PATCHED #1 (fix_run.log): kern.securelevel: -1 -> 3 ; [bypass-FLUSH] setsockopt(IP_FW_X, opcode=52) -> rc=-1 errno=1 (Operation not permitted) ; === RESULT: GATED (FIXED) ===. Deterministic across fix_run.2.log. Single-fix kernel build: === NK_DONE rc=0 === (Thu Jul 9 12:13:57 UTC 2026).
PoC changes
Authored full evidence pack from scratch (no prior PoC existed in findings/poc/DF-0761/). df0761.c: minimal C trigger opening a raw socket and issuing setsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=IP_FW_FLUSH=52}); constants defined in-file (IP_FW_X=49, opcodes from ip_fw3.h:372-377) so it builds on stock master-DEV without kernel headers in /usr/include. build.sh: cc -O0 -g -o df0761 df0761.c. run.sh: orchestrates the test (flip net.filters_default_to_accept=1 so kldload ipfw3 does not black-hole ssh via its default-deny at ip_fw3.c:1468; kldload ipfw3; sysctl kern.securelevel=3; run probe). Two iteration fixes: (1) first run hung ssh because ipfw3 default-deny cut the control channel before the test ran -- fixed by setting filters_default_to_accept=1 pre-load; (2) PoC's internal sysctl securelevel read used wrong KERN_SECURELVL constant (6 -> 9 per sys/sys/sysctl.h:505) -- fixed.
Verified recommended fix
fix.diff adds the missing gate to ip_fw3_sockopt() in sys/net/ipfw3/ip_fw3_glue.c (one hunk, git-apply-able): after ASSERT_NETISR0, add 'if (sopt->sopt_dir == SOPT_SET && securelevel >= 3) return EPERM;'. Matches the dummynet3 sibling form (ip_dummynet3_glue.c:159-163) and matches the finding's proposed fix (for ipfw3, sopt_name is always IP_FW_X at this point, so checking sopt_dir==SOPT_SET alone is equivalent and simpler). SOPT_GET remains allowed, consistent with ipfw2/dummynet3. 'matches finding proposal'.
Verdict
REPRODUCED + FIX VALIDATED. sys/net/ipfw3/ip_fw3_glue.c:50-62 ip_fw3_sockopt() dispatches every IP_FW_X setsockopt straight to ip_fw_ctl_x_ptr() WITHOUT the securelevel>=3 firewall-mutability gate that ALL sibling glue layers enforce (ip_fw2_glue.c:65-69, ip_dummynet3_glue.c:159-163, pf_ioctl.c:992). Confirmed on unpatched #0 kernel: at kern.securelevel=3, setsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=IP_FW_FLUSH=52}) returned rc=0 (BYPASS CONFIRMED) -- root can keep mutating the ipfw3 ruleset after the system has gone immutable. Closed on single-fix #1 kernel: same call returns rc=-1 errno=EPERM (GATED/FIXED). Privileged-only policy/logic bypass (NOT memory corruption) -- defeats the securelevel-3 boundary whose explicit purpose is to contain a compromised root; hence Medium per finding.
No comments yet.