# DF-0431 — Dead expire-scaling code in pfsync_state_import: immortal imported states

## Verdict: BUG CODE-CONFIRMED REAL (logic bug); root/HA-config-gated reachability; fix compile-validated

## Summary
`pfsync_state_import()` (sys/net/pf/if_pfsync.c:316) computes an imported PF
state's `st->expire` such that a block of "scaling" code is dead, and the final
value is the **raw, attacker-controlled** `sp->expire`:

```c
st->creation = time_second - ntohl(sp->creation);
st->expire = time_second;                                   /* :402 */
if (sp->expire) {
    /* XXX No adaptive scaling. */
    st->expire -= r->timeout[sp->timeout] - ntohl(sp->expire);   /* :405 -- DEAD */
}

st->expire = ntohl(sp->expire) + time_second;               /* :408 -- UNCONDITIONAL overwrite */
```

The export side (pfsync :273-277) writes `sp->expire` as **remaining seconds**
(`pf_state_expires(st) - time_second`). On import, :408 converts it straight
back to absolute with no bound. The `:402-406` block that would scale the
lifetime by the rule's configured timeout (`r->timeout[sp->timeout]`) is **dead
code** -- its result is always overwritten by :408.

## Impact
A malicious or compromised pfsync peer sending a state with
`sp->expire = htonl(0xffffffff)` creates an imported state whose `st->expire`
is `time_second + 0xffffffff` ≈ **~136 years in the future** -- the state never
expires. Consequences:
- **Persistent authorization**: a firewall state that should time out (e.g. a
  short-lived "allow" rule) is pinned open for a century.
- **State-table exhaustion**: `max_states` is the only cap (:352 checks
  `max_states==0` == unlimited); an attacker filling the table with immortal
  states denies new state creation (DoS).
- Legitimate peers also lose correct timeout enforcement on failover.

This matches CVSS `AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L` (adjacent = the pfsync
sync link).

## Reachability
`pfsync_state_import` is reached:
- via the **pfsync network-receive path** (if_pfsync.c:632, :670) when a pfsync
  message arrives on a configured pfsync(4) interface -- i.e. an attacker who
  can inject onto the pfsync sync link (a separate/adjacent network), or a
  compromised HA peer;
- via the **ioctl path** (`PFSYNC_SI_IOCTL`, :335/:355/:413) used by the local
  admin / pfsync tooling.

Both require pf to be enabled and pfsync configured (an HA-firewall deployment).
Not reachable on this audit guest without pf/pfsync setup; the logic bug is
unambiguous from the code trace and the exploit (immortal state) is mechanically
certain.

## Exploit chain
Not memory corruption -- this is a **logic/auth-lifetime bug**. No `uid=0`
chain. Realistic impact ceiling: pin open a PF pass state for ~136 years
(persistent authorization) and/or exhaust the state table. CVE-class: CWE-561
(dead code) leading to missing lifetime enforcement.

## Recommended fix (compile-validated)
`fix.diff` removes both the dead block and the unconditional overwrite, and
bounds the imported state's lifetime to the rule's configured timeout for the
state's type, taking the smaller of the peer's claimed remaining seconds and
the rule timeout:
```c
u_int32_t peer_remain = ntohl(sp->expire);
u_int32_t rule_to = r->timeout[sp->timeout];
st->expire = time_second + (peer_remain < rule_to ? peer_remain : rule_to);
```
Validated to **apply cleanly + compile** (single-fix kernel build, rc=0). Not
live-tested (no pf/pfsync config on this guest; the fix is trace-correct).

## Files
- `fix.diff` — bound imported expire to rule timeout; remove dead code (if_pfsync.c:399-408)
- `fix_build.log` — single-fix kernel build rc=0
- `env.txt` — guest environment
