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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3561 Β· 10 files
FileTypeDescriptionSize
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:

  1. *_xts_reinit() overwrites ctx->tweak (cryptoapi.c:422-427), and
  2. crypt_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 commands
  • guest_race.sh β€” race + LibreSSL X25519 cross-match attempt
  • fix.diff β€” the verified fix
  • cryptoapi.c.fixed β€” patched translation unit used for fix validation
  • fix_build.log, fix_run.log β€” patched-source build + race results
  • kernel_dm_crypt_demo.log β€” end-to-end in-kernel demonstration transcript
  • verification/ β€” the four-file correctness verification suite (rijndael/sha2/curve25519 KATs, LibreSSL cross-match, full logs, env, static checks)
VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (stock kernel; fix validated on byte-identical patched translation unit, not a rebuilt kernel)

Confirmed kernel references

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.