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

serpent_set_key() one-sided key-length validation: negative lengths wrap through u_int32_t i/lk into ~16 GiB runaway/wild OOB writes; non-byte lengths read past the key buffer

Summary

serpent_set_key() only rejects key_len > 256. i and lk are u_int32_t: key_len <= -63 makes lk ~ 0xFFFFFFFF so the load loop runs away, writing ctx->l_key[i] = key-buffer words for billions of indices while reading in_key OOB the whole way (~16 GiB linear smash); key_len in [-63,-32] makes a single wild read-modify-write at l_key[0xFFFFFFFF] (+16 GiB); key_len not a multiple of 8 (e.g. 129) reads up to 3 bytes past the caller's key buffer; key_len in [-31,-1] additionally computes 1<<(negative) (UB). All variants DEMONSTRATED live from a verbatim /usr/src build (3/3 fault demos: SIGSEGV at guard page for the runaway; SIGBUS at 0 for the wild index; SIGSEGV for the 129-bit OOB read). UNREACHABLE in-tree (cryptoapi pre-validates: serpent_cbc_setkey switches on 128/192/256, serpent_xts_setkey on 256/512 halved) - the serpent twin of twofish DF-0965/DF-2371, filed so the primitive is fixed at the source. Fix validated: 3/3 -> 0/3 faults and the 6349-check KAT corpus unchanged. Correctness of the primitive also proven live: 4 Linux testmgr KATs + 169 Botan verified-corpus vectors + 6000 random round-trips + 3x1000 iterated chains = 6349 checks 0 fails (an in-run cross-match failure was traced to the reviewer's own reference wiring, replaced by the published corpus).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3562 Β· 22 files
FileTypeDescriptionSize
README.md β€” 4.5 KB ↓ raw
VERDICT.md β€” 9.0 KB ↓ raw
verdict.json β€” 4.1 KB view raw
manifest.json β€” 1.5 KB view raw
build.sh β€” 145 B view raw
run.sh β€” 108 B view raw
serpoob.c β€” 4.9 KB view raw
fix.diff β€” 1.0 KB view raw
build.log β€” 1.3 KB view raw
run.log β€” 4.3 KB view raw
env.txt β€” 188 B view raw
fix_build.log β€” 937 B view raw
fix_run.log β€” 712 B view raw
wg_e2e.log β€” 257 B view raw
verification/serpent/serptest.c β€” 4.9 KB view raw
verification/serpent/kats_serpent.txt β€” 26.4 KB view raw
verification/twofish/twotest.c β€” 3.9 KB view raw
verification/rijndael-api/rijnapitest.c β€” 9.4 KB view raw
verification/chachapoly/cptest.c β€” 13.2 KB view raw
verification/chachapoly/shim_systm.h β€” 1.0 KB view raw
verification/chachapoly/shim_mbuf.h β€” 3.5 KB view raw
verification/chachapoly/prep.sh β€” 2.2 KB view raw

DF-3562 β€” serpent_set_key() unsigned key-length wraparound (pass-2, batch anchor pack)

This pack is the evidence anchor for the GLM-5.3 pass-2 audit of four DragonFlyBSD kernel crypto files:

file outcome
sys/crypto/serpent/serpent.c 1 new finding (DF-3562) + correctness verified
sys/crypto/twofish/twofish.c clean (known DF-0965/2371/0966 stand; correctness verified)
sys/crypto/rijndael/rijndael-api-fst.c clean pass 2 (known DF-0967/0968/0969/2369/2370 stand, two confirmed live; dead in-kernel code)
sys/crypto/chachapoly.c clean pass 2 (known DF-0970/2363/2364/2365 stand; correctness verified incl. live wg(4) tunnel)

Build

On the DragonFly guest (sources at /usr/src/sys, SHA256-verified identical to the audit tree β€” see env.txt era logs):

cd /tmp/DF-3562
./build.sh          # stages verbatim copies of the kernel sources; only
                    # kernel-only header includes are redirected to shims
                    # (recorded in verification/chachapoly/prep.sh)
./run.sh            # runs all four harnesses; exit 0 == all green

Prereqs: cc (guest base gcc 8.3), the pack directory.

What each part proves

  • verification/serpent/serptest β€” 4 Linux-mainline testmgr KATs + 169 vectors from Botan's verified serpent corpus (128/192/256-bit keys, multi-block up to 1 KB) + 6000 random round-trips + 1000x iterated chains: 6349 checks, 0 fails.
  • verification/serpent/serpoob β€” the DF-3562 OOB demonstrations (3/3 fault on pristine code, 0/3 on fixed code):
  • Case A key_len=-64: lk = (u_int32_t)((-64+31)/32) = 0xFFFFFFFF β†’ runaway ctx->l_key[i] fill loop (linear OOB write of key-buffer words, SIGSEGV at the first unmapped page).
  • Case B key_len=-32: i = (u_int32_t)(-32/32) = 0xFFFFFFFF β†’ single wild access at l_key[0xFFFFFFFF] (+16 GiB, SIGBUS).
  • Case C key_len=129: (129+31)/32 = 5 words read from a 17-byte key buffer β†’ 3-byte OOB read (SIGSEGV on a guard page).
  • verification/twofish/twotest β€” 6 published KATs (official ecb_ival-derived + LibTomCrypt) + 1000x iterated + 15000 random round-trips: 15018 checks, 0 fails.
  • verification/rijndael-api/rijnapitest β€” FIPS-197 ECB KATs, RFC 3602 CBC cases #1-#4, 192 pad round-trips, CFB1 single-block round-trip, and live confirmation records for the KNOWN pass-1 findings DF-0968 (CBC numBlocks==0 writes a full block past the advertised 8-byte payload β€” canary clobbered) and DF-0969 (ECB padDecrypt rejects valid padLen==16 on 6/6 exact-multiple inputs): 208 checks, 0 fails (known-bug confirmations counted separately).
  • verification/chachapoly/cptest β€” RFC 8439 Β§2.8.2 AEAD (ct+tag byte-exact), RFC 8439 A.5 decrypt+re-encrypt, draft-irtf-cfrg-xchacha A.1 both directions, tamper rejection (ct/tag/aad), 264 boundary round-trips (0..4096 B Γ— ad lengths Γ— chacha/xchacha Γ— separate and in-place), and the mbuf API through bounds-aborting DFly-faithful mbuf shims (9 lengths Γ— 6 fragmentation patterns byte-matched against the contiguous result, corruption β†’ EBADMSG): 22 checks, 0 fails.
  • wg_e2e.log β€” live kernel end-to-end: two wg(4) peers over loopback UDP (kldload if_wg), 200/200 + 20/20 (1300 B) pings, 0% loss β€” the real in-kernel chacha20poly1305 mbuf/handshake paths.

Fix validation (DF-3562)

fix.diff adds the missing lower-bound / byte-multiple check to serpent_set_key(). fix_build.log / fix_run.log show it applied to a verbatim /usr/src copy of the file, rebuilt, and re-run: baseline 3/3 faults β†’ patched 0/3 faults, KAT corpus still 6349/0. A full kernel rebuild was not performed for this fix: the changed line is unreachable from any in-tree caller (cryptoapi.c serpent_cbc_setkey / serpent_xts_setkey validate 128/192/256 (and 256/512 halved) bits before calling β€” see VERDICT.md), so no kernel code path can observe a behavior difference; the harness rebuild exercises the identical function source.

Files

build.sh run.sh                  exact build/run entry points
serpoob.c                        DF-3562 OOB demonstration
fix.diff                         the fix (git-apply-able vs sys/)
build.log run.log env.txt        full untrimmed guest logs
fix_build.log fix_run.log        fix-validation logs
wg_e2e.log                       live wg(4) end-to-end
verification/serpent/            serptest.c serpoob (built) kats_serpent.txt
verification/twofish/twotest.c
verification/rijndael-api/rijnapitest.c
verification/chachapoly/         cptest.c shim_systm.h shim_mbuf.h prep.sh
VERDICT.md
↓ download raw

DF-3562 VERDICT β€” serpent_set_key() unsigned key-length wraparound; batch anchor for the 4-file crypto pass 2

Finding (sys/crypto/serpent/serpent.c)

serpent_set_key() validates only the upper bound of key_len (if (key_len > 256) return;, serpent.c:456). i and lk are u_int32_t (serpent.c:453), so negative or non-byte key_len values wrap/round catastrophically:

  • key_len ≀ βˆ’63 β€” lk = (key_len + 31) / 32 truncates to a negative int (e.g. βˆ’64 β†’ βˆ’1) which wraps to ~0xFFFFFFFF when compared against the unsigned loop counter: the load loop (while (i < lk) ctx->l_key[i] = htole32(*(u32*)(in_key + 4*i));, serpent.c:461-464) runs away β€” a linear OOB write of key-buffer words into ctx->l_key[i] for billions of words (~16 GiB), reading the caller's key buffer OOB the whole way. Demonstrated live (run.log: SIGSEGV, si_code=2, at the first unmapped page below the frame).
  • key_len ∈ [βˆ’63, βˆ’32] (with (key_len+31)/32 == 0) β€” the load loop is skipped but i = key_len / 32 = βˆ’1/βˆ’2 β†’ (u_int32_t)0xFFFFFFFF/ 0xFFFFFFFE β†’ single wild read-modify-write at ctx->l_key[0xFFFFFFFF] (offset +16 GiB). Demonstrated live (SIGBUS, si_code=3). (key_len ∈ [βˆ’31, βˆ’1] additionally evaluates 1 << (key_len % 32) with a negative shift count β€” UB, benignly masked on x86.)
  • key_len not a multiple of 8 (e.g. 129) β€” lk = (129+31)/32 = 5 reads 20 bytes from a 17-byte key buffer: up to 3 bytes OOB read. Demonstrated live (SIGSEGV on a guard page).
  • key_len > 256 β€” silent early return leaves the caller's ctx untouched (cryptoapi pre-fills it with karc4random_buf, so no info leak there) and the session silently encrypts with a random key.

Reachability (why this is Low / defense-in-depth)

serpent_set_key has exactly one in-tree caller chain: cryptoapi.c serpent_cbc_setkey (cryptoapi.c:826-839) and serpent_xts_setkey (cryptoapi.c:927-943) β€” both switch on keylen_in_bytes * 8 and only call through for 128/192/256 (XTS: 256/512 halved to 128/256). cryptoapi_cipher_setkey is reached from hammer2/dm-crypt-style consumers with operator-supplied keys (root configured). No unprivileged or remote path exists today; the bug is the serpent twin of the twofish DF-0965/DF-2371 class (which pass 1 filed with the same reachability caveat). Filed so the primitive is fixed at the source, not at one caller.

Evidence

  • run.log β€” serpoob section: 3/3 fault demonstrations with per-child si_code/si_addr.
  • fix.diff, fix_build.log, fix_run.log β€” two-line fix (key_len < 0 || key_len > 256 || (key_len & 7)) applied to a verbatim /usr/src copy: 3/3 β†’ 0/3 faults, KAT corpus still 6349/0. git apply --check passes against the audit tree. No kernel rebuild was performed: the only in-tree caller pre-validates the argument, so no kernel code path can reach the changed line; the harness compiles the identical function source.

Correctness verification (all four files, live on the guest)

Guest: DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS) x86_64, gcc 8.3, guest /usr/src/sys SHA256-matched to the audit tree for all staged files.

serpent.c β€” 6349 checks, 0 fails

  • 4 Linux-mainline testmgr serpent KATs (0-bit key, 128, 256, and the …80 key), enc+dec.
  • 169 KATs extracted from Botan's verified serpent corpus (58/55/56 for 128/192/256-bit keys, incl. multi-block up to 1 KB), enc+dec.
  • 6000 random encβ†’dec round-trips + 1000x iterated chains per key size.
  • The 121/160-bit short-key padding branch was exercised in an earlier revision via direct serpent_set_key calls and cross-checked against the spec padding rule (l_key[kbits/32] = (l_key[…] & mask) | bit); the published corpus covers the lengths cryptoapi actually passes.

twofish.c β€” 15018 checks, 0 fails (pass-2: clean)

  • 6 published KATs (official ecb_ival-derived zero-key/0123…77/0123…ff + 3 LibTomCrypt self-test vectors), enc+dec.
  • 1000x iterated encrypt/decrypt chains per vector.
  • 15000 random round-trips across 128/192/256-bit keys.
  • Known findings DF-0965/DF-2371 (keylen validation β€” cryptoapi validates before calling, unreachable in-tree) and DF-0966 (me_key/ mo_key locals unzeroed) were NOT re-derived; no new defects found: h_fun/gen_mk_tab table writes bounded for k_len ∈ {2,3,4}; cryptoapi_cipher_freesession wipes the whole ctx (0xFF + explicit_bzero), so l_key/s_key/mk_tab do not leak.

rijndael-api-fst.c β€” 208 checks, 0 fails (pass-2: clean)

  • Reachability: zero in-tree callers (only test00.c, a userland test, references the API; opencrypto/wlan use rijndael-api.c / rijndael-alg-fst.c directly). The file is compiled into the crypto build (sys/conf/files:2270) but dead.
  • FIPS-197 ECB KATs (3 key sizes) + RFC 3602 CBC cases #1-#4 through blockEncrypt/blockDecrypt; 192 padEncrypt/padDecrypt round-trips; CFB1 single-block round-trip under the (documented-by-behavior) in-place API contract.
  • Known pass-1 findings confirmed live for the record: DF-0968 (blockEncrypt CBC with inputLen=64 bits: returns 0 but wrote a full 16-byte ciphertext block, clobbering a canary placed after the contract-sized 8-byte output buffer) and DF-0969 (padDecrypt ECB rejects the valid padLen==16 that padEncrypt generates β€” 6/6 exact-multiple inputs).
  • The KAME CFB1 branch never reads input in the encrypt direction and requires the plaintext preloaded in outBuffer (AESAVS/MOVS-era API contract) β€” a single-block round-trip passes once used correctly; the multi-block non-advancing bug remains DF-2369 (known).
  • Nothing new memory-unsafe beyond the known set; no info leak beyond DF-0967/DF-2370 (raw key retained in keyMaterial/stack).

chachapoly.c β€” 22 checks, 0 fails + live kernel e2e (pass-2: clean)

  • RFC 8439 Β§2.8.2: chacha20poly1305_encrypt reproduces the reference ct AND tag byte-exact (proves poly-key derivation at counter 0, IETF nonce split into words 13-15, message stream from counter 1, MAC construction incl. AD/data padding and LE64 lengths).
  • RFC 8439 A.5 (265-byte ct + tag): decrypt verifies, re-encrypt reproduces ctβ€–tag; tampered ct/tag rejected.
  • draft-irtf-cfrg-xchacha A.1: enc ct+tag byte-exact, dec round-trip, tamper rejection (hchacha20 subkey derivation + 4-zero-prefixed nonce).
  • 264 boundary round-trips (22 lengths 0..4096 Γ— 4 AD lengths Γ— {chacha, xchacha} Γ— {separate, in-place}) β€” exercises the update(64k)/final(tail) split at every 64-byte boundary.
  • mbuf API through DFly-faithful bounds-aborting shims (shim_mbuf.h mirrors m_copydata/m_copyback/m_getptr from sys/kern/uipc_mbuf.c): 9 lengths Γ— 6 fragmentation patterns byte-match the contiguous result; decrypt trims the tag; mid-chain ciphertext corruption β†’ EBADMSG; 8-byte and tag-only packets β†’ EBADMSG. No shim abort fired anywhere in _chacha20poly1305_mbuf.
  • Live kernel end-to-end (wg_e2e.log): kldload if_wg, two wg(4) peers over loopback UDP (handshake = xchacha20poly1305 cookie + chacha20poly1305 handshake AEAD; data = chacha20poly1305_encrypt/ decrypt_mbuf on real mbuf chains): 200/200 small + 20/20 Γ— 1300-byte pings, 0% loss.
  • Hunted, not found (negative results): nonce reuse at wg rekey β€” wg_noise.c enforces REKEY_AFTER_MESSAGES (2^60) / REJECT_AFTER_MESSAGES both directions (wg_noise.c:62-63,874,901-902) and the transport nonce is a 64-bit LE counter (wg_noise.c:996-997); cookie nonces are 24 fresh random bytes (wg_cookie.c:231). Tag comparison is timingsafe_bcmp (chachapoly.c:195) β€” constant-time. The 64β†’32-bit chacha_encrypt_bytes length truncation (DF-2364) and plaintext-before-tag-check (DF-2365) and poly_key-on-stack (DF-0970/DF-2363) remain the known set; nothing new. Harness debugging during this run initially suggested chacha/poly1305 anomalies (wrong tags) β€” all were traced to harness bugs (passing ct-length instead of ct+tag-length to decrypt; XORing keystream into non-zeroed in-place buffers; reading a vector from a mis-extracted awk range). Both primitives were additionally cross-verified against a pure-Python RFC 8439 Poly1305 oracle and the RFC's printed A.5 poly-key.

Bottom line

  • DF-3562 (new, Low, certain, memcorrupt bucket, unreachable in-tree): one-sided key-length validation in serpent_set_key() β†’ u32 wraparound β†’ runaway/wild OOB writes. Demonstrated live 3/3, fixed and re-verified (0/3, KATs 6349/0).
  • twofish.c, rijndael-api-fst.c, chachapoly.c: no new findings; correctness proven against published vectors (15018/208/22 checks, 0 fails) plus a live kernel wg(4) tunnel; two known rijndael-api findings (DF-0968, DF-0969) confirmed live as evidence for the existing records.

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

fix.diff adds the missing lower-bound/byte-multiple check to serpent_set_key(); applied to a verbatim /usr/src copy, rebuilt and re-run on the guest: baseline 3/3 OOB fault demonstrations -> 0/3 after the fix, 6349-check KAT corpus unchanged. No kernel rebuild performed: the only in-tree caller (cryptoapi) pre-validates keylen, so no kernel code path can reach the changed line - the harness builds the identical function source.

fix.diff, fix_build.log, fix_run.log
↓ fix.diffper-fix-DF-3562

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (serpoob section: 3/3 fault demonstrations with per-child si_code/si_addr), fix.diff + fix_run.log (0/3 faults after the fix, KATs 6349/0), verification/serpent/serptest + kats_serpent.txt (correctness), VERDICT.md (full narrative incl. reachability analysis and the other three files' verification)

PoC changes

Seed concept was a 4-byte-before-the-array write via l_key[-1]; live debugging showed the real primitive is u_int32_t wraparound (i/lk) producing a ~16GiB runaway fill loop or a +16GiB wild index access - serpoob.c was rewritten with signal-reporting children (si_code/si_addr) and a guard-page OOB-read case; harness also fixed: v4 KAT hex typo, Linux-vector reinterpretation (0-bit key / 16-byte key), replacement of a buggy self-written reference implementation with the Botan verified vector corpus (169 vectors).

Verified recommended fix

serpent.c:456: replace if (key_len > 256) with if (key_len < 0 || key_len > 256 || (key_len & 7)) (see fix.diff)

Verdict

serpent_set_key() (sys/crypto/serpent/serpent.c:451-474) validates only key_len > 256; with negative key_len the u_int32_t locals i/lk wrap ((u32)((key_len+31)/32) ~= 0xFFFFFFFF for key_len <= -63 -> runaway linear OOB write of l_key[i] sourced from OOB key-buffer reads; i = (u32)(key_len/32) = 0xFFFFFFFF for key_len in [-63,-32] -> single wild RMW at +16GiB; non-multiple-of-8 lengths read up to 3 bytes past the key buffer). All three variants demonstrated live on the guest from a verbatim /usr/src build (3/3 fault demos, SIGBUS/SIGSEGV with si_addr evidence in run.log). NOT reachable in-tree: the only caller chain (cryptoapi.c serpent_cbc_setkey:826 / serpent_xts_setkey:928) validates 128/192/256 (256/512 halved) first, so impact=none today - defense-in-depth parity with the twofish twins DF-0965/DF-2371. Two-line fix validated: 3/3 -> 0/3 faults, 6349-vector KAT corpus still clean.