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

SipHash_SetKey retains verbatim 128-bit key on the stack without zeroization

Summary

SipHash_SetKey() copies the caller's 16 raw key bytes into stack local uint64_t k[2], XORs them into the context, and returns without wiping - even though the same file wipes the derived context state in SipHash_End. The verbatim key outlives the call on the dead frame. REPRODUCED: same-source userland object, noinline wrapper + call-free inline stack scan finds the verbatim 16-byte key at offset -112 below the caller frame ('DF-3564 CONFIRMED'); one-line explicit_bzero fix validated live (retention gone, KAT suite still 2111/2111). WireGuard computes SipHash with long-term secret keys on the network-reachable handshake path (wg_noise l_hash_key; wg_cookie MAC keys over attacker-supplied bytes) - 16 bytes of raw secret land on kernel stacks at attacker-driven packet rates. No in-tree primitive reads kernel stacks today - hardening ceiling: residue harvest given a second, unrelated leak primitive. Transform itself proven correct: 64 official vectors + independent from-paper reference for both 2-4 and wg's 1-3 + chunked equivalence = 2111/2111.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3564 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
sip_keyscan.c β€” 1.9 KB view raw
sip_kat.c β€” 8.0 KB view raw
build.sh β€” 191 B view raw
run.sh β€” 89 B view raw
shim/ β€” 4.0 KB ↓ download
build.log β€” 8.7 KB view raw
run.log β€” 2.2 KB view raw
fix.diff β€” 379 B view raw
fix_run.log β€” 1.2 KB view raw
fix_build.log β€” 61 B view raw
env.txt β€” 1.3 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.6 KB view raw

DF-3564 β€” SipHash_SetKey retains verbatim 128-bit key on the stack

Target: sys/crypto/siphash/siphash.c (pass 2 GLM 5.3 re-audit) Class: key-material hygiene / CWE-459 (Incomplete Cleanup) Severity: Low (hardening; no standalone leak path)

What

SipHash_SetKey() (sys/crypto/siphash/siphash.c:69-85) decodes the caller's 16-byte key into a stack local uint64_t k[2] (line 71), XORs it into the context state, and returns without zeroizing k. The same file's own hygiene standard β€” SipHash_End() at line 188 does bzero(ctx, sizeof(*ctx)) β€” makes the omission stand out: the context is wiped, but the raw key copy on the dead frame is not.

Reachability

In-tree consumers (all WireGuard, all network-reachable with attacker-driven call rates on the handshake path):

  • sys/net/wg/wg_noise.c:240 siphash24() β†’ SipHashX(ctx, 2, 4, key, ...) used at wg_noise.c:457 and :498 to index the handshake/remote hash tables with the long-term secret l_hash_key (random, per-locality).
  • sys/net/wg/wg_cookie.c:136 siphash13() β†’ SipHashX(ctx, 1, 3, key, ...) over received handshake message bytes (MAC1 path).

Every received UDP handshake packet to a wg interface causes k[2] to hold 16 bytes of a long-term secret on the handler thread's kernel stack. The bytes persist until that stack region is reused. No in-tree primitive reads kernel stacks back to userspace today β€” hence Low / hardening, not a leak finding β€” but it is exactly the kind of residue a second, unrelated info-leak bug would harvest.

Reproduce (same-source userland-object pattern)

# guest, in this directory (sources + shim included)
cc -O0 -g -I shim -c /usr/src/sys/crypto/siphash/siphash.c -o siphash.o
cc -O0 -g -I shim sip_keyscan.c siphash.o -o sip_keyscan
./sip_keyscan

Expected (baseline, unpatched sys/crypto/siphash/siphash.c):

RETAINED: verbatim 16-byte key found on the dead stack, offset -112 below caller frame
DF-3564 CONFIRMED: raw key bytes persist on the stack after SipHash_SetKey returned

The scan uses an inline byte-compare loop (no calls), so the comparison itself cannot clobber the dead frame being inspected β€” the first version of this PoC used memcmp(3) and self-clobbered the evidence.

Fix

fix.diff (one line): explicit_bzero(k, sizeof(k)); after the last use of k. Validated live: with the patched copy the scan reports NOT FOUND (exit 1) and the full SipHash KAT suite still passes 2111/2111 (see fix_run.log), so the fix is behavior-preserving.

Full-suite context

run.log in this pack is the decisive run of the whole four-file batch: sha1 1859/0, siphash 2111/0, des 8916/0, blake2s 6878/8 checks β€” all PASS β€” plus this retention demonstration. env.txt records guest uname, compiler (LibreSSL 3.6.1 cross-match) and byte-identical source hashes between the guest /usr/src and the audited tree.

VERDICT.md
↓ download raw

DF-3564 β€” VERDICT

Status: reproduced (retention condition demonstrated in a same-source userland build of the exact audited file; impact ceiling: hardening β€” no standalone disclosure path exists in-tree).

Narrative

Pass-2 audit of sys/crypto/siphash/siphash.c (235 LOC, KAME/FreeBSD-lineage SipHash implementation by Andre Oppermann). The transform itself was verified correct first (see below), then hygiene was reviewed against the file's own standard:

  • SipHash_End() wipes the entire context with bzero(ctx, sizeof(*ctx)) (siphash.c:188) β€” key-influenced state does not linger in the context.
  • SipHash_SetKey() (siphash.c:69-85) copies the caller's 16 raw key bytes into stack local uint64_t k[2] (line 71), uses them at lines 79-82, and returns without wiping. The verbatim key β€” stronger material than anything derivable from the context state alone β€” stays on the dead frame.

Demonstration (sip_keyscan.c): call SipHash24_Init + SipHash_SetKey from a noinline wrapper, return, then scan the caller's downward stack with an inline (call-free) byte comparison for the 16-byte key pattern.

Result (guest, stock kernel sources compiled as userland objects, -O0):

RETAINED: verbatim 16-byte key found on the dead stack, offset -112 below caller frame
DF-3564 CONFIRMED: raw key bytes persist on the stack after SipHash_SetKey returned

Methodological note: the first PoC attempt scanned with memcmp(3) and found nothing — the libc call's own frame landed exactly on the dead SetKey frame and overwrote it during comparison. The inline-loop version is the valid measurement. In the kernel the analogous overwrite risk means the residue is not guaranteed to survive arbitrary subsequent traffic processing, but it is present at the moment of return and survives the common SetKey→Update→End call pattern (Update's deeper frames sit below SetKey's frame), so exposure windows are real, frequent, and cheap to eliminate.

Why Low and not higher

No in-tree path exposes kernel stack contents to unprivileged readers. The finding is the standard key-residue hardening class: it raises the blast radius of any future unrelated info-leak primitive on the wg handler thread stack. Consumers hash with long-term secrets (wg_noise l_hash_key, wg_cookie MAC keys) at attacker-driven rates.

Correctness work that preceded this finding (negative results with weight)

Same-source KAT suite sip_kat.c, all PASS (2111 checks, 0 failures):

  • All 64 official reference vectors (key 000102..0f, in = first i bytes of 00..ff, SipHash-2-4) β€” identical to the in-tree sys/crypto/siphash/siphash_test.c table β€” match.
  • Kernel vs an independently written from-the-paper reference for SipHash-2-4 and SipHash-1-3 (the wg_cookie variant): 1024 cross-checks (3 keys Γ— 256 lengths Γ— 2 round-variants) match. (An earlier revision of my scratch reference had the partial-word byte placement reversed; the veorq-reference semantics β€” byte j at shift 8*j β€” were re-derived and the kernel confirmed correct. The failure was in my probe, not the kernel.)
  • Chunked-update equivalence (256 random 2-way and 3-way splits) exercises SipBuf partial-word buffering incl. buflen>0 crossing: matches.
  • SipHash_Final emits the LE encoding of SipHash_End: verified.

Memory-safety review: SipBuf bounds (MIN(len, 8 - buflen)), the final path (only writes buf.b8[7], never dereferences src), the aligned/unaligned word loops, and bytes (uint64) overflow were all traced and are sound. SipHash_Update(ctx, p, 0) trips the KASSERT on INVARIANTS kernels (harmless on production builds) β€” no in-tree caller passes 0; noted, not filed.

Fix validation

fix.diff adds explicit_bzero(k, sizeof(k)); after last use. Validated on a patched copy of the file (never applied to sys/):

  • baseline: key found at stack offset -112 (reproduced=1)
  • patched: NOT FOUND (exit 1) β€” residue eliminated
  • patched KAT suite: 2111/2111 PASS β€” fix is behavior-preserving

See fix_run.log.

Kernel references

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

One-line explicit_bzero(k, sizeof(k)) in SipHash_SetKey. Baseline: verbatim key found on dead stack at -112. Patched copy of the same source: NOT FOUND (residue eliminated). Full SipHash KAT suite on the patched copy: 2111/2111 PASS, so hashing behavior is unchanged.

fix_run.log: '=== DF-3564 fix validation ===' -> 'NOT FOUND ... exit=1 (1 = retention GONE = fix effective)' and 'RESULT: PASS: 2111 checks, 0 failures'.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (userland same-source validation; kernel unchanged)

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log lines under '############ sip_keyscan (DF-3564) ############': 'RETAINED: verbatim 16-byte key found on the dead stack, offset -112 below caller frame'. fix_run.log: patched copy -> 'NOT FOUND' + 'RESULT: PASS: 2111 checks, 0 failures'. Scanning is call-free (inline byte loop) so the probe cannot clobber the dead frame.

PoC changes

Initial seed sketch upgraded: (1) the naive memcmp(3)-based stack scan self-clobbered the dead SetKey frame (libc call frame landed on it) and always reported NOT FOUND; replaced with an inline call-free byte-compare loop which finds the key at offset -112. (2) scan window reduced to 32KB to stay inside the main-thread stack mapping. (3) correctness harness sip_kat.c written from scratch: 64 in-tree official vectors + independent from-paper reference for both SipHash-2-4 and the wg SipHash-1-3 variant + chunked-update equivalence.

Verified recommended fix

Add explicit_bzero(k, sizeof(k)) after the last XOR use of k in SipHash_SetKey (see fix.diff).

Verdict

SipHash_SetKey (sys/crypto/siphash/siphash.c:69-85) leaves a verbatim 16-byte copy of the caller's key in stack local k[2] (line 71) after returning, while the rest of the file wipes even the derived context state (SipHash_End bzero, line 188). Demonstrated in a same-source userland build of the exact audited file: the raw key bytes are found on the dead stack at offset -112 below the caller frame. WireGuard computes SipHash with long-term secret keys on the network-reachable handshake path (wg_noise.c:457,498; wg_cookie.c:136), so the residue appears at attacker-driven rates on kernel thread stacks. No standalone disclosure path exists in-tree, so impact is hardening-only (any future kernel-stack info leak would harvest it); one-line explicit_bzero eliminates it with no behavior change (KAT suite 2111/2111 before and after).