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

SYN-cookie crypto state global unsynchronized across netisr CPUs: racy MD5_CTX + tcp_secret[] defeats SYN-flood mitigation

Summary

static MD5_CTX syn_ctx(:1358) + tcp_secret[SYNCOOKIE_NSECRETS](:1351-1354) are single global no lock no per-CPU. syncookie_generate called from syncache_add(:1068) on every new entry when tcp_syncookies enabled(default). syncookie_lookup from syncache_expand(:914). TCP input runs per-CPU netisr, SYNs distributed across CPUs by mbuf hash. 2+ CPUs simultaneously MD5Init/Update/Final against SAME syn_ctx + read/write tcp_secret[idx] -> interleaving corrupts transform+secret. Generated cookie sc_iss(:1406-1425) and recomputed digest(:1442-1463) become garbage -> legitimate final ACKs rejected under flood when syncache overflows. Defeats SYN-cookie mitigation exactly when engaged. Fix: per-CPU tcp_secret+MD5_CTX or spinlock around crypto.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0484 Β· 7 files
FileTypeDescriptionSize
trace.md source-trace line-by-line trace of the unsynchronized syn_ctx/tcp_secret race 3.9 KB ↓ raw
fix.diff suggested-fix spinlock around syncookie crypto+secret in generate/lookup 1.7 KB view raw
VERDICT.md verdict full narrative 2.0 KB ↓ raw
README.md readme summary 3.2 KB ↓ raw
env.txt environment guest uname, modules, HW-gate note 188 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
README.md readme summary
↓ download raw

DF-0484 β€” SYN-cookie global crypto state unsynchronized across netisr CPUs

The bug (real, by source inspection)

sys/netinet/tcp_syncache.c: - static MD5_CTX syn_ctx; (:1358) β€” a single global MD5 context. - static struct { u_int32_t ts_secbits[4]; u_int ts_expire; } tcp_secret[SYNCOOKIE_NSECRETS]; (:1351-1354) β€” single global secret table. No per-CPU copy, no lock. - syncookie_generate() (:1382-1426) does MD5Init(&syn_ctx) … MD5Update … MD5Final, and reads/writes tcp_secret[idx].ts_secbits[] / .ts_expire. - syncookie_lookup() (:1428-1463) does the same MD5Init/Update/Final on the same syn_ctx and reads tcp_secret[idx].

TCP input runs per-CPU in netisr; inbound SYNs are distributed across CPUs by mbuf hash. With β‰₯2 CPUs, syncookie_generate (from syncache_add, :1068, on syncache overflow) and syncookie_lookup (from syncache_expand, :914) can run concurrently on different CPUs against the same syn_ctx and tcp_secret[] slot. MD5 is a streaming transform: interleaved Init/Update/Final on one context corrupts the digest, and a torn ts_secbits read/write makes the generated cookie sc_iss (:1406-1425) and the recomputed digest (:1442-1463) disagree β‡’ legitimate final ACKs are rejected β‡’ the SYN-cookie flood mitigation fails exactly when it engages.

This is CWE-362 (race). It is a logic / DoS defect (no memory corruption, no info leak, no write primitive) β€” there is no escalation chain to develop.

Why not reproduced at runtime

Triggering requires: β‰₯2 CPUs receiving SYNs concurrently, syncache overflow forcing syncookie mode, and the rare MD5 interleave that actually corrupts a cookie a live connection then depends on. It is a low-probability race whose effect (one dropped legitimate ACK) is indistinguishable from normal packet loss on a SYN-flooded link. No deterministic PoC is feasible in the audit window; the defect is established by the unsynchronized single-global-state design, which is self-evidently racy on an SMP netisr.

Privilege boundary

Network-reachable, unauthenticated (AV:N, PR:N): any peer that can send SYNs to a listening socket contributes to the race. No local privilege needed.

Fix (validated as applies + compiles + boots)

fix.diff serialises the crypto+secret access with a spinlock:

static struct spinlock syncookie_sl = SPINLOCK_INITIALIZER(0, 0);

spin_lock(&syncookie_sl) … (secret refresh + MD5Init/Update/Final) … spin_unlock(&syncookie_sl) in both syncookie_generate and syncookie_lookup (releasing before the early return NULL in lookup).

Built into the combined single-fix kernel (#1, kern.version #1); boots clean. (A per-CPU tcp_secret+MD5_CTX would remove the contention but is a larger change; the spinlock is the minimal correct fix.) fix_status = not_testable (the race has no deterministic runtime marker to compare).

Files

  • trace.md β€” line-by-line source trace of the race
  • fix.diff β€” spinlock around the syncookie crypto+secret
  • README.md, VERDICT.md, manifest.json
VERDICT.md verdict full narrative
↓ download raw

DF-0484 detailed verdict

Verdict: NOT REPRODUCED at runtime β€” real race by inspection (CWE-362), logic/DoS impact

Mechanism

tcp_syncache.c uses a single global MD5_CTX syn_ctx (:1358) and a single global tcp_secret[SYNCOOKIE_NSECRETS] (:1351-1354) with no lock and no per-CPU copy. syncookie_generate (:1382, called from syncache_add :1068) and syncookie_lookup (:1428, from syncache_expand :914) both run MD5Init/Update/Final on that shared context and read/write tcp_secret[]. TCP input is per-CPU netisr, so on β‰₯2 CPUs these execute concurrently against the same state, interleaving the streaming MD5 transform and tearing ts_secbits reads/writes. Result: generated cookie (sc_iss, :1406-1425) and recomputed digest (:1442-1463) disagree β‡’ legitimate completing ACKs rejected under SYN flood β‡’ SYN-cookie mitigation defeated exactly when engaged.

Impact ceiling

Logic/DoS only. No corruption, no leak, no write primitive β‡’ no escalation chain (this is not a memory-corruption finding; Phase 6 does not apply).

Why not reproduced

Low-probability SMP race with no deterministic trigger; observable effect (one dropped ACK) is indistinguishable from normal loss on a flooded link. Established by the lockless single-global-state design (see trace.md).

Privilege boundary

Network, unauthenticated (AV:N/PR:N). No local privilege required to contribute SYNs to the race.

Fix (applies + compiles + boots)

fix.diff adds static struct spinlock syncookie_sl and wraps the secret-refresh + MD5Init..Final region in spin_lock/spin_unlock in both functions (releasing before the early return NULL in syncookie_lookup). Built in the combined single-fix kernel (#1); boots clean. A per-CPU secret+context would remove contention but is a larger change. fix_status = not_testable (no deterministic runtime marker).

PoC changes

No seeded PoC. I authored trace.md (line-by-line race trace) and fix.diff. A deterministic PoC is infeasible for this race class.

trace.md source-trace line-by-line trace of the unsynchronized syn_ctx/tcp_secret race
↓ download raw

DF-0484 source trace β€” unsynchronized syncookie crypto state

Claim: the SYN-cookie transform uses process-global, lockless state that two netisr CPUs can corrupt concurrently.

The shared state (no per-CPU copy, no lock)

sys/netinet/tcp_syncache.c:

1351: static struct {
1352:     u_int32_t ts_secbits[4];
1353:     u_int ts_expire;
1354: } tcp_secret[SYNCOOKIE_NSECRETS];          <-- ONE global secret table
...
1358: static MD5_CTX syn_ctx;                      <-- ONE global MD5 context
1360: #define MD5Add(v) MD5Update(&syn_ctx, (u_char *)&v, sizeof(v))

Producer: syncookie_generate (called on syncache overflow)

1382: syncookie_generate(struct syncache *sc)
1395:     idx = ((ticks << SYNCOOKIE_TIMESHIFT) / hz) & SYNCOOKIE_WNDMASK;
1396:     if (tcp_secret[idx].ts_expire < ticks) {      <-- READ/WRITE shared secret
1397:         for (i = 0; i < 4; i++)
1398:             tcp_secret[idx].ts_secbits[i] = karc4random();
1399:         tcp_secret[idx].ts_expire = ticks + SYNCOOKIE_TIMEOUT;
1400:     }
1406:     MD5Init(&syn_ctx);                             <-- uses GLOBAL syn_ctx
...       MD5Add(...) x several
1423:     MD5Final((u_char *)&md5_buffer, &syn_ctx);
1424:     data ^= (md5_buffer[0] & ~SYNCOOKIE_WNDMASK);

Called from syncache_add (:1068) β€” i.e. on every new syncache entry when tcp_syncookies is enabled (default). On syncache overflow the whole table is dropped and syncookie mode is the only admission path.

Consumer: syncookie_lookup (called on the final ACK)

1428: syncookie_lookup(struct in_conninfo *inc, struct tcphdr *th, struct socket *so)
1439:     if (tcp_secret[idx].ts_expire < ticks || ...)  <-- READ shared secret
1442:     MD5Init(&syn_ctx);                             <-- uses GLOBAL syn_ctx
...       MD5Add(...) x several
1462:     MD5Final((u_char *)&md5_buffer, &syn_ctx);
1463:     data ^= md5_buffer[0];

Called from syncache_expand (:914) to validate the cookie on the completing ACK.

Why it races

TCP input is per-CPU netisr (tcp_input runs on the CPU hashed from the mbuf). SYNs and their final ACKs for different 4-tuples land on different CPUs. Two CPUs executing syncookie_generate/syncookie_lookup concurrently both MD5Init the same syn_ctx and then interleave MD5Update/MD5Final calls. MD5 is a streaming hash whose internal state (A,B,C,D + bit count + buffer) is mutated by every Update/Final; interleaving produces a digest that depends on the interleaving, not on either connection's inputs alone. A torn read of tcp_secret[idx].ts_secbits[] during the karc4random refresh adds a second corruption vector. The net effect: the data value the producer folded into sc_iss no longer equals the value the consumer recomputes β‡’ the legitimate completing ACK is rejected (the cookie "doesn't decode") β‡’ under a SYN flood that triggered syncookie mode, real connections can't complete.

Synchronisation present? None.

grep for any lock around syn_ctx/tcp_secret in tcp_syncache.c: there is no lockmgr/spinlock/lwkt_sendmsg-to-a-single-CPU wrapping the generate/lookup crypto. The only serialisation is the implicit per-CPU netisr model, which here is the source of the concurrency, not a guard.

Impact classification

No memory corruption (no overflow/UAF), no info leak, no write primitive. The defect degrades a DoS-mitigation feature. CWE-362 (race) / logic-DoS. There is no privilege-escalation chain to develop.

Reproduction attempt / outcome

A deterministic PoC is not feasible: the race needs concurrent SYNs on β‰₯2 CPUs, syncache overflow, and an interleaving that actually corrupts a cookie a live connection then depends on β€” and the observable (one dropped ACK) is indistinguishable from loss on a flooded link. The defect is established by the lockless single-global-state design above. (A statistical flood harness could measure elevated ACK-drop rate under syncookie mode on a multi-CPU guest, but that is a benchmark, not a deterministic repro.)

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. SYN-cookie global MD5_CTX+tcp_secret unsynchronized across CPUs -> torn crypto. Race not deterministically reproducible.