Dead expire-scaling code: imported state expiry is always raw attacker value, enabling infinite-lifetime states
Summary
pfsync_state_import(:402-408): expire computed with rule timeout scaling at :402-406 then UNCONDITIONALLY OVERWRITTEN at :408 by st->expire=ntohl(sp->expire)+time_second. Timeout scaling dead code. Attacker sends sp->expire=0xFFFFFFFF -> state lives ~136 years never expires. Persistent authorization + state-table exhaustion. max_states==0(unlimited) checked at :352. Legitimate peers also lose correct timeout.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0431 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | bound imported expire to rule timeout; remove dead code + unconditional overwrite | 1.3 KB | view raw |
| fix_build.log | build-log | single-fix kernel build rc=0 | 228 B | view raw |
| build.sh | build-script | build instruction (fix applies + compiles) | 209 B | view raw |
| run.sh | run-script | no standalone trigger (logic bug, pf/pfsync-gated) | 157 B | view raw |
| VERDICT.md | verdict | full dead-code analysis + impact + fix | 3.6 KB | β raw |
| env.txt | environment | uname | 150 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-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:
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:
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=0env.txtβ guest environment
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. pfsync dead expire-scaling code -> immortal imported state via sp->expire=0xffffffff. Compile validated.
No comments yet.