Software XTS (aes/twofish/serpent) mutates the shared per-session tweak: concurrent cryptoapi_cipher_crypt on one session tears the tweak and silently corrupts dm-crypt data at rest
Summary
struct aes_xts_ctx (and the twofish/serpent twins) embeds tweak[16] in the heap session context shared by all crypt calls on the session. aes_xts_reinit() overwrites ctx->tweak at the start of every call and crypt_block_xts()/crypt_data_xts() then read and GF(2^128)-advance that same buffer per block for the whole request. cryptoapi exposes no locking, and its only in-tree consumer drives one shared priv->crypto_session from concurrent bio paths with no serialization (dm_target_crypt.c:670,898,959-983). Two simultaneous IOs on a software-XTS dm-crypt volume tear each other's tweak, so wrong ciphertext is persisted with successful write status - silent, persistent data corruption. Same defect at cryptoapi.c:725-792 (twofish) and :888-957 (serpent). CBC paths are context-read-only (proven by control) and the AES-NI XTS path keeps the tweak on the stack, so both are unaffected. REPRODUCED two ways on the live guest: (1) same-source harness, 4 pthreads racing one aes-xts session vs single-threaded references -> 983,991 corrupted outputs / 984,352 iterations (99.96%), CBC control 0/3,734,502; (2) live kernel end-to-end: dm_target_crypt + hw.aesni_disable=1 + 64MB vnconfig + dmsetup aes-xts-plain64 -> serial write byte-perfect, two concurrent dd writers -> 5/5 rounds both halves CORRUPT on readback. Fix validated (tweak moved to stack buffer in all three XTS functions + explicit_bzero): 0 corrupted / 3,401,362 iterations, XTS correctness 11/11, git apply --check clean. Not memory corruption; data-at-rest integrity/availability. Non-AES-NI CPUs, VMs without AESNI, or hw.aesni_disable=1 resolve to software XTS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3561 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| cryptoapi_test.c | β | 8.6 KB | view raw | |
| guest_build.sh | β | 878 B | view raw | |
| fix.diff | β | 4.0 KB | view raw | |
| cryptoapi.c.fixed | β | 26.8 KB | β download | |
| fix_build.log | β | 2.2 KB | view raw | |
| fix_run.log | β | 472 B | view raw | |
| kernel_dm_crypt_demo.log | β | 1.6 KB | view raw | |
| verification/ | β | 4.0 KB | β download |
DF-3561 β cryptoapi software XTS mutates shared per-session tweak (concurrency race β silent dm-crypt data corruption)
What
sys/crypto/cryptoapi/cryptoapi.c β the software XTS implementations
(aes_xts_crypt, twofish_xts_crypt, serpent_xts_crypt) keep the 16-byte
tweak inside the shared, heap-allocated session context
(struct aes_xts_ctx.tweak, cryptoapi.c:373). Every call to
cryptoapi_cipher_crypt() on the same session:
*_xts_reinit()overwritesctx->tweak(cryptoapi.c:422-427), andcrypt_data_xts()βcrypt_block_xts()reads and advances the same shared tweak block-by-block for the whole operation (cryptoapi.c:217-236, 242-250).
cryptoapi exposes no locking. Its only in-tree consumer, dm_target_crypt
(sys/dev/disk/dm/crypt/dm_target_crypt.c), invokes
cryptoapi_cipher_crypt() on one shared priv->crypto_session from
concurrently-running bio paths with no serialization
(dm_target_crypt_strategy β dmtc_bio_write_encrypt β dmtc_bio_encdec,
dm_target_crypt.c:670, 898, 959-983).
Result: two simultaneous IOs on one software-XTS dm-crypt volume tear each other's tweak β wrong ciphertext is silently persisted to disk and decryption returns garbage. Persistent, silent data corruption of encrypted volumes triggered by ordinary concurrent IO from any local user.
The CBC paths are unaffected (no context mutation β verified as a control).
The AES-NI XTS path (aesni_encrypt_xts, stack-local tweak) is unaffected.
Reproduce (same-source userspace harness, exact kernel translation units)
# on the DragonFly guest (sources at /usr/src verified identical to audited tree) sh guest_build.sh # builds rijndael_kat, sha2_kat, curve25519_kat, cryptoapi_test, cross_check ./cryptoapi_test race # 4 threads racing on ONE cryptoapi XTS session # expected (vulnerable): "RACE aes-xts : ... corrupted outputs -> CORRUPTION OBSERVED", exit 3 # "RACE aes-cbc : ... 0 corrupted outputs" (control)
Observed (stock kernel sources, 6 vCPU guest):
RACE aes-xts : 984352 iterations, 983991 corrupted outputs -> CORRUPTION OBSERVED
RACE aes-cbc : 3734502 iterations, 0 corrupted outputs -> no corruption
Reproduce (live kernel, end-to-end)
See kernel_dm_crypt_demo.log: kldload dm_target_crypt, vn-backed 64 MB
volume, dmsetup create cry0 --table "0 131072 crypt aes-xts-plain64 <key> 0 /dev/vn0 0",
sysctl hw.aesni_disable=1 (forces the software path). Serial write+readback:
OK. Two concurrent dd writers to disjoint halves: 5/5 rounds both halves
CORRUPT.
Fix validation
fix.diff (git-apply-able against sys/, verified with git apply --check)
moves the tweak to a stack buffer in all three software XTS functions and
drops the dead struct member. Rebuilt the identical harness against the
patched source:
RACE aes-xts : 3401362 iterations, 0 corrupted outputs -> no corruption β
and XTS correctness suite still 11/11 (reference cross-check + round-trips).
Files
cryptoapi_test.cβ race PoC + CBC/XTS correctness harness (the finding's trigger)guest_build.shβ exact build commandsguest_race.shβ race + LibreSSL X25519 cross-match attemptfix.diffβ the verified fixcryptoapi.c.fixedβ patched translation unit used for fix validationfix_build.log,fix_run.logβ patched-source build + race resultskernel_dm_crypt_demo.logβ end-to-end in-kernel demonstration transcriptverification/β the four-file correctness verification suite (rijndael/sha2/curve25519 KATs, LibreSSL cross-match, full logs, env, static checks)
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_ctxembedsuint8_t tweak[16]inside the per-session heap context (allocated once incryptoapi_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 sharedctx->tweakand 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_encdecper-sector loop) β all callcryptoapi_cipher_crypt()on the single sharedpriv->crypto_sessionwith 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) andserpent_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
- Same-source userspace harness (compiles the exact kernel translation
units from
/usr/src, sha256-verified identical to the audited tree): 4 pthreads racingcryptoapi_cipher_encrypton 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. - 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=1to force the software path. Serial write/readback round-trips byte-perfect; two concurrentddwriters to disjoint halves β 5/5 rounds, both halves CORRUPT on readback.kernel_dm_crypt_demo.log. - Fix:
fix.diffmoves the tweak to a stack buffer in all three software-XTS functions (and drops the dead struct member).git apply --checkclean againstsys/. 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 dds, 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.
Fix verification
fixedfix.diff moves the tweak to a stack buffer in aes_xts_crypt/twofish_xts_crypt/serpent_xts_crypt (out-parameter in *_xts_reinit) and drops the dead struct member; git apply --check clean against sys/. Rebuilt the identical harness against the patched translation unit on the same guest: 0 corrupted outputs / 3,401,362 iterations (baseline 983,991/984,352) and XTS correctness suite still 11/11. Kernel image not rebuilt: the finding is a correctness race (not memory corruption), and the harness compiles the byte-identical kernel source (sha256-verified against /usr/src), so the source-level fix proof is exact.
fix_run.log: 'RACE aes-xts : 3401362 iterations, 0 corrupted outputs -> no corruption', 'SUMMARY cryptoapi_xts tests=11 fails=0'; fix.diff applies with git apply --check
Confirmed kernel references
- sys/crypto/cryptoapi/cryptoapi.c:370
- sys/crypto/cryptoapi/cryptoapi.c:412
- sys/crypto/cryptoapi/cryptoapi.c:216
- sys/crypto/cryptoapi/cryptoapi.c:725
- sys/crypto/cryptoapi/cryptoapi.c:780
- sys/crypto/cryptoapi/cryptoapi.c:888
- sys/crypto/cryptoapi/cryptoapi.c:945
- sys/dev/disk/dm/crypt/dm_target_crypt.c:670
- sys/dev/disk/dm/crypt/dm_target_crypt.c:959
Detail
Exploit chain
local user issues concurrent IO (two dd/writers) to a dm-crypt volume whose session uses the software XTS path (non-AES-NI CPU, VM without AESNI, or hw.aesni_disable=1) -> dmtc_bio_encdec on two CPUs share priv->crypto_session -> torn ctx->tweak -> wrong ciphertext written to the backing device with successful write status -> silent persistent data corruption (integrity/availability).
Evidence (decisive lines)
race.log: 'RACE aes-xts : 984352 iterations, 983991 corrupted outputs -> CORRUPTION OBSERVED' + 'RACE aes-cbc : 3734502 iterations, 0 corrupted'; kernel_dm_crypt_demo.log: 'round 1..5: A=CORRUPT B=CORRUPT' with 'serial A OK / serial B OK' baseline; fix_run.log: 'RACE aes-xts : 3401362 iterations, 0 corrupted outputs' after fix.diff
PoC changes
PoC authored fresh for this finding (no seed): userspace build of the exact kernel cryptoapi/rijndael/twofish/serpent translation units (cryptoapi.c is dual-compilable by design), 4 racing pthreads on one XTS session vs single-threaded references computed on an uncontended session, plus a CBC control on a shared session; verified serial sanity first. Kernel demo uses stock dmsetup/vnconfig/dm_target_crypt.ko.
Verified recommended fix
Keep the XTS tweak in a stack-local buffer inside aes_xts_crypt/twofish_xts_crypt/serpent_xts_crypt (pass it as an out-parameter to *_xts_reinit) instead of the shared per-session struct field; see fix.diff.
Verdict
Software XTS in cryptoapi (aes/twofish/serpent) keeps the per-operation tweak in the SHARED heap session context (struct *_xts_ctx.tweak, cryptoapi.c:373/728/891) and rewrites/advances it during every crypt call (aes_xts_reinit cryptoapi.c:422-427, crypt_block_xts cryptoapi.c:217-236). cryptoapi has no locking and its only in-tree consumer (dm_target_crypt) drives one shared session from concurrent bio paths with no serialization (dm_target_crypt.c:670/898/959-983). Concurrent IO on a software-XTS dm-crypt volume therefore tears the tweak and silently persists wrong ciphertext. Proven twice on the guest: (1) userspace harness compiled from the byte-identical kernel translation units: 983,991 corrupted outputs / 984,352 iterations on a shared XTS session vs 0 / 3,734,502 on a shared CBC session (control proving it is the tweak, not general concurrency); (2) live stock kernel: kldload dm_target_crypt + vn-backed volume + dmsetup aes-xts-plain64 + hw.aesni_disable=1, serial write/readback byte-perfect, two concurrent dd writers -> 5/5 rounds both halves CORRUPT on readback. Persistent silent data corruption at rest; no memory-safety impact, no leak, no escalation.
No comments yet.