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)
PoC verification
Evidence pack
findings/poc/DF-3562 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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β runawayctx->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 atl_key[0xFFFFFFFF](+16 GiB, SIGBUS). - Case C
key_len=129:(129+31)/32 = 5words 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
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) / 32truncates to a negative int (e.g. β64 β β1) which wraps to ~0xFFFFFFFFwhen 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 intoctx->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 atctx->l_key[0xFFFFFFFF](offset +16 GiB). Demonstrated live (SIGBUS,si_code=3). (key_len β [β31, β1] additionally evaluates1 << (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 = 5reads 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-childsi_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/srccopy: 3/3 β 0/3 faults, KAT corpus still 6349/0.git apply --checkpasses 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
testmgrserpent KATs (0-bit key, 128, 256, and theβ¦80key), 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_keycalls 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_freesessionwipes 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 thecryptobuild (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=64bits: 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
inputin the encrypt direction and requires the plaintext preloaded inoutBuffer(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_encryptreproduces 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.hmirrorsm_copydata/m_copyback/m_getptrfrom 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-bitchacha_encrypt_byteslength 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
fixedfix.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
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.
No comments yet.