rijndael_set_key performs no keyBits validation; invalid bits -> key overread + Nr=0 wild round-key walk
Summary
rijndael_set_key is a void wrapper that forwards keyBits to rijndaelKeySetupEnc/Dec unvalidated; the only guard is the alg's KASSERT, compiled out on production kernels. Invalid bits (1) reads up to 32 bytes of key material regardless of keyBits (the rk[0..7] loads execute before/beside the keyBits branches) - a 24-byte overread past an 8-byte key; (2) returns Nr=0 silently; (3) makes rijndael_encrypt/decrypt walk the round-key pointer forever - a wild OOB read loop, panic-class in kernel context. DEMONSTRATED in-guest under -DNDEBUG production semantics: guard mode 8-byte key at page edge + bits=64 -> SIGSEGV at key+8 (first OOB byte of the rk[2] load); safe mode -> Nr=0 silently then rijndael_encrypt segfaults from the unbounded rk walk. FIPS-197 128/192/256 KATs pass alongside. No trigger path today - both in-tree consumers validate first (wlan_ccmp requires wk_keylen==16 on the remote CCMP path; cryptoapi validates 128/192/256 and XTS halves). Hazard is the void-return + assert-only API contract for future consumers. Same class as known DF-0963/DF-2368 on the -fst API - filed on the live consumed wrapper.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3568 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| verdict.json | β | 4.5 KB | view raw | |
| manifest.json | β | 982 B | view raw | |
| guard_rij_badbits.c | β | 2.7 KB | view raw | |
| rijkat.c | β | 2.4 KB | view raw | |
| rijndael-api_fixed.c | β | 2.2 KB | view raw | |
| build.sh | β | 125 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 387 B | view raw | |
| run.log | β | 1.7 KB | view raw | |
| guard.log | β | 435 B | view raw | |
| fixval.log | β | 1.3 KB | view raw | |
| env.txt | β | 203 B | view raw | |
| fix.diff | β | 1.0 KB | view raw |
DF-3568 β rijndael_set_key performs no keyBits validation; invalid bits β OOB key read + wild round-key walk (hardening)
File: sys/crypto/rijndael/rijndael-api.c:38-43 (wrapper, void return, no check)
chained into sys/crypto/rijndael/rijndael-alg-fst.c:734-815 (only a KASSERT at :738)
and :951-979 / :1132-1160 (encrypt/decrypt for(;;) loop breaks only on --r == 0).
Class: CWE-125 (key overread) + CWE-1284 (unvalidated crypto parameter), demonstrated
as a wild OOB read loop on non-INVARIANTS kernels.
Reachability today: NONE from unprivileged input β both consumers validate before
calling (sys/netproto/802_11/wlan_ccmp/ieee80211_crypto_ccmp.c:134-141 requires
wk_keylen == 16; sys/crypto/cryptoapi/cryptoapi.c:308-320 switch-validates 128/192/256,
XTS halves validated at :389-403). The API contract itself is the hazard: a future caller
that passes an unvalidated bits value gets, on a production (non-INVARIANTS) kernel:
1. reads of key[0..31] regardless of keyBits (up to 24 bytes past an 8-byte key), then
2. ctx->Nr = 0 (silent), then
3. rijndael_encrypt walks the round-key pointer forward forever
(r = Nr>>1 = 0, --r β -1, never 0) until it hits unmapped memory β panic-class.
Severity: Info (hardening) β no current trigger path; behavior chain proven on-guest.
Reproduce (in-guest, same-source userland-object pattern)
-DNDEBUG makes the userland KASSERT=assert (alg-fst.c:36) a no-op β exactly the
non-INVARIANTS production kernel semantics.
build: cc -O2 -DNDEBUG -I /usr/src/sys -o guard_rij_badbits \
rij/guard_rij_badbits.c /usr/src/sys/crypto/rijndael/rijndael-api.c \
/usr/src/sys/crypto/rijndael/rijndael-alg-fst.c
run: ./guard_rij_badbits guard # 8-byte key at page edge, bits=64
./guard_rij_badbits safe # 32-byte key, bits=64 β wild walk demo
Expected output (baseline)
$ ./guard_rij_badbits guard calling rijndael_set_key(&ctx, key8=0x800476ff8, bits=64) (guard page at 0x800477000) FAULT-CONFIRMED: sig=11 si_addr=0x800477000 (expected key+8 on guard page) exit=42 # first OOB byte load at key+8 (rk[2] load) $ ./guard_rij_badbits safe Segmentation fault (core dumped) # rijndael_encrypt Nr=0 wild walk exit=139
si_addr == key+8 β key material is read past keyBits/8 (the rk[0..3] loads at
alg-fst.c:740-743 run before any keyBits branch). The safe-mode segfault proves the
Nr=0 wild round-key walk (would be a kernel panic in-kernel).
Expected output (with fix.diff applied to a copy of the source)
$ ./guard_rij_fixed guard BUG-NOT-PRESENT: survived bits=64 with 8-byte key exit=0 # no key read at all for invalid bits $ ./guard_rij_fixed safe safe mode: bits=64 accepted silently, ctx.Nr=0 (0 = unusable schedule, no error surfaced) exit=0 # encrypt refuses to run (returns early)
Correctness cross-check
rijkat: FIPS-197 C.1/C.2/C.3 AES-128/192/256 vectors all PASS, Nr β {10,12,14};
in-guest LibreSSL openssl enc -aes-{128,192,256}-ecb outputs byte-identical (run.log).
Still PASS after the fix (fixval.log).
See guard.log (baseline), fixval.log (patched), run.log (KAT + cross-match).
DF-3568 VERDICT
Status: reproduced (same-source userland-object harness, in-guest) β impact: none (all in-tree callers validate). Fix: validated at harness level (fixed).
What was audited
Pass-2 full audit of sys/crypto/rijndael/rijndael-api.c (57 LOC): the
makeKey-style glue consumed by wlan CCMP (remote packet path) and cryptoapi
(dm-crypt, hammer2). rijndael-api-fst.c (the other, unused API) went clean as
dead code in a prior batch; this file is live.
Root cause
rijndael_set_key(ctx, key, bits) (rijndael-api.c:38-43) is void, returns no
error, and passes bits to rijndaelKeySetupEnc/Dec unvalidated. The only guard
is a KASSERT in the alg (rijndael-alg-fst.c:738) β compiled out on production
(non-INVARIANTS) kernels. With bits β {128,192,256}:
- Key overread: the rk[0..3] loads (alg:740-743) run before any keyBits branch, and rk[4..7] (alg:762-785) run whenever bits != 128/192 β so up to 32 bytes of key material are read regardless of the actual key size.
- Nr = 0: every keyBits branch is skipped β
return 0(alg:814) β ctx->Nr = 0, silently. - Wild round-key walk:
rijndaelEncryptcomputesr = Nr >> 1(= 0) and its main loop breaks onlyif (--r == 0)(alg:951-979; decrypt identical at 1132-1160) β r goes to -1 and the loop walksrk += 8forever, reading progressively further past the 60-word schedule until unmapped memory. In kernel context: panic-class DoS.
Reachability (why Info)
Both in-tree consumers validate before calling:
ieee80211_crypto_ccmp.c:134-141β ccmp_setkey rejects wk_keylen != 16 beforerijndael_set_key(&ctx->cc_aes, k->wk_key, k->wk_keylen*NBBY).cryptoapi.c:308-320β aes_cbc_setkey switch-validates 128/192/256; XTS halves validated (cryptoapi.c:376-386, 398-403) to 128/256 each.
Both consumers are privileged/config-driven (wpa_supplicant-driven setkey for the first, dmsetup/hammer2 mount for the second), and neither can presently deliver an invalid bits value. The hazard is the API contract for future callers: void return + no validation + assert-only guard.
Verification (guest: DragonFly 6.5-DEVELOPMENT #0, gcc 8.3 [DragonFly])
-DNDEBUG disables the userland KASSERT=assert (alg-fst.c:34-36) β exact
production-kernel semantics.
- Guest source sha256 == audited tree (5727df5130a6ef37b93a299b598f9bb71241bed3f57b8686bdaf511457be8b2e).
- Baseline overread (guard.log): 8-byte key at a page edge, bits=64 β SIGSEGV,
si_addr == key+8(exit 42) β the first OOB byte load of the rk[2]=GETU32(key+8) word. Proves key is read past keyBits/8. - Baseline wild walk (guard.log): 32-byte safe buffer, bits=64, then
rijndael_encryptβ SIGSEGV exit 139 β the Nr=0 round-key walk (no handler installed in safe mode, hence raw segfault). In-kernel equivalent: panic. - Correctness (run.log): FIPS-197 C.1/C.2/C.3 vectors PASS for 128/192/256 with
Nr = 10/12/14; in-guest LibreSSL
openssl enc -aes-{128,192,256}-ecboutputs byte-identical. - Fix validation (fixval.log): with fix.diff (reject invalid bits without reading key; encrypt/decrypt refuse Nr β {10,12,14}) applied to a source copy: guard mode exit 0 (no key read at all), safe mode exit 0 (no wild walk β rijndael_encrypt returns early; note the harness's "produced garbage" line predates the fix semantics: with the fix ct is simply left untouched), rijkat still all-PASS.
Exploit chain
None β no unprivileged trigger path; consumers validate. Hardening only.
kernel_refs
- sys/crypto/rijndael/rijndael-api.c:38-43 (unvalidated bits, void return)
- sys/crypto/rijndael/rijndael-alg-fst.c:738 (KASSERT-only guard)
- sys/crypto/rijndael/rijndael-alg-fst.c:740-785 (unconditional key loads)
- sys/crypto/rijndael/rijndael-alg-fst.c:951-979, 1132-1160 (Nr=0 wild walk)
- sys/netproto/802_11/wlan_ccmp/ieee80211_crypto_ccmp.c:134-141 (validating caller)
- sys/crypto/cryptoapi/cryptoapi.c:308-320, 398-403 (validating caller)
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 applied to a source copy (never sys/): guard mode prints BUG-NOT-PRESENT exit 0 (no key read for invalid bits), safe mode exit 0 (encrypt refuses Nr=0, wild walk gone), and rijkat_fixed still passes all FIPS-197 vectors. Harness-level validation only (no kernel build) - justified because no in-tree caller can deliver invalid bits today.
fixval.log: 'BUG-NOT-PRESENT: survived bits=64 with 8-byte key' exit=0; safe exit=0; rijkat_fixed 'RESULT-PASS: 0 failures'
Confirmed kernel references
- sys/crypto/rijndael/rijndael-api.c:38
- sys/crypto/rijndael/rijndael-alg-fst.c:738
- sys/crypto/rijndael/rijndael-alg-fst.c:740
- sys/crypto/rijndael/rijndael-alg-fst.c:784
- sys/crypto/rijndael/rijndael-alg-fst.c:814
- sys/crypto/rijndael/rijndael-alg-fst.c:951
- sys/crypto/rijndael/rijndael-alg-fst.c:979
- sys/netproto/802_11/wlan_ccmp/ieee80211_crypto_ccmp.c:134
- sys/crypto/cryptoapi/cryptoapi.c:308
Detail
Evidence (decisive lines)
guard.log: 'FAULT-CONFIRMED: sig=11 si_addr=0x800477000' (== key+8) and safe-mode 'Segmentation fault ... exit=139' (Nr=0 wild walk); fixval.log: guard mode exit 0, safe mode exit 0, rijkat_fixed 'RESULT-PASS: 0 failures'; run.log: FIPS-197 C.1/C.2/C.3 PASS + in-guest LibreSSL aes-ecb byte-identical cross-match
PoC changes
No seed PoC existed; authored guard_rij_badbits.c (guard + safe modes) and rijkat.c. Compile fix en route: DragonFly siginfo_t uses si->si_addr and _exit needs unistd.h. Note: the safe-mode print text predates fix semantics - with the fix, rijndael_encrypt returns early leaving ct untouched (the exit=0 and absence of SIGSEGV is the decisive signal).
Verified recommended fix
rijndael-api.c: reject keyBits not in {128,192,256} before touching key material and have rijndael_encrypt/decrypt refuse schedules with Nr not in {10,12,14} (see fix.diff).
Verdict
rijndael_set_key (rijndael-api.c:38-43) is a void wrapper that passes keyBits to rijndaelKeySetupEnc/Dec unvalidated; the only guard is the alg's KASSERT (alg-fst.c:738), compiled out on production kernels. Demonstrated in-guest with -DNDEBUG (production semantics) via the same-source userland-object pattern: (a) bits=64 with an 8-byte key faults at key+8 - the rk[0..3]/rk[4..7] loads (alg-fst.c:740-785) read up to 32 bytes of key material before any keyBits branch; (b) bits=64 with a safe buffer silently yields Nr=0, and the subsequent rijndael_encrypt runs the r=Nr>>1 loop (alg-fst.c:951-979) with r going negative - a wild round-key walk that segfaults (exit 139; panic-class in kernel). Impact is none today: both in-tree callers validate (ccmp_setkey requires wk_keylen==16 at ieee80211_crypto_ccmp.c:134-141; cryptoapi validates 128/192/256 and XTS halves at cryptoapi.c:308-320,398-403). Filed as Info hardening of the API contract (void return, assert-only guard) for future callers. Fix validated at harness level: rejecting invalid bits without reading key plus Nr-invariant guards in encrypt/decrypt removes both the overread and the wild walk, with FIPS-197 128/192/256 KATs still passing.
No comments yet.