
Full ML-KEM-1024 key recovery from a partial Fujisaki-Okamoto comparison in wolfSSL (CVE-2026-6330 NEON, CVE-2026-10097 AVX2)
CVE-2026-6330 (NEON) and CVE-2026-10097 (AVX2). Fixed in wolfSSL 5.9.2.
The full technical write-up is published as
Cryptology ePrint Archive, Paper 2026/1682
(local copy in paper/). See Citation below.
ML-KEM (Kyber, FIPS 203) resists chosen-ciphertext attacks because of one check inside decapsulation: the receiver re-encrypts the message it just decrypted and returns the real shared secret only if the result matches the input ciphertext exactly. Any mismatch triggers implicit rejection, and a pseudorandom value is returned instead. That check is the Fujisaki-Okamoto transform, the single gate separating the IND-CCA2 scheme from the malleable IND-CPA scheme underneath.
wolfSSL implemented that comparison in hand-written SIMD assembly, and on two backends it compared only part of the ciphertext. On ARM64 NEON it compared about half, which Nicholas Carlini (Wikipedia) found and reported (CVE-2026-6330). On x86-64 AVX2 it compared 1536 of the 1568 bytes in ML-KEM-1024 (CVE-2026-10097).
On either backend a tampered ciphertext is accepted as genuine, which was reported as a
weakening of IND-CCA2 security. This write-up shows the flaw goes further, on both
backends. The bytes the check skips leak the decapsulation's decryption noise, and that
noise is a linear function of the secret key, so the key can be recovered by
least-squares regression. It is not a
lattice problem, and it is not the classical key-mismatch attack: on the AVX2 backend the
flaw validates all of u, so the chosen-u ciphertexts those attacks require are rejected.
The decryption-noise regression instead recovers the key from the unchecked v-coefficients.
The full ML-KEM-1024 private key is recovered end-to-end against the live vulnerable binary
on both the AVX2 and NEON backends.
The attack needs a reused ML-KEM key: HPKE recipients, KEMTLS, or pinned and embedded keys. An ephemeral hybrid TLS 1.3 key share uses a fresh key per handshake and is not key-recoverable this way; there the flaw is only a distinguishing break. Both flaws are fixed and public, and this is a post-disclosure write-up.
| NEON (CVE-2026-6330) | AVX2 (CVE-2026-10097) | |
|---|---|---|
| Backend | ARM64 NEON | x86-64 AVX2 |
| Flaw | compares ~half the ciphertext | compares 1536 of 1568 bytes |
| Bug reported by | Nicholas Carlini (Anthropic) | 007bsd |
| CVE severity | Medium (CVSS 4.0 6.3, CWE-327) | High (CVSS 4.0 8.3, CWE-697) |
| Key recovery, model | full (~500 ct) | full (~1300 ct) |
| Key recovery, live binary | 98.5% @ 600 ct (QEMU-emulated) | 98.1% @ 350 ct (native) |
| Fix | PR #10192 | PR #10430 |
"live" is the demonstrated result on the real binary: NEON needs more ciphertexts (600 vs 350) because its per-coefficient measurement is far noisier (about 4x). "model" is a noise-free check that the regression recovers the full key (all 2048 coefficients, 100%); its ciphertext count is set by how many equations each ciphertext yields, not by noise, so it is not comparable to the live figure and does not reflect attack difficulty.
Both flaws were found independently in the same wolfSSL release window and fixed together
in 5.9.2 (see the wolfSSL
security vulnerabilities page for
both CVE entries). Carlini's NEON bug was documented as an IND-CCA2 weakening, and the AVX2 bug is
CVE'd as key recovery. The decryption-noise regression attack in this repository recovers
the key on both. Backend-specific detail is in
neon-cve-2026-6330/ and
avx2-cve-2026-10097/.
An incomplete comparison in wolfSSL's ML-KEM implicit-rejection check, present in two SIMD assembly backends. It compared fewer than all of the ciphertext bytes, so decapsulation accepts ciphertexts that a correct implementation rejects. The unchecked bytes turn decapsulation into an oracle for the decryption noise, and that is enough to recover the private key when the key is reused.
You are affected if you used wolfSSL with ML-KEM on a build with the vulnerable SIMD assembly: AVX2 (x86-64) in 5.7.0-5.9.1, or ARM64 NEON in 5.7.4-5.9.1. The key-recovery attack additionally needs the ML-KEM private key to be reused across decapsulations, as with HPKE recipients, KEMTLS, or a pinned or embedded key. An ephemeral hybrid TLS 1.3 key share uses a fresh key per handshake and is not key-recoverable; there the flaw is only a distinguishing break. The fix is in wolfSSL 5.9.2.
Recover the entire ML-KEM-1024 secret key on either backend, by sending crafted decapsulation queries and observing, for each, whether the real secret or a reject value comes back. Against the live binaries this recovered the key with roughly 10^5 to 10^6 decapsulation queries.
Take one ciphertext coefficient in the unchecked region. Its plaintext bit is decided by
rounding, m'_j = Compress_1(v_j - (s^T u)_j). Because those bytes are not compared,
sweeping the compressed value of v_j and watching the one decapsulation output that
changes locates a rounding boundary, and the boundary position measures the decryption
noise δ_j to a precision of about ±q/64. In terms of the secret (s, e) and the
encryption randomness the attacker chose:
δ_j = ( e^T y - s^T(e1 + c_u) + e2 + c_v )_j (verified exact; small; never wraps mod q)
That is a linear equation in the 2048-coefficient secret (s, e), with coefficients the
attacker knows. Each ciphertext gives one such equation per unchecked coefficient. Stacking
enough ciphertexts and solving the normal equations recovers the whole key, and the last
few coefficients are pinned by the public-key relation e = t - A·s ∈ CBD(η). It is
ordinary least squares, not a
lattice problem.
The two backends differ in how much they leak. NEON leaves about 50% of the ciphertext unchecked (125 coefficients in three bands), against about 2% for AVX2 (51 coefficients in one band). NEON still needs more ciphertexts, because its unchecked bytes are non-contiguous and its per-coefficient measurement is noisier, so leaking more does not make the recovery easier here.