β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0431

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0431 Β· 8 files
FileTypeDescriptionSize
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
VERDICT.md verdict full dead-code analysis + impact + fix
↓ download 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.

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=0
  • env.txt β€” guest environment

Fix verification

not_testable

compile 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.