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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3568 Β· 15 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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}:

  1. 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.
  2. Nr = 0: every keyBits branch is skipped β†’ return 0 (alg:814) β†’ ctx->Nr = 0, silently.
  3. Wild round-key walk: rijndaelEncrypt computes r = Nr >> 1 (= 0) and its main loop breaks only if (--r == 0) (alg:951-979; decrypt identical at 1132-1160) β€” r goes to -1 and the loop walks rk += 8 forever, 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 before rijndael_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.

  1. Guest source sha256 == audited tree (5727df5130a6ef37b93a299b598f9bb71241bed3f57b8686bdaf511457be8b2e).
  2. 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.
  3. 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.
  4. 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}-ecb outputs byte-identical.
  5. 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

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 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'
↓ fix.diffper-fix-DF-3568

Confirmed kernel references

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.