# DF-3561 VERDICT — reproduced (kernel + same-source harness), fix validated

**Status: reproduced. Impact: silent persistent data corruption of software-XTS
dm-crypt volumes (local integrity/availability attack; no memory corruption,
no info leak, no privilege escalation). Confidence: certain.**

## Root cause (path:line)

- `sys/crypto/cryptoapi/cryptoapi.c:370-374` — `struct aes_xts_ctx` embeds
  `uint8_t tweak[16]` inside the per-session heap context (allocated once in
  `cryptoapi_cipher_newsession`, `cryptoapi.c:1045-1088`), shared by every
  crypt call on the session.
- `sys/crypto/cryptoapi/cryptoapi.c:412-428` — `aes_xts_reinit()` memcpy's the
  IV into the shared `ctx->tweak` and encrypts it in place at the start of
  **every** crypt call.
- `sys/crypto/cryptoapi/cryptoapi.c:216-250` — `crypt_block_xts()` /
  `crypt_data_xts()` then read and GF(2^128)-advance that same shared buffer
  per block for the whole request.
- Consumers: `sys/dev/disk/dm/crypt/dm_target_crypt.c:670` (strategy),
  `:898-921` (write encrypt), `:959-983` (`dmtc_bio_encdec` per-sector loop)
  — all call `cryptoapi_cipher_crypt()` on the single shared
  `priv->crypto_session` with no lock; multiple bios are processed
  concurrently (one per issuing CPU/thread).
- Identical defect in `twofish_xts_crypt` (`cryptoapi.c:780-792`,
  `struct twofish_xts_ctx` `:725-729`, reinit `:731-751`) and
  `serpent_xts_crypt` (`cryptoapi.c:945-957`, struct `:888-892`, reinit
  `:894-916`).
- Not affected: CBC paths (context read-only during crypt — proven by the
  3.7M-iteration control run with 0 mismatches) and the AES-NI XTS path
  (tweak derived on stack inside `aesni_encrypt_xts`).

## How it was proven

1. **Same-source userspace harness** (compiles the exact kernel translation
   units from `/usr/src`, sha256-verified identical to the audited tree):
   4 pthreads racing `cryptoapi_cipher_encrypt` on ONE aes-xts session with
   two distinct sector IVs against single-threaded reference outputs.
   Result: **983,991 corrupted outputs / 984,352 iterations (99.96%)**;
   CBC control on the same session: 0 / 3,734,502. `race.log`.
2. **Live kernel, end-to-end**: kldload'ed dm_target_crypt on the stock
   INVARIANTS guest kernel, 64 MB vn-backed volume,
   `dmsetup create cry0 --table "0 131072 crypt aes-xts-plain64 <256-bit key> 0 /dev/vn0 0"`,
   `hw.aesni_disable=1` to force the software path. Serial write/readback
   round-trips byte-perfect; two concurrent `dd` writers to disjoint halves →
   **5/5 rounds, both halves CORRUPT on readback**. `kernel_dm_crypt_demo.log`.
3. **Fix**: `fix.diff` moves the tweak to a stack buffer in all three
   software-XTS functions (and drops the dead struct member).
   `git apply --check` clean against `sys/`. Rebuilt the identical harness
   against the patched source: **0 corrupted / 3,401,362 iterations**, XTS
   correctness suite still 11/11 (reference cross-check incl. wrap-edge
   sectors + round-trips for AES/Twofish/Serpent). `fix_run.log`,
   `fix_build.log`.

## Exploit chain / impact ceiling

Any local user performing concurrent IO (two `dd`s, parallel file writes) on a
software-XTS dm-crypt volume silently corrupts the ciphertext being written —
data at rest is destroyed with no error reported (writes "succeed"). This is
an integrity/availability attack (CWE-362 race → wrong result), not memory
corruption: there is no OOB, no leak, no escalation path. Mitigating factor:
systems whose CPU has AES-NI (and `hw.aesni_disable=0`, the default) take the
race-free hardware path; the software path is the fallback for non-AES-NI
CPUs, VMs without AESNI passthrough, and `hw.aesni_disable=1` configurations.

## Why not kernel-rebuild for the fix

The finding is a correctness race, not memory corruption, so the mandatory
kernel-rebuild loop does not apply. The fix was validated on the byte-identical
translation unit compiled by the same harness on the same guest (0/3.4M vs
983,991/984K baseline), and `git apply --check` confirms the diff fits `sys/`.

## Side verification performed in the same run (all four audited files)

- rijndael-alg-fst.c: FIPS-197 KATs (128/192/256, enc+dec) + 2000 random
  round-trips + 30 ECB vectors cross-matched to LibreSSL: 2003/2003 + 30/30,
  0 fails. Static proof that Td0..Td3 are rotations of Td0, Td4 = Si
  replicated, Si∘S = id, and Td0/Td0-matrix = InvMixColumns∘Si: PASS
  (`verification/static_checks.log`).
- sha2.c: anchors (verified against Python hashlib + LibreSSL),
  incremental-vs-oneshot for every length 0..1024 × 3 chunk patterns
  (exercises every padding boundary), 26 lengths × {sha256,sha384,sha512}
  cross-matched to LibreSSL `dgst`: 2082/2082 + 78/78, 0 fails.
- curve25519.c: RFC 7748 §5.2 vectors, iterated 1×/1000×, §6.1 DH both
  directions, base-point public-key generation, 7 canonical low-order points
  → all-zero shared secret: 15/15, 0 fails; edge inputs cross-verified against
  an independent pure-Python RFC 7748 oracle (4/4); field-arithmetic core
  token-identical to Linux mainline `lib/crypto/curve25519-fiat32.c` (7/7
  functions; formally-verified lineage).
- cryptoapi.c: XTS vs independent IEEE-1619 reference across 7 sector values
  (incl. 0, 1, 2^63, 2^64-1), CBC vs LibreSSL, round-trips for AES/Twofish/
  Serpent in CBC+XTS: all 0 fails. Full log: `verification/run.log`.

Harness bugs encountered and fixed during verification (not code defects):
wrong Nr passed by my first KAT (kl/16+6 instead of kl/32+6), vector-parser
off-by-one-token, a typo'd memorized SHA384("") anchor, and transcription
swaps in the disputed low-order list — each resolved against independent
oracles (LibreSSL, Python hashlib, Python RFC 7748 ladder). The four audited
sources were correct in every case.

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