# DF-2368 VERDICT — no-op keyBits validation -> Nr=0 -> runaway round loop / OOB read

**Status: REPRODUCED** (impact: panic — kernel DoS landmine; confidence: certain)
**Rig:** DragonFly 6.5-DEVELOPMENT x86_64, stock X86_64_GENERIC with
`options INVARIANTS` (config line 56) and `device crypto` (line 299), QEMU/KVM,
kernel-compiled sysctl harness (runtime kldload GPFs on this guest).
**Guest:** `DragonFly dfbsd 6.5-DEVELOPMENT #1/#2 ... x86_64` (env.txt).

## What the finding claims and what was proven

### Claim 1 — validation is KASSERT-only (CWE-617)

`rijndaelKeySetupEnc` (rijndael-alg-fst.c:734-815) validates `keyBits` only via
`KASSERT(keyBits == 128 || keyBits == 192 || keyBits == 256, ...)` at lines
738-739. The public kernel KPI wrapper `rijndael_set_key`
(rijndael-api.c:37-43) passes `bits` through with no check of its own.

REPRODUCED on the stock INVARIANTS kernel (kernel #1 + rig):

    RIJNH: cmd="setkey 0"
    RIJNH: setkey: calling real rijndael_set_key(bits=0) now (ctx pre-zeroed)
    panic: Invalid key size (0).
    rijndaelKeySetupEnc() at rijndaelKeySetupEnc+0x3ed
    rijndaelKeySetupEnc() at rijndaelKeySetupEnc+0x3ed
    rijndael_set_key() at rijndael_set_key+0x1f
    rijnh_run_setkey() ... rijnh_sysctl() ...
    Debugger("panic")                                    (panic.setkey0.txt)

The panic IS the assertion firing — proof that the assert is the only gate. On a
production (INVARIANTS-free) build the same call path compiles the KASSERT to
`do { } while (0)` and falls through: none of the three `if (keyBits == ...)`
branches is taken (lines 744/764/786) and the function returns **0** at line 814.
The wrapper stores `ctx->Nr = 0`. Note rk[0..7] are written unconditionally
before the branches (lines 740-743, 762-763, 784-785), so the stored state is a
partially-written schedule with Nr=0. (`rijndaelKeySetupDec` has the same
KASSERT-only shape and inherits the fix through `rijndaelKeySetupEnc`.)

### Claim 2 — Nr=0 makes the round loop run away and read OOB

`rijndaelEncrypt` (line 862) compiles the `#else /* !FULL_UNROLL */` path
(FULL_UNROLL is defined nowhere in the tree — verified): the loop

    r = Nr >> 1;                       /* line 951: 0 when Nr==0 */
    for (;;) {
        ... 4 lookups x 4 words ...    /* reads rk[4..7] */
        rk += 8;                       /* line 978: +32 bytes per iteration */
        if (--r == 0) break;           /* line 979: r was 0 -> -1, -2, ... */
        ... reads rk[0..3] ...
    }

never breaks (`--r` starting at 0 goes negative; it would need 2^32-1 decrements
to wrap to 0 again) and walks `rk` forward 32 bytes per iteration, past the
60-word `ek`/`dk` schedules.

REPRODUCED — the rig builds a REAL `rijndael_ctx` with a valid 128-bit
`rijndael_set_key` at the end of a mapped (pre-faulted) user page followed by an
unmapped page, forces `ctx->Nr = 0` (byte-identical to the state the broken
`rijndael_set_key` stores), and calls the real `rijndael_encrypt`
(`rijndael-api.c:53-57`) — i.e. the full public API, not a synthetic call:

    RIJNH: poison: ctx built (Nr was 10), ctx->Nr forced to 0; entering rijndael_encrypt(ctx) now
    Fatal user address access from kernel mode from rijnh at ffffffff809f1c09
    Fatal trap 12: page fault while in kernel mode
    fault virtual address  = 0x800475000
    fault code             = supervisor read data, page not present
    Stopped at rijndaelEncrypt+0x1d9: xorl -0x8(%rdi),%ecx   (panic.poison.txt)

The faulting VA is 0x1200 (4608) bytes past `ek` — i.e. the loop executed ~144
iterations and read **4128 bytes past the end of the entire 488-byte ctx**
(DragonFly services kernel-mode demand faults on user VA while the surrounding
mapping exists, so the walk crossed the ctx page and stopped at the first truly
non-present page). In the real in-kernel scenario the ctx lives in kernel memory
(`wlan_ccmp` ctx, `cryptoapi` session ctx), where the same walk would consume
adjacent kernel objects and only trap — if ever — at an unmapped KVA hole up to
gigabytes later: exactly the claimed kernel hang/panic. Identical result via a
direct `rijndaelEncrypt(ek, 0, ...)` call (panic.direct.txt), confirming the
runaway is in the core function, not the wrapper.

### Control (not a rig bug)

16/16 KAT vectors (FIPS-197 App. C.1-C.3, SP800-38A ECB, and an OpenSSL-derived
extended set) PASS through the same real functions with correct encrypt+decrypt
round-trip (kat_baseline_dmesg.txt); the instrumented twin used for the DF-2367
evidence matches the real function byte-exact on 600 random vectors.

## Reachability honesty (who can actually hit this)

Not reachable from unprivileged userspace today — every in-tree caller
pre-validates the key size:

- `sys/netproto/802_11/wlan_ccmp/ieee80211_crypto_ccmp.c:130-141` — `ccmp_setkey`
  rejects `wk_keylen != 16`; keys arrive via privileged ioctls / association
  paths that fix 128-bit CCMP keys.
- `sys/crypto/cryptoapi/cryptoapi.c:305-317` (`aes_cbc_setkey`) and `:399-411`
  (`aes_xts_setkey` via `aes_xts_valid_keysize_in_bits`, 256/512 only) — the
  only cryptoapi consumer is `sys/dev/disk/dm/crypt/dm_target_crypt.c` (dm-crypt),
  whose table configuration is privileged.
- `sys/crypto/rijndael/rijndael-api-fst.c:48-52` (`rijndael_makeKey`) validates.
- opencrypto/IPsec do NOT use this rijndael at all (AES-NI/xform path).

So the finding stands as filed: a latent DoS landmine at the kernel KPI boundary
(one future caller that forgets to pre-validate turns invalid keyBits into a
kernel hang/panic), Low severity, CWE-617. The guest harness reaches it only
because the rig is compiled into the kernel for verification.

## Privesc assessment (team directive: crypto-core class — close or flag)

CLOSED as non-privesc with reasoning: the runaway loop performs OOB **reads
only** (round-key words are XOR-ed into on-stack cipher state; nothing OOB is
written), the walked data never reaches userspace, and the terminal behavior is
a fatal trap. No write primitive, no controlled pointer, no info return path —
there is no escalation chain from this bug. Impact ceiling: kernel DoS.

## Fix validation (A/B, same guest, same rig)

`fix.diff` (generated against the read-only sys/ tree; applied only inside the
guest /usr/src with `patch -p1`; `git apply --check` clean on the host):

1. `rijndaelKeySetupEnc`/`rijndaelKeySetupDec`: runtime check returning `-1` for
   keyBits not in {128,192,256} (KASSERT kept for INVARIANTS builds).
2. `rijndael_set_key`: rejects invalid `bits` — stores `Nr = 0` and zeroes
   `ek`/`dk` (no stale schedule reuse) without calling the setup functions.
3. `rijndaelEncrypt`/`rijndaelDecrypt`: guard `Nr != 10 && != 12 && != 14` ->
   immediate return, killing the runaway even for direct callers.

Baseline (kernel #1) vs fixed (kernel #2):

| input / op            | stock kernel                    | fixed kernel                          |
|-----------------------|---------------------------------|---------------------------------------|
| setkey 0/1/127/257    | panic (assertion)               | no panic; Nr=0, ek/dk zeroed          |
| setkey 128/192/256    | Nr=10/12/14                     | Nr=10/12/14, identical schedules      |
| poison (Nr=0 + encrypt)| Fatal trap 12 in rijndaelEncrypt| SURVIVED (guard returns), ct untouched|
| direct rijndaelEncrypt| Fatal trap 12                   | SURVIVED                              |
| KAT 16 vectors        | 16/16 PASS                      | 16/16 PASS, bit-identical ciphertexts |
| twinval               | 600/600 match                   | 300/300 match                         |

Full log: fixrun_dmesg.txt (fixed), kat_baseline_dmesg.txt (baseline).
Bug inputs now rejected/clamped and all FIPS-197 vectors bit-identical — fix
objective met.

## Notes

- The guest was returned to the pristine `with-src` snapshot after verification.
- PoC sources: rijndael_harness.c (kernel rig), rijnh.c (userland driver);
  sha256 in verdict.json.

## 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.
