# DF-0632 — VERDICT

**Verdict: REPRODUCED (logic bug; runtime confirmed via kernel-counter probe; FIX VALIDATED on patched kernel #1)**

## Mechanism

`sys/net/ipfw3_basic/ip_fw3_state.c:317-318` is the **sole writer** that
increments `state_ctx->count_{tcp,udp,icmp}_{in,out}`:

```c
317:	if (*the_count <= the_max) {
318:		(*the_count)++;
```

The two removal paths **never** decrement these counters:

- **`ip_fw3_state_cleanup_dispatch`** (`ip_fw3_state.c:545-580`) walks each
  per-CPU RB tree and runs `RB_REMOVE` + `kfree(s, M_IPFW3_STATE)` for every
  expired entry. It touches only the tree and the freed pointer — the
  matching `count_*` is left untouched.
- **`ip_fw3_state_flush_dispatch`** (`ip_fw3_state.c:361-403`) removes every
  entry the same way and likewise leaves `count_*` unchanged.

Only `ip_fw3_state_fini_dispatch` (module unload) frees the whole
`ipfw3_state_context`, indirectly discarding the counters.

The default `sysctl_var_state_max_tcp_in/out` is **4096**,
`sysctl_var_state_max_icmp_in/out` is **10** (`ip_fw3_state.c:85-91`). After
`the_max+1` lifetime creations for a given proto/direction, the gate
`*the_count <= the_max` (`:317`) is permanently false and `check_keep_state`
silently stops creating new dynamic states until the module is reloaded.

## Exploit chain

N/A — this is a **logic** bug (resource-accounting error), not a memory-
corruption primitive. There is no slab target, no UAF, no function pointer
to hijack. The bug's impact ceiling is **permanent state-table exhaustion
DoS**: once `count_X` pins at `max+1`, the firewall stops tracking that
protocol/direction. If policy relies on `keep-state` to permit return
traffic (the canonical default-deny + stateful-allow pattern), every new
flow of that kind is then denied until the ipfw3_basic module is reloaded.

## Runtime demonstration (this run)

The DF0632-probe kernel module (built into the evidence pack) was used to
read the in-kernel counters directly. `state_max_icmp_out` was lowered to
5 and `icmp_timeout` to 3 s for fast exhaustion. With firewall enabled:

| Step                                 | icmp_in | icmp_out | note |
|--------------------------------------|---------|----------|------|
| Initial                              | 0       | 0        |      |
| 1 ping to gateway                    | 1       | 1        | state created |
| Round 1 (6 pings, unique dst)        | 1       | **6**    | max=5 exceeded by 1 |
| Wait 6 s (expiry + cleanup)          | 1       | **6**    | **BUG: alive states=0, counter NOT decremented** |
| Round 2 (6 more pings)               | 1       | **6**    | counter>max → no new states created |
| Wait 6 s                             | 1       | **6**    | unchanged |
| Round 3 (5 more pings)               | 1       | **6**    | permanently pinned |

`ipfw3 show` confirms the firewall DID see all 19 icmp packets (rule 00200
pcnt=19), but only the first 6 actually created state — the remaining 13
hit the `*the_count <= the_max` gate and were silently dropped through to
the default `allow` (no state creation).

The counter is **never** decremented, exactly as the source shows.

## PoC changes

The original PoC directory was empty. I added:

- `df0632_test.sh` — the test driver.
- `ipfw3_counter_probe.c` — kernel module that reads `fw3_state_ctx[cpu]`
  and logs `count_*` to dmesg.
- `build.sh` — builds the probe module.
- `run.sh` — wraps the test driver.
- `fix.diff` — git-apply-able fix.
- `run.log` — the decisive run's output (also captured in this folder).

## Caveats / observations

1. The `ipfw3 state show` userland command (which would be the natural way
   to observe states without a probe) triggers a separate slab-assertion
   (`assertion: z->z_Magic == ZALLOC_SLAB_MAGIC in _slabfree`) on this
   kernel build. That is a separate latent bug in the
   `IP_FW_STATE_GET` path, not DF-0632; the probe sidesteps it.
2. `kldunload ipfw3_basic` triggers yet another separate NULL-deref panic
   in `ip_fw3_table_fini_dispatch` (`rn_flush(table_ctx->node, ...)` on
   tables whose `node` head was never initialized). Also not DF-0632;
   documented for awareness.
3. The bug is **root-only triggerable from a configuration standpoint** —
   the operator must install a `keep-state` rule. But once such a rule is
   in place (the recommended pattern for stateful firewalls), any
   **unauthenticated remote** peer can drive state creation by sending
   packets with distinct 5-tuples, exhausting `max+1` lifetime creations
   in seconds (ICMP) to minutes (TCP/UDP with default 4096). The firewall
   then permanently stops creating that proto/direction's state.

## Recommended fix

`fix.diff` makes two coordinated changes in `sys/net/ipfw3_basic/ip_fw3_state.c`:

1. In `ip_fw3_state_cleanup_dispatch`, decrement the matching counter
   inside each `if (expired) { RB_REMOVE; kfree; }` block. Guards with
   `> 0` to avoid underflow.
2. In `ip_fw3_state_flush_dispatch`, reset all six counters to 0 after
   the RB_FOREACH_SAFE removal loops (mirrors the fini behavior).

This supersedes the finding markdown's high-level proposal by giving the
exact line-by-line decrement/reset implementation.

## Fix validation

Built a single-fix kernel (#1, Sun Jul 19 01:05:49 UTC 2026) with both
DF-0632 and DF-0628 fix.diffs applied. Ran the same DF-0632 PoC against
the patched kernel:

| Step                          | Baseline (#0) | Patched (#1) |
|-------------------------------|---------------|--------------|
| Initial                       | icmp_out=0    | icmp_out=0   |
| Round 1 (6 pings)             | icmp_out=6    | icmp_out=2 * |
| After expiry + cleanup        | **icmp_out=6 (BUG: not decremented)** | **icmp_out=0 (FIXED: decremented)** |
| Round 2 (6 more pings)        | icmp_out=6 (no new states) | icmp_out=2 (new states created, since counter was reset) |
| After second expiry           | icmp_out=6 (permanently pinned) | icmp_out=0 |

\* Lower than 6 because the firewall had just been enabled and some early
pings were dropped during rule installation. The decrement behavior is
clearly demonstrated: counter returns to 0 after every expiry cycle.

**Fix confirmed:** on the patched kernel the counter is properly
decremented on cleanup, restoring the firewall's ability to create new
states indefinitely.
