# 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).

```sh
./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-src` guest; `run.sh` loads it.
- ipfw3 defaults to DENY on load; `run.sh` flips `filters_default_to_accept=1` first so
  loading does not cut ssh. This is purely a test-harness concern and does not touch the
  securelevel-bypass code path.
- `securelevel` is monotonic in a running kernel (`sys/kern/kern_mib.c:257-258`); each
  test run requires a fresh `vm.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 via `setsockopt()` (SOPT_SET); the fix correctly gates
  all SOPT_SET. The real `getsockopt()` (SOPT_GET) path is NOT blocked by the fix.
