# How ML-KEM Breaks in Real Life: The Machine Leaked While the Math Held

> ML-KEM shipped with a machine-checked security proof, yet its reference code leaked secret keys through a division instruction. Why every break missed the math.

*Published: 2026-07-18*
*Canonical: https://paragmali.com/blog/how-ml-kem-breaks-in-real-life-the-machine-leaked-while-the-*
*© Parag Mali. All rights reserved.*

---
<TLDR>
ML-KEM (FIPS 203) is the first standardized post-quantum KEM, and as of publication its Module-LWE mathematics is unbroken -- the IND-CCA2 security of its Fujisaki-Okamoto construction is even machine-checked [@fvk], and the two headline quantum "breaks" both failed to withstand expert scrutiny -- Chen withdrew his own claim, and Luo's was refuted by named experts [@chen2024][@postquantum-luo]. Yet every real attack on it has recovered secret keys anyway: KyberSlash through a variable-time division instruction disclosed in December 2023 [@kyber-commit][@kyberslash-paper], Clangover through a compiler that reintroduced a secret branch [@clangover], and fault attacks like Carry Your Fault straight through the masking meant to stop them [@carry-fault]. The through-line is a law two decades old, first seen when AES was attacked at the implementation and never at the cipher [@bernstein2005]: a security proof constrains values, not execution, so the attacker's frontier moves outward -- instruction, compiler, construction, countermeasure -- and never has to reach the lattice. This article catalogs each documented break from its primary source and shows you how to choose an ML-KEM implementation that does not leak.
</TLDR>

## 1. A fortress breached at the doorknob

On 13 August 2024, the U.S. National Institute of Standards and Technology published FIPS 203 and gave the world its first standardized post-quantum key-encapsulation mechanism: ML-KEM [@fips203]. It arrived with something almost no cipher in history has had at birth -- a machine-checked mathematical proof that the construction is secure [@fvk]. Eight months earlier, on 1 December 2023, the maintainers of its reference code had quietly committed a patch to stop that same code from handing an attacker the full secret key [@kyber-commit]. Not through any weakness in the lattice mathematics. Through a single division instruction.

The proof was about the numbers. The attack was about the machine. This article is about the gap between them.

<Definition term="Key Encapsulation Mechanism (KEM)">
A KEM is a three-algorithm protocol -- key generation, encapsulation, and decapsulation -- for agreeing on a shared secret over an insecure channel. Encapsulation takes a public key and outputs a ciphertext plus a shared secret; decapsulation takes the ciphertext and the private key and recovers the same shared secret. ML-KEM is the KEM standardized in FIPS 203, derived from CRYSTALS-Kyber, and its security rests on the Module Learning With Errors problem [@fips203].
</Definition>

Here is the thesis this article defends: as of publication, every documented attack on ML-KEM is an implementation attack. Not one has touched the Module-LWE mathematics. The keys have been recovered through a division instruction, a compiler optimization, and a fault injected into a countermeasure -- never through the lattice. This is exactly the class of attack that its structural-mathematics sibling, "How ML-KEM and ML-DSA Would Break," deliberately sets aside to ask how the primitive itself might one day fall. That article studies the door; this one shows you that so far, every intruder has come through the doorknob.

<Aside label="What counts as a break here">
There is no documented in-the-wild exploitation of a production ML-KEM deployment as of publication. The "breaks" cataloged here are of three honest kinds: vulnerable code shipping in the reference implementation and downstream libraries; CVE-class advisories such as RUSTSEC-2023-0079 [@rustsec]; and researcher proof-of-concept key recovery on real hardware [@kyberslash-paper]. That is stronger for the thesis, not weaker. The implementation layer broke first, at deployment scale, before the primitive had finished rolling out -- which is precisely what the pattern predicts.
</Aside>

And it has finished rolling out into places that matter. Google's BoringSSL and the Go standard library's constant-time `crypto/mlkem` both ship ML-KEM today [@kyberslash-site][@go-mlkem], as do `filippo.io/mlkem768` and the vectorized AVX2 reference paths [@kyberslash-site]. The common deployment shape is a hybrid key exchange, `X25519MLKEM768`, that stays secure if either half holds [@tls-hybrid] -- a hedge, as we will see, against both a future math break and a present implementation bug. When we say the reference code was leaking keys, we mean the code from which production ML-KEM was built.

The proof was real. So where did the key go? To answer that we have to be precise about what the proof actually promises -- and what it does not.

## 2. The contract: what ML-KEM actually promises

A security proof is a legal contract, and like every contract it binds only what it names. Read ML-KEM's proof carefully and you find it makes a precise, narrow promise about what an adversary can learn from a public key and a set of ciphertexts. It says nothing at all about how many clock cycles decapsulation takes, what assembly the compiler emits, how the power rail fluctuates, or what happens when a cosmic ray flips a carry bit. Those are not loopholes in the contract. They are simply not in it.

ML-KEM began life as CRYSTALS-Kyber, presented at IEEE EuroS&P in 2018 as "a CCA-secure module-lattice-based KEM" [@kyber2018]. Six years of analysis and a NIST competition later, it became FIPS 203 with only modest changes [@fips203]. Its security rests on one hard problem.

<Definition term="Module Learning With Errors (Module-LWE)">
Module-LWE hides a short secret vector inside a system of noisy linear equations over a polynomial ring modulo a public prime $q = 3329$. Given many pairs $(\mathbf{a}, \mathbf{a}\cdot\mathbf{s} + e)$ with small error $e$, recovering the secret $\mathbf{s}$ is believed hard for both classical and quantum computers. ML-KEM uses a module of rank 2, 3, or 4 to produce its three security levels; the internals of why the problem is hard belong to the companion article on how the lattice itself might fall [@kyber2018][@fips203].
</Definition>

The module rank is the only structural dial, and it defines the three parameter sets.

| Parameter set | Module rank | FIPS 203 security category | Typical role |
|---------------|-------------|----------------------------|--------------|
| ML-KEM-512    | 2           | Category 1                 | Lightweight  |
| ML-KEM-768    | 3           | Category 3                 | Default / recommended |
| ML-KEM-1024   | 4           | Category 5                 | High assurance |

The modulus $q = 3329$ is a public constant baked into the standard.<Sidenote>Everyone knows the divisor. What is secret is the numerator the divisor acts on -- the coefficients of the decrypted polynomial. That asymmetry, a public modulus dividing a secret operand, is the entire seed of the first break in the catalog.</Sidenote> That distinction between the public and the secret operand is worth holding onto; it returns with a vengeance in Section 4.

Now picture the whole story as a set of concentric layers around that untouched mathematics.

<Mermaid caption="The four-layer frontier: the Module-LWE mathematics sits at the center, and each documented attack lands one layer further out without ever reaching back to it.">
flowchart LR
    M["Layer 0: Module-LWE mathematics (unbroken)"]
    I["Layer 1: the instruction (KyberSlash)"]
    C["Layer 2: the compiler (Clangover)"]
    F["Layer 3: the construction (FO plaintext-checking oracle)"]
    K["Layer 4: the countermeasures (Carry Your Fault)"]
    M --> I --> C --> F --> K
</Mermaid>

Why does an active adversary matter here? Because a KEM that only resisted passive eavesdroppers -- the property called [IND-CPA](/blog/secure-against-whom-the-security-definitions-every-protocol-/) -- would be useless for a reused key. Real deployments reuse a decapsulation key across many sessions, and an attacker who can submit chosen or malformed ciphertexts and watch the results needs a stronger guarantee.

<Definition term="IND-CCA2 (adaptive chosen-ciphertext security)">
IND-CCA2 is the gold-standard secrecy notion for a KEM: even an adversary who can submit ciphertexts of its choosing and observe how the system responds cannot distinguish the real shared secret from random. It is exactly the notion you need when one long-lived private key decapsulates attacker-supplied ciphertexts, which is how KEMs are used in practice [@fips203].
</Definition>

Kyber reaches IND-CCA2 from a weaker IND-CPA core using a classic construction.

<Definition term="Fujisaki-Okamoto (FO) transform">
The FO transform, introduced in 1999 [@fo1999] and given its modern implicit-rejection analysis in 2017 [@hhk2017], upgrades a passively secure encryption scheme to an actively secure KEM. On decapsulation, it decrypts the ciphertext to a candidate message, then re-encrypts that message and checks whether the result matches the ciphertext it received. A genuine ciphertext re-encrypts to itself; a tampered one does not.
</Definition>

What happens on a mismatch is the hinge of the entire article.

<Definition term="Implicit rejection">
When re-encryption does not match, ML-KEM does not return an error. It returns a pseudorandom shared secret $\bar{K} = J(z, c)$, derived by hashing a secret rejection seed $z$ together with the ciphertext $c$. To the caller, a rejected ciphertext is indistinguishable from an accepted one -- there is no decapsulation-failure signal to query. By construction, the FO transform gives an attacker no caller-visible oracle [@fips203].
</Definition>

<Mermaid caption="ML-KEM decapsulation: re-encrypt, compare, and on mismatch return a pseudorandom secret. The dashed truth is that the compare step must run on secret-derived data, which is where every documented leak lives.">
flowchart TD
    A["Ciphertext c arrives at decapsulation"]
    B["Decrypt c to a candidate message"]
    C["Re-encrypt the candidate to c-prime"]
    D&#123;"Re-encryption matches?"&#125;
    E["Return the true shared secret K"]
    F["Return pseudorandom K-bar equals J of z and c"]
    A --> B --> C --> D
    D -->|yes| E
    D -->|no| F
</Mermaid>

That construction is not just believed secure; in 2024 a team machine-checked the IND-CCA2 security and correctness of Kyber in EasyCrypt, tying the proof to a real implementation [@fvk]. So the baseline is as strong as cryptography gets: the mathematics is unbroken, and the reduction from it to the KEM has been verified by a computer.

<PullQuote>
A security proof is a contract about values; a side channel is a breach in execution.
</PullQuote>

And there is the first shift in understanding. The proof constrains what an adversary learns from the specified inputs and outputs -- the values. It is silent on cycles, on emitted assembly, on power traces, on a flipped carry bit -- the execution. The key can walk out through the machine while the proof stays perfectly true. The contract is airtight about values. But the contract is run by a machine, and the machine is where this story happens. Before we watch it leak, one piece of history tells us it was always going to.

## 3. The precedent: AES was attacked at the implementation, never at the cipher

In April 2005, a widely used, carefully written AES implementation was handing out its key. Not to a cryptanalyst who had found a weakness in the cipher, but to a stopwatch. Daniel J. Bernstein showed that the time AES took to encrypt depended on its secret key, because the standard software used secret-indexed table lookups whose cache behavior varied with the data [@bernstein2005]. The cipher was fine. The code was leaking. The same researcher would, eighteen years later, be among the first to flag the division timing in Kyber [@kyberslash-site].

<Definition term="Side-channel attack">
A side-channel attack recovers secrets not from the mathematical output of an algorithm but from physical or behavioral byproducts of running it: elapsed time, cache state, power consumption, electromagnetic emissions, or the effect of induced faults. The algorithm can be perfectly secure on paper and still leak through the machine executing it.
</Definition>

A year later, Osvik, Shamir, and Tromer turned Bernstein's observation into a general methodology, naming the Prime and Probe and Evict and Time techniques that let one process read another's cache footprint and recover an AES key from a few thousand encryptions [@ost2006]. [Cache-timing attacks](/blog/how-aes-breaks-in-real-life-the-attacks-that-never-touched-t/) became a standard tool, and "How AES Breaks in Real Life," the earlier article in this series, is largely their story.

Now the counterweight, and it is the whole point. In all the years since, the AES *mathematics* has barely moved. The best known single-key attack on the full cipher is the 2011 [biclique cryptanalysis](/blog/the-fortress-and-the-afterthought-how-aes-would-break-at-its/), which recovers an AES-128 key in about ${2}^{126.1}$ operations rather than the ${2}^{128}$ of brute force [@biclique2011]. That is a speedup of less than a factor of four against a number with thirty-nine digits -- a result its own authors framed as not threatening the practical use of AES.

Two decades of the world's best cryptanalysts, and the cipher's security margin shrank by two bits, while its implementations leaked keys to a stopwatch in an afternoon.

> **Note:** A strong new primitive is attacked at the implementation first, and often only ever there. The mathematics is the hardest thing to break and the last thing to fall; the code around it is where the keys actually walk out. AES lived this for twenty years. ML-KEM, as of publication, is living it from the moment it was born.

If that law holds, ML-KEM's first breaks should be implementation breaks -- and the theory behind them should already exist. It does. Years before FIPS 203, researchers had worked out exactly how to attack a CCA-secure lattice KEM through its physics. In 2019, Ravi and colleagues demonstrated generic side-channel key recovery against CCA-secure lattice-based PKE and KEMs, exploiting the decryption-failure structure of these schemes [@ravi2019]. In 2021, Ueno and colleagues published "Curse of Re-encryption," showing that the FO re-encryption step itself is a generic power and electromagnetic oracle across post-quantum KEMs [@ueno2021].<Sidenote>Bernstein bookends both halves of this series: the 2005 cache-timing attack that defined how AES breaks, and the 2023 division-timing flag that opened how ML-KEM breaks. The same person, the same lesson, eighteen years apart.</Sidenote>

So the blueprint for breaking ML-KEM's implementation was drawn before the standard was finished. If the pattern holds, the first real breaks should show up early, maybe before the ink on the standard is dry. They did. Here is the catalog.

## 4. The catalog: the frontier moves outward

Here is the pattern to watch as we walk the documented breaks: each one lands one layer further from the mathematics than the last. The first is a single machine instruction. The second is the compiler. The third is the construction that proves the whole scheme secure. The fourth is the countermeasure built to stop the third. Not one of them reaches the lattice.

<Mermaid caption="The documented breaks arrived in a tight cluster around standardization, marching outward from the instruction to the countermeasures.">
timeline
    title The frontier moves outward, December 2023 to 2025
    2023 : KyberSlash1 patched on 1 December, commit dda29cc : Bernstein flags it possibly exploitable on 15 December : KyberSlash2 disclosed on 30 December
    2024 : Clangover compiler-introduced branch : Carry Your Fault on masked Kyber : FIPS 203 standardized on 13 August
    2025 : KyberSlash paper at IACR TCHES, a CHES best paper
</Mermaid>

The whole break surface fits in one table, and every number in it comes from a primary source.

| Layer | Attack | Mechanism | Measured result | Defense |
|-------|--------|-----------|-----------------|---------|
| 1. Instruction | KyberSlash | Secret numerator divided by the public modulus compiles to a variable-time `DIV` [@kyberslash-site] | Full keys in minutes for KyberSlash2, a few hours for KyberSlash1 -- both on the Cortex-M4 and Raspberry Pi 2 (Cortex-A7) [@kyberslash-paper] | Reciprocal multiplication; never-divided and AVX2 paths |
| 2. Compiler | Clangover | Clang re-emits a secret-dependent branch from constant-time source [@clangover] | ML-KEM-512 key in 5-10 minutes on an i7-13700H [@clangover] | Verify the binary; verified assembly |
| 3. Construction | Plaintext-checking oracle | Any physical pass/fail leak in FO re-encrypt-and-compare [@ravi2019][@ueno2021] | Generic key recovery against a reused key | Constant-time plus masked comparison |
| 4. Countermeasure | Carry Your Fault | Fault in the A2B carry chain that masking itself added [@carry-fault] | Keys from arbitrary-order masked Kyber via EM fault on an STM32 [@carry-fault] | Fault detection and redundancy |

### Layer 1 -- the instruction: KyberSlash

Start with one line of reference C. In `poly_tomsg`, the routine that turns a decrypted polynomial back into message bits, the code computes `t = (((t << 1) + KYBER_Q/2)/KYBER_Q) & 1;` [@kyberslash-site]. It divides a *secret* numerator `t` by the *public* modulus `KYBER_Q`, which is 3329. On paper that is one arithmetic operation. On silicon it is a catastrophe.

<Definition term="Constant-time (data-oblivious execution)">
Code is constant-time, or data-oblivious, when its execution -- the instructions retired, the memory addresses touched, and the total cycle count -- does not depend on any secret value. No secret-dependent branches, no secret-indexed memory, no variable-latency instructions on secret operands. It is the baseline defense against timing side channels, and it is a property of the running binary, not of good intentions.
</Definition>

The trouble is that on many processors, integer division is not constant-time. On many CPU, compiler, and flag combinations, `/KYBER_Q` compiles to a hardware `DIV` whose cycle count depends on the numerator: on an AMD Zen 2 core the division slows by a cycle once the numerator reaches 8192; on the SiFive U74 RISC-V core the timing shifts at 2048, 4096, and 8192; `gcc -Os` emits a `DIV` where `gcc -O3` emits a constant-time multiplication; and on a Raspberry Pi 2, `gcc -Os` -- whose default ABI does not guarantee a hardware divide -- emits a call to a branchy software-division routine instead [@kyberslash-site]. Any one of these makes decapsulation time depend on the secret.

<PullQuote>
KYBER_Q is a public integer 3329, but t is secret ... the time taken by division depends on the inputs.
</PullQuote>

That quote is from the KyberSlash FAQ [@kyberslash-site], and it is the entire attack in one sentence. There was, it turns out, a second copy of the same bug: KyberSlash2 is the identical secret-dependent division in `poly_compress` and `polyvec_compress`, the compression functions called from `indcpa_enc` [@kyberslash-site]. Because decapsulation *also* runs encryption during FO re-encryption, both the decryption and the re-encryption paths leak [@pqcforum-kyberslash2]. The measured consequence, from the KyberSlash paper, is blunt: Kyber secret keys are "reliably recovered within minutes for KyberSlash2 and a few hours for KyberSlash1" on an Arm Cortex-M4 and a Raspberry Pi 2 with an Arm Cortex-A7 [@kyberslash-paper].

<Mermaid caption="The KyberSlash mechanism: a public modulus dividing a secret operand becomes a variable-time instruction, and the cycle count becomes an oracle.">
flowchart TD
    A["Secret numerator t from the decrypted polynomial"]
    B["Public modulus KYBER_Q, the constant 3329"]
    C["Source computes t divided by KYBER_Q"]
    D["Compiler emits a hardware DIV on many targets"]
    E["DIV latency depends on the operand"]
    F["Decapsulation time leaks bits of the secret"]
    G["Plaintext-checking oracle, then full key recovery"]
    A --> C
    B --> C
    C --> D --> E --> F --> G
</Mermaid>

The timeline matters, because it is the proof of the thesis. The reference implementation was patched in `pq-crystals/kyber` commit dda29cc on 1 December 2023, with the message "Updated poly_tomsg to prevent a compiler from using DIV," crediting Cryspen's Goutam Tamvada, Karthikeyan Bhargavan, and Franziskus Kiefer [@kyber-commit].<Sidenote>The leak was found not by an attacker but by a formal-verification team. Cryspen's engineers noticed the division while building a machine-checked Rust implementation of Kyber [@cryspen]. The people proving the code correct were the ones who saw it leak.</Sidenote> Bernstein announced it publicly as a "possibly exploitable" issue on 15 December 2023 [@pqcforum-bernstein]; Ravi and Kannwischer disclosed the compression-function copy, KyberSlash2, on 30 December 2023 [@pqcforum-kyberslash2]. This is eight months *before* FIPS 203 existed. The standard had a machine-checked proof and a leaking reference implementation at the same time.<Sidenote>KyberSlash carries a CVE-class advisory but no NVD number. The record is RUSTSEC-2023-0079 against the `pqc_kyber` crate, rated CVSS 7.4 HIGH, with no patched versions released upstream [@rustsec].</Sidenote>

You can see why the fix looks trivial: never divide by a public constant on a secret. Multiply by its reciprocal instead. The two compute the same value, but only one is data-oblivious.

<RunnableCode lang="js" title="Same decoded bit, two costs: secret division versus constant-time reciprocal multiplication (illustrative cycle model, not a hardware measurement)">{`
const q = 3329;

// Illustrative model of hardware DIV latency: on several real cores the
// cycle count creeps up as the (secret) dividend grows. Not a measurement.
function divCycles(secretNumerator) {
  return secretNumerator >= 8192 ? 18 : 17;
}

// Reciprocal multiplication: multiply by a precomputed constant, then
// shift. One fixed cost, whatever the operand.
function reciprocalCycles(_secretNumerator) {
  return 3;
}

// Both routines decode the same message bit; only the timing differs.
function decodeBit(t) {
  return (((t << 1) + (q >> 1)) / q) & 1;
}

const secrets = [1200, 6000, 9000, 15000];
const divCosts = secrets.map(divCycles);
const mulCosts = secrets.map(reciprocalCycles);

console.log("secret numerators :", secrets.join(", "));
console.log("DIV cycle model   :", divCosts.join(", "), " (varies with the secret)");
console.log("reciprocal model  :", mulCosts.join(", "), "    (constant)");
console.log("decoded bits match:", secrets.map(decodeBit).join(", "));
`}</RunnableCode>

The obvious fix, then, is to delete the division and write constant-time source. So people did. It was not enough.

### Layer 2 -- the compiler: Clangover

You wrote constant-time C. The compiler disagreed. This is the uncomfortable discovery behind Clangover, published by Antoon Purnal in 2024 [@clangover]. The reference implementation's `poly_frommsg` routine, which expands each message bit into a polynomial coefficient, was written to be branch-free by construction: no `if` on a secret, just arithmetic and masks. Inspect the C and it is constant-time.

Then compile it. Clang versions 15 through 18, under optimization levels `-Os`, `-O1`, and `-O2` or `-O3` with `-fno-vectorize`, recognize the branch-free idiom and "optimize" it back into a *secret-dependent conditional branch* in the emitted x86 assembly [@clangover]. The compiler, trying to be helpful, reintroduced exactly the leak the programmer had carefully removed. End-to-end decapsulation-timing measurements then recover the full ML-KEM-512 secret key in 5 to 10 minutes on an Intel Core i7-13700H compiled with Clang 16.0.6 [@clangover].

> **Key idea:** Constant-time source is not constant-time binary. A proof about C, or even a careful reading of C, does not bind the machine code an optimizing compiler emits. The property you need holds of the instructions that actually run, and the compiler stands between you and them.

The reference maintainers already knew the compiler was an adversary, even if the full extent was not yet clear. Recall the wording of the KyberSlash patch: not "fix a bug" but "prevent a compiler from using DIV" [@kyber-commit].<Sidenote>The commit message names the real opponent. The source did not contain a division instruction; it contained a division operator that the maintainers had to actively stop the compiler from turning into a `DIV`. The adversary was the toolchain, not the algorithm.</Sidenote> Clangover is the general form of that fear: the toolchain can undo any source-level constant-time property you think you have.

> **Note:** Every defense so far lived in the source language. KyberSlash was patched in source; the branch-free `poly_frommsg` was written in source. Clangover shows the guarantee you actually need lives one level down, in the binary -- and nothing about writing careful C forces the binary to inherit the property. This is the coverage gap that Section 6's strongest defenses exist to close.

One detail in the Clangover repository ties this layer to the next. Purnal describes his attack, in his own words, as "a plaintext-checking oracle attack a la Ravi et al. and Ueno et al." [@clangover] -- and so does the KyberSlash lineage. Both breaks call themselves the same thing, which is no coincidence: it is the tell that they are two instantiations of a single, deeper surface. Time to see it.

### Layer 3 -- the construction: the plaintext-checking oracle

The armor is the wound. The re-encrypt-and-compare step that *proves* ML-KEM secure against active attackers is the exact code that every documented break pries open. To see why, you have to understand what the attacks are actually querying.

<Definition term="Plaintext-checking (PC) oracle">
A plaintext-checking oracle answers one question about a decapsulation: did this ciphertext decrypt to a particular message, or not? Lattice KEMs have a small, secret-dependent decryption-failure event, so an attacker who crafts ciphertexts near that boundary and learns pass-or-fail for each one can, over many queries against a reused key, solve for the secret. The oracle need not be a protocol feature; any physical leak that separates the two cases will serve [@ravi2019][@ueno2021].
</Definition>

This is the surface Ravi and colleagues formalized in 2019, showing generic PC-oracle key recovery against CCA-secure lattice KEMs, and that Ueno and colleagues generalized in 2021 to the FO re-encryption step as a power and electromagnetic oracle across post-quantum schemes [@ravi2019][@ueno2021]. Both predate FIPS 203.

And now the earlier layers snap into focus: KyberSlash is this oracle instantiated at the *instruction* (division timing separates pass from fail), and Clangover is the same oracle instantiated at the *compiler* (a reintroduced branch separates pass from fail). Purnal names the lineage outright [@clangover].

<Mermaid caption="The plaintext-checking oracle loop: a physical pass-or-fail leak in re-encrypt-and-compare, queried thousands of times, reconstructs a reused secret key.">
flowchart TD
    A["Attacker crafts a ciphertext near a decryption-failure boundary"]
    B["Victim decapsulates with the reused secret key"]
    C["Re-encrypt-and-compare runs on secret-derived data"]
    D&#123;"Side channel reveals pass or fail?"&#125;
    E["Record one bit about the secret key"]
    F["Update the secret-key estimate"]
    G["Full secret key recovered"]
    A --> B --> C --> D
    D -->|leak observed| E --> F
    F -->|repeat with a new ciphertext| A
    F -->|enough bits gathered| G
</Mermaid>

Here is the point that has to be stated precisely, because it is the most common misreading of ML-KEM.

> **Note:** Implicit rejection means a re-encryption mismatch returns a pseudorandom shared secret, not an error [@fips203]. There is no caller-visible reject to query, so the FO transform hands an attacker no oracle by construction. Every real oracle in this article is a side-channel leak in the re-encryption and comparison -- timing, power, electromagnetic, or fault -- never a signal the protocol emits.

So the FO transform's mathematics never failed. It is machine-checked [@fvk]. What "failed" is subtler and more interesting: the construction that earns the security proof is precisely the construction that forces secret-dependent computation to run at decapsulation time. The proof guarantees the *values* leak nothing. It cannot guarantee the *execution* leaks nothing, because guaranteeing that was never the mathematician's job -- it is the implementer's. The armor and the wound are the same lines of code.

If timing and compiler leaks feed this oracle, the natural next move is to attack a different channel entirely: mask the implementation so that power and electromagnetic measurements reveal nothing either. That worked. Until the defense became the surface.

### Layer 4 -- the countermeasure: masking, and Carry Your Fault

The last layer out is the defense itself. If timing and compiler channels can be closed, the remaining physical threat is an attacker with an oscilloscope or an electromagnetic probe against a device in hand. The standard answer is masking.

<Definition term="Masking (and Arithmetic-to-Boolean conversion)">
Masking splits every secret into $t+1$ random shares whose combination recovers the secret, so that any $t$ probes of the circuit are statistically independent of it -- the guarantee of the Ishai-Sahai-Wagner probing model. Linear operations stay cheap per share; the expensive part in Kyber is the nonlinear Arithmetic-to-Boolean (A2B) conversion needed because the scheme mixes arithmetic modulo $q$ with bitwise operations. Masking is the primary defense against power and electromagnetic analysis [@masking-kyber].
</Definition>

Masking is not busywork or redundancy.<Sidenote>Constant-time execution closes the timing channel; masking closes the power and electromagnetic channel. They defend different physics, so a device facing a physical adversary needs both. Neither subsumes the other [@masking-kyber].</Sidenote> First- and higher-order masked implementations of Kyber are a mature line of work [@masking-kyber]. And this is where the frontier does something remarkable: it marches one layer further and turns the countermeasure into the target.

<Definition term="Fault-injection attack">
A fault-injection attack deliberately corrupts a computation -- with a voltage or clock glitch, a laser, or an electromagnetic pulse -- and infers secrets from how the wrong result differs from the right one. It is a different threat from passive measurement: the attacker actively perturbs the device rather than only observing it [@carry-fault].
</Definition>

Carry Your Fault, published by Kundu and colleagues in 2024, does exactly this [@carry-fault]. The A2B conversion that masking requires is built on an adder with a carry chain. Inject a fault into that carry chain -- the very component added to *enable* masking -- and its data-dependent propagation leaks information about the secret. Using belief propagation over the faulted values, the attack recovers keys from Kyber masked to *arbitrary order*, validated with electromagnetic fault injection on an STM32 microcontroller [@carry-fault]. Read that again: no masking order defeats it, because the attack feeds on a structure that higher-order masking only makes larger.

It is not an isolated result. Deep-learning side-channel analysis has defeated a masked *hardware* implementation of Kyber [@ji-dubrova], and the surveys of the field confirm that the entire documented corpus of attacks on these schemes is implementation-class -- [side channel and fault](/blog/correct-constant-time-and-still-owned-a-field-guide-to-side-/), not lattice [@sca-survey].

So count the layers. The instruction leaked (Layer 1). The compiler leaked (Layer 2). The construction defined the surface (Layer 3). The countermeasure for that surface became the surface (Layer 4). Four layers out from the lattice, and the mathematics has not moved a single bit. That is not luck. It is the shape of the problem -- and the shape points straight at the fix.

## 5. The breakthrough: constant-time or it is not secure

Step back and the four layers share one address. Every break lives in the gap between what the specification promises about values and what the machine does during execution. The response to that, the actual breakthrough, is not a new theorem. It is a change in what counts as "done."

> **Key idea:** The armor that proves CCA security is exactly the code that leaks. So side-channel resistance cannot be an afterthought bolted onto a correct implementation -- it is a property the implementation must be built and verified to have, at the level of the binary that ships. Constant-time or it is not secure.

For decades, constant-time execution was treated as a performance-adjacent nicety, a thing careful cryptographers did by hand and hoped the compiler respected. The ML-KEM breaks retire that attitude. The discipline has three prongs: treat data-obliviousness as a hard security requirement; move the guarantee into formally verified implementations; and build tooling that *checks* the property instead of trusting it.

That last prong is where the KyberSlash authors made their most durable contribution. Ordinary constant-time checkers had a blind spot. The lineage runs from Adam Langley's ctgrind in 2010 [@ctgrind], which repurposed Valgrind's uninitialized-memory tracking to flag secret-dependent branches, through TIMECOP's automated sweep of the SUPERCOP benchmark suite [@timecop], to dudect's black-box statistical timing test [@dudect] and the ct-verif formal verifier [@ctverif].

But as the TIMECOP authors themselves note, Valgrind "cannot spot cases where variable-time code is caused by variable-time CPU instructions" [@timecop] -- which is precisely what a `DIV` is. So the KyberSlash team patched Valgrind to flag variable-time *instructions* operating on secret data, and ran it across more than 1000 implementations in SUPERCOP [@kyberslash-paper]. The tool now catches the exact class of bug that produced it.

Why was the burden on implementers in the first place? Because the standard never claimed otherwise. As Cryspen puts it plainly:

<PullQuote>
The goal of the NIST specification is clarity and succinctness, not performance or even security concerns like side-channel resistance. -- Cryspen
</PullQuote>

Side-channel resistance is the implementer's responsibility [@cryspen]. The breakthrough is not pretending otherwise -- it is making that responsibility mechanically checkable, so a leak is caught in continuous integration instead of in a paper two years later. And checking is something you can do yourself. The dudect method needs no source access at all: measure two populations of decapsulation timings, one for a fixed input and one for random inputs, and apply Welch's t-test. A large t-statistic means the timing depends on the data.

<RunnableCode lang="js" title="A dudect-style timing-leak test via Welch's t-statistic (illustrative model; a real run measures actual decapsulations)">{`
// dudect-style leak test: compare two timing populations with Welch's t.
// The dudect threshold: |t| above 10 is strong evidence of a timing leak.
// A deterministic generator keeps the demo reproducible.
let seed = 424242;
function rand() {
  seed = (1103515245 * seed + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}

function welchT(a, b) {
  const mean = xs => xs.reduce((s, x) => s + x, 0) / xs.length;
  const vary = (xs, m) => xs.reduce((s, x) => s + (x - m) * (x - m), 0) / (xs.length - 1);
  const ma = mean(a), mb = mean(b);
  const va = vary(a, ma), vb = vary(b, mb);
  return (ma - mb) / Math.sqrt(va / a.length + vb / b.length);
}

function measure(n, leaks) {
  const fixedClass = [], randomClass = [];
  for (let i = 0; i < n; i++) {
    const noise = rand() * 2;
    // A leaking build spends extra cycles on the fixed-input class.
    fixedClass.push(100 + noise + (leaks ? 6 : 0));
    randomClass.push(100 + rand() * 2);
  }
  return welchT(fixedClass, randomClass);
}

console.log("leaking build     t =", measure(4000, true).toFixed(1), " -> leak, |t| well above 10");
console.log("constant-time build t =", measure(4000, false).toFixed(1), " -> no leak");
`}</RunnableCode>

Discipline is a goal; a shipped binary is a fact. So how do you actually build an ML-KEM that does not leak, today?

## 6. State of the art: how a correct ML-KEM is built today

Here is the good news the catalog has earned: every one of those breaks has a known, shipping answer, and not one of the answers required touching the mathematics. The state of the art is not a single silver bullet. It is a stack of defensive layers, and it is useful to read them from nearest the math outward.

**Division-free constant-time source.** The innermost fix is the one KyberSlash demanded: never divide by a public constant on a secret operand. Replace `/KYBER_Q` with a fixed-point reciprocal multiplication -- multiply by a precomputed $\lceil 2^k / q \rceil$ and shift right by $k$ -- which is constant-cycle and frequently *faster* than the `DIV` it replaces. Replace secret-dependent branches with bit-masks and conditional moves. This is now the default in maintained libraries: the Go standard library's `crypto/mlkem` [@go-mlkem], Google's BoringSSL, `filippo.io/mlkem768`, and the PQClean AVX2 paths all ship division-free constant-time ML-KEM [@kyberslash-site].<Sidenote>The vectorized AVX2 and NEON code paths never issued a scalar `DIV` on secret data in the first place, so a KyberSlash-immune implementation existed in parallel the entire time [@kyberslash-site]. The bug was in the portable reference, not in the fast paths people actually deployed on servers.</Sidenote>

**Formally verified implementations.** Constant-time source is necessary but, as Clangover proved, not sufficient. The stronger layer moves the guarantee into the binary. There are two mature routes. The first is verified assembly: the formosa-crypto libjade project, built with Jasmin and EasyCrypt, ships assembly "connected through computer-verified proofs to a cryptographic security notion," including constant-time proven of the emitted code itself [@libjade]. That closes the exact gap Clangover opened -- there is no optimizing compiler left to betray you -- though its latest release supports only the AMD64 architecture [@libjade].

The second route is verified Rust: Cryspen's libcrux uses the hax toolchain and F* to machine-check correctness, memory safety, and source-level secret-independence, reusing verified code from HACL* [@libcrux][@haclstar].

There is an important honesty in the libcrux documentation, and it is on-thesis. The repository states that "executables compiled from the code in this repository are not verified to be side-channel resistant, although we try to enforce that the source code is secret-independent" [@libcrux]. In other words, libcrux verifies the source, not yet the binary, and it is still pre-release.<Sidenote>The verified-Rust effort that produced libcrux is the same effort that surfaced KyberSlash [@cryspen]. The tooling built to prove the code correct is what found the leak first -- the strongest possible argument for verification as a discipline.</Sidenote>

**Hardware timing guarantees.** As a backstop beneath the software, modern processors offer modes that guarantee operand-independent timing for a listed subset of instructions: Arm's Data-Independent Timing (DIT) register [@arm-dit] and Intel's Data Operand Independent Timing Mode (DOITM) [@intel-doitm]. These push part of the constant-time burden from the programmer to the silicon.<MarginNote>Both are opt-in: the operating system or program must set the register or mode before the guarantee applies, and only the listed instructions are covered.</MarginNote> Section 7 returns to their limits.

**Binary verification tooling.** Wrapping the whole stack is the checking discipline from Section 5: dudect for black-box statistical timing tests [@dudect], TIMECOP for automated SUPERCOP-wide sweeps [@timecop], ct-verif for formal constant-time verification [@ctverif], and the KyberSlash Valgrind patch for variable-time instructions on secret data [@kyberslash-paper]. Run them against the artifact you ship.

<Mermaid caption="The defensive stack over the unbroken mathematics: each layer closes a different physical channel, and binary verification wraps all of them.">
flowchart TD
    M["Unbroken Module-LWE mathematics"]
    subgraph timing["Closes the timing channel"]
        T1["Division-free constant-time source"]
        T2["Verified assembly, proven at the binary"]
        T3["Hardware DIT and DOITM backstop"]
    end
    subgraph power["Closes the power and EM channel"]
        P1["Masking with t+1 shares"]
    end
    subgraph fault["Closes the fault channel"]
        FA["Redundancy and fault detection"]
    end
    M --> timing
    M --> power
    M --> fault
</Mermaid>

> **Note:** Verify the binary you ship, not the source you wrote. KyberSlash lived in a source-level division operator; Clangover lived in compiler output that no source review would catch. Every layer here is ultimately about making a guarantee that holds of the instructions that actually execute [@kyberslash-paper].

Notice what is absent from the entire stack: any change to Module-LWE, any new hardness assumption, any adjustment to the parameter sets. The mathematics was never the problem, so it is not part of the fix. But these layers are not free, and they do not all defend the same thing. Choosing among them is its own problem.

## 7. Competing approaches: what each defense actually buys

There is no single winner here, and pretending otherwise is how deployments end up with the wrong protection. Each layer answers a different threat model at a different cost with a different strength of guarantee. Lay them side by side and no column is all-yes.

| Defense | Channel closed | Guarantee | Cost | Platform scope |
|---------|----------------|-----------|------|----------------|
| Constant-time source | Timing | Self-asserted, source only | Near zero | Portable |
| Verified assembly (libjade) [@libjade] | Timing | Proven at the binary | Low | AMD64 only in latest release |
| Verified Rust (libcrux) [@libcrux] | Timing (source) | Machine-checked source, binary not yet claimed | Low | Portable, pre-release |
| Masking [@masking-kyber] | Power and EM | Probing model of order $t$ | Quadratic, $O(t^2)$ | Portable |
| Fault countermeasures [@carry-fault] | Fault | Detection and redundancy | Roughly 2x or more | Portable |
| Hardware DIT / DOITM [@arm-dit][@intel-doitm] | Timing | Operand-independent for listed instructions | Near zero | Specific microarchitectures |

Read across the rows and the trade-offs are stark. Constant-time source costs almost nothing but certifies nothing about the binary -- Clangover is the proof. Verified assembly gives the strongest timing guarantee anyone can offer, proven of the shipped code, but libjade's latest release is AMD64-only [@libjade], and most ML-KEM in the world runs on Arm phones and microcontrollers. Verified Rust is maintainable and machine-checks correctness and safety, but its authors do not yet claim binary-level constant-time [@libcrux].

The physical-defense rows trade differently. Masking is the *only* layer that stops power and electromagnetic differential analysis, but it costs quadratically in the protection order [@masking-kyber] and, as Carry Your Fault showed, does not stop faults at any order [@carry-fault]. Fault countermeasures add redundancy and roughly double the work. Hardware DIT and DOITM are nearly free but cover only listed instructions on specific microarchitectures [@arm-dit][@intel-doitm].

Which explains why different deployments make opposite choices, and are both right to.

<Aside label="Threat model first">
A server or browser attacker cannot put an oscilloscope on your power rail or fire an electromagnetic pulse at your CPU. For that threat model the whole risk is timing, so a division-free constant-time implementation plus binary scanning is sufficient, and general-purpose stacks like the Go standard library and BoringSSL skip masking entirely [@kyberslash-site]. A smartcard, hardware security module, or embedded device sitting in an adversary's hand faces power, electromagnetic, and fault attacks directly, so it must add masking and fault countermeasures on top [@masking-kyber][@carry-fault]. Same primitive, same proof, opposite implementations -- because the threat models are opposite.
</Aside>

The deepest tension in the table is between the two things you most want. Formal verification gives the strongest guarantee but is platform-scoped: the proof for x86-64 says nothing about the Arm build. Masking is essential against physical adversaries but is expensive and, alone, incomplete. No single implementation today is verified-constant-time at the binary, masked to high order, fault-resistant, and portable all at once. You assemble the stack from partial guarantees.

Stack them all and you still hit walls -- and some of those walls are provable. So the honest next question is: what can side channels *not* let you close?

## 8. Theoretical limits: what side channels can and cannot close

The limits in this story are strikingly asymmetric, and not one of them is a limit on the lattice.

Timing is the easy one. Secret-independent execution is achievable at essentially zero asymptotic cost, because ML-KEM's compression divide was never necessary in the first place -- reciprocal multiplication is just as fast and constant-cycle. The "lower bound" on timing safety is therefore not a performance penalty but a matter of discipline plus verification. You can close the timing channel provably and for free.

Masking is where the hard floors appear. Resisting an adversary who can probe $t$ intermediate values requires at least $t+1$ shares -- the Ishai-Sahai-Wagner probing-model bound -- and Kyber's nonlinear steps, the Arithmetic-to-Boolean conversions, scale as $\Theta(t^2)$ [@masking-kyber]. High-order masking is genuinely expensive, and no clever engineering buys it cheaply. Then comes the sharpest result of all, the one with the flavor of an impossibility theorem.

> **Note:** Carry Your Fault recovers keys from Kyber masked to arbitrary order [@carry-fault]. There is no protection order you can reach that also buys fault safety, because the attack feeds on the carry chain that masking itself introduces. Masking and fault resistance do not compose for free -- you must add fault countermeasures as a separate layer, and no amount of the first substitutes for the second.

Step back and the shape of the remaining danger becomes clear, and it is not what people expect.

> **Key idea:** The gap is coverage, not lattice hardness -- and the mathematics was never the problem. The unsolved risks are all gaps in what a guarantee covers: source versus binary (Clangover [@clangover]), platform versus platform (libjade proves AMD64, not your Arm build [@libjade]; DIT and DOITM cover only listed instructions), and channel versus channel (timing is not power is not fault). None of these gaps touches Module-LWE.

That last clause deserves the honesty the series is built on. The series thesis is that the mathematics *almost never* causes the break, and "almost never" is doing real work: in other episodes, deployment defects were sometimes structural in a deeper way. For ML-KEM, as of publication, the lattice has simply not been reached -- and the two times anyone reached for it are themselves the strongest evidence.

<Aside label="The two times someone reached for the lattice">
In April 2024, Yilei Chen circulated a claimed polynomial-time quantum algorithm for lattice problems. Within days, informal peer review found a flaw: Hongxun Wu and, independently, Thomas Vidick identified a bug in Step 9, and Chen updated the paper to state that the claim does not hold [@chen2024][@quantuminsider]. The author withdrew it himself. In May 2026, Ming-Xing Luo posted a preprint claiming a polynomial-time quantum attack on Module-LWE and ML-KEM [@luo2026]. It has not been peer-reviewed and the author has not withdrawn it -- but named experts identified fatal flaws: Daniel Apon showed the attack's output is provably independent of the secret key, and Thomas Espitau found its central reduction dimensionally wrong and a supporting result fabricated [@postquantum-luo]. It is a reach that did not land -- and a weaker one than Chen's, because Chen retracted while Luo's claim has simply been refuted. ML-KEM is not considered broken.
</Aside>

The genuine lattice question -- whether Module-LWE itself has an efficient attack -- is the subject of the companion article, "How ML-KEM and ML-DSA Would Break: Module-LWE, Lattice Reduction, and the Correlated-Failure Problem." As of publication it remains open, and open is not the same as broken.

So the frontier is parked at the countermeasure layer. Not because the mathematics is the next thing to fall, but because closing the last coverage gaps -- binary, platform, channel -- is genuinely unsolved. That is exactly where the research is.

## 9. Open problems: where the frontier is now

The unsolved problems in ML-KEM security are, almost without exception, engineering problems. That is itself the thesis, restated as a research agenda.

**One artifact, all three guarantees.** The clearest open problem is a single machine-checked implementation that is provably constant-time *and* masked *and* fault-resistant, portably, at production scale. Today you assemble those from different tools -- a verified-assembly timing proof here, a masked gadget there, a fault countermeasure bolted on -- and trust that the composition holds. Carry Your Fault is the standing warning that it may not: the pieces can each be correct and still leak at their seams [@carry-fault].

**The compiler as an unquantified adversary.** Clangover is a specific bug in specific Clang versions, but the phenomenon is general [@clangover]. There is no widely deployed guarantee that an optimizing compiler *preserves* a source-level constant-time property through arbitrary transformations. "Constant-time preservation" as a compiler discipline is an active research direction, not a solved feature you can switch on.

**Portable verification.** The strongest proofs are architecture-bound. Verified assembly covers AMD64; hardware timing modes cover listed instructions on particular cores. Yet the most-deployed ML-KEM in the world runs on Arm phones and Cortex-M microcontrollers -- exactly the targets KyberSlash and Carry Your Fault attacked on real silicon [@carry-fault]. Closing the platform gap between where the proofs are and where the code runs is unsolved.

**Hardware accelerators.** As ML-KEM moves into dedicated hardware, the side-channel surface moves with it. A masked *hardware* implementation of Kyber has already fallen to deep-learning side-channel analysis [@ji-dubrova], and the surveys treat hardware side channels as an open, active area rather than a closed one [@sca-survey].

**Migration and [captured traffic](/blog/q-day-has-not-happened-the-incident-already-has-harvest-now-/).** The most realistic exposure needs no wild exploit at all.

<Definition term="Harvest-now, decrypt-later">
An adversary records encrypted traffic today and stores it, betting on decrypting it in the future once cryptanalysis or a quantum computer catches up. Because captured ciphertext cannot be un-sent, any recording made now is a standing liability -- which is the entire motivation for migrating to post-quantum key exchange before a break exists [@cryspen].
</Definition>

<Aside label="Harvest now, decrypt later">
Here is the sharp version of why implementation leaks matter even with the mathematics intact. Traffic protected by a hybrid ML-KEM handshake is being recorded now. If the implementation carrying that handshake leaks the key through a side channel, the recorded session is exposed -- no future quantum computer required. A live timing or fault leak in a deployed TLS or SSH path is the realistic threat, and it sits entirely in execution, not in the lattice [@cryspen].
</Aside>

**The lattice itself.** The one open problem that *is* mathematical -- whether Module-LWE has an efficient attack -- is deliberately not this article's.<Sidenote>That question is the whole subject of the companion piece, "How ML-KEM and ML-DSA Would Break," which takes on how it breaks now, in the implementation, versus how it would break later, in the mathematics.</Sidenote> As of publication it is open, and it is handed to the sibling.

Research aside, you have to ship something on Monday. Here is the decision that survives all of the above.

## 10. What to actually do

The one sentence: pick an ML-KEM implementation that never divided on a secret or is formally verified, and verify the binary you actually ship. Everything else is refinement by threat model.

**General-purpose software, no local physical adversary.** If you run on servers, browsers, or backends, an attacker cannot probe your power rail, so timing is your whole threat. Use a division-free constant-time implementation from a maintained source -- the Go standard library's `crypto/mlkem` [@go-mlkem], Google's BoringSSL, `filippo.io/mlkem768`, or an AVX2/NEON path [@kyberslash-site] -- and scan the binary with a patched Valgrind or TIMECOP [@timecop] and dudect [@dudect] in continuous integration. You do not need masking or fault countermeasures for this threat model.

**Maximum timing assurance on x86-64.** If you want the strongest guarantee available, use verified assembly: libjade's constant-time property is proven of the emitted code, which is the only thing that closes the Clangover gap outright [@libjade]. Remember its latest release is AMD64-only.

**Maintainable high-assurance Rust.** libcrux gives machine-checked correctness and source-level secret-independence [@libcrux], with the caveat that it is pre-release and does not yet claim binary-level constant-time.

**Embedded, smartcard, or HSM with a physical adversary.** Stack the layers: division-free constant-time *and* masking (sized to your threat, remembering the quadratic cost) *and* fault detection (because arbitrary-order masking still falls to Carry Your Fault [@carry-fault]) *and* hardware DIT or DOITM where available.

The library map, at a glance:

| Status | Implementation | Why |
|--------|----------------|-----|
| Known-good | Go `crypto/mlkem` [@go-mlkem] | Constant-time, upstreamed from `filippo.io/mlkem768`, never divided |
| Known-good | BoringSSL, `filippo.io/mlkem768`, PQClean AVX2 [@kyberslash-site] | Never issued a secret-operand `DIV` |
| Known-good | formosa-crypto libjade [@libjade] | Verified-assembly constant-time on AMD64 |
| Known-good, pre-release | libcrux [@libcrux] | Verified Rust, source secret-independent |
| Patched reference | `pq-crystals/kyber` portable C [@kyber-commit] | Fixed December 2023; downstream forks may still lag |
| Known-bad | `pqc_kyber` Rust crate [@rustsec] | RUSTSEC-2023-0079, no patched versions; migrate off it |

> **Note:** The Rust `pqc_kyber` crate carries RUSTSEC-2023-0079 for KyberSlash, rated CVSS 7.4 HIGH, and upstream released no patched versions [@rustsec]. It remains installable today. If it is anywhere in your dependency tree, migrate to the `ml-kem` crate or the maintained `safe_pqc_kyber` fork [@rustsec]. A deprecated-but-still-live dependency is the whole series thesis in miniature: the mathematics is fine, and the artifact you pulled from the registry is not.

<Spoiler kind="solution" label="How to check whether the unpatched crate is in your dependency tree">
Run `cargo audit` in your project: it flags RUSTSEC-2023-0079 when `pqc_kyber` appears anywhere in the graph [@rustsec]. To find what pulls it in, run `cargo tree -i pqc_kyber`, then swap in the `ml-kem` crate or the `safe_pqc_kyber` fork and re-run the audit until it comes back clean.
</Spoiler>

Every pitfall in this article maps to a named incident and a concrete fix.

| Pitfall | Incident it recreates | Fix |
|---------|-----------------------|-----|
| Dividing a secret by `q` in compression or decode | KyberSlash [@kyberslash-paper] | Reciprocal multiply; use a never-divided or AVX2 path; scan with patched Valgrind |
| Trusting constant-time C through the compiler | Clangover [@clangover] | Verify the binary; prefer verified assembly |
| Masking without fault countermeasures | Carry Your Fault [@carry-fault] | Add fault detection and redundancy as a separate layer |
| Depending on an unpatched crate | RUSTSEC-2023-0079 [@rustsec] | Migrate to `ml-kem` or `safe_pqc_kyber` |

Finally, deploy hybrid.<Sidenote>The hybrid group `X25519MLKEM768` runs a classical elliptic-curve exchange and ML-KEM together. By construction the session stays secure as long as either component does [@tls-hybrid], so it hedges a future lattice break and a present implementation bug in the other component at the same time.</Sidenote> A hybrid handshake is the one move that protects you against both the break that has not happened to the math and the break that already happened to the code.

> **Note:** Ship a division-free or formally verified ML-KEM, verify the binary you actually deploy, add masking and fault countermeasures only when a physical adversary can touch the device, and deploy hybrid so you stay secure if either half holds.

Do that and you have closed every gap anyone has actually walked through. The questions that remain are the ones readers always ask.

## 11. Frequently asked questions

A few misconceptions come up every time this material is discussed. Here they are, answered against the primary sources.

<FAQ title="Frequently asked questions">
<FAQItem question="Is ML-KEM broken?">
No. As of publication its Module-LWE mathematics is intact and its IND-CCA2 security has been machine-checked [@fips203][@fvk]. Every documented break is an implementation or side-channel attack against specific code -- a division instruction, a compiler optimization, a fault in a countermeasure -- not a weakness in the primitive itself.
</FAQItem>
<FAQItem question="Did a quantum computer break Kyber?">
No. Two prominent claims reached for the lattice mathematics and both failed review. Yilei Chen's 2024 quantum algorithm was withdrawn after Hongxun Wu and Thomas Vidick found a bug in Step 9 [@chen2024]. A 2026 preprint by Ming-Xing Luo was refuted by named experts, who showed the attack's output is provably independent of the secret key [@luo2026][@postquantum-luo]. No quantum machine has broken ML-KEM, and none is known to exist that could.
</FAQItem>
<FAQItem question="Is KyberSlash a CVE?">
It has a CVE-class advisory but no NVD number. The record is RUSTSEC-2023-0079 against the `pqc_kyber` crate, rated CVSS 7.4 HIGH [@rustsec]. The disclosure and reference patches were December 2023 [@kyber-commit], and the full paper appeared at IACR TCHES in 2025 [@kyberslash-paper].
</FAQItem>
<FAQItem question="Does implicit rejection hand an attacker an oracle?">
Not by construction. A re-encryption mismatch returns a pseudorandom shared secret, not an error, so there is no caller-visible rejection to query [@fips203]. The oracle in every real attack is a side-channel leak in the re-encryption and comparison -- timing, power, electromagnetic, or fault -- never a signal the protocol emits.
</FAQItem>
<FAQItem question="Was any of this exploited in the wild?">
There is no documented in-the-wild exploitation of a production ML-KEM deployment as of publication. The breaks are vulnerable code shipping in libraries, CVE-class advisories, and researcher proof-of-concept key recovery on real hardware [@kyberslash-paper].
</FAQItem>
<FAQItem question="Is masking enough for a physical device?">
No. Masking stops power and electromagnetic analysis but not faults: Carry Your Fault recovers keys from masking of arbitrary order [@carry-fault], and masked hardware has fallen to deep-learning side-channel analysis. A device facing a physical adversary needs constant-time execution and masking and fault countermeasures together.
</FAQItem>
<FAQItem question="So is post-quantum crypto safe to deploy?">
Yes, with care. Pick an implementation that never divided on a secret or is formally verified, verify the binary you ship, add masking and fault countermeasures only for physical-adversary threat models, and deploy a hybrid group such as `X25519MLKEM768` so you stay secure if either half holds [@tls-hybrid].
</FAQItem>
</FAQ>

Count the layers one last time. The instruction leaked; the compiler leaked; the construction defined the surface; the countermeasure became the surface. Four steps outward from the lattice, and the Module-LWE mathematics never moved -- its security proof machine-checked, its two attempted quantum breaks both failed under expert scrutiny (Chen withdrew his claim; Luo's was refuted by named experts) [@fvk][@chen2024][@postquantum-luo]. The mathematics almost never caused the break. The deployment did: the reference division, the compiler's optimization, the unpatched crate, the fault through the mask. The frontier is still marching outward, and the day it finally reaches the lattice is the subject of the companion article, "How ML-KEM and ML-DSA Would Break." Until then, the same law that spared the AES cipher for two decades while its implementations leaked keys to a stopwatch now governs the first post-quantum KEM.

<PullQuote>
The machine leaked while the math held. That is not a footnote to ML-KEM's security -- as of publication, it is the entire story of how it breaks.
</PullQuote>

<StudyGuide slug="how-ml-kem-breaks-in-real-life" keyTerms={[
  { term: "Key Encapsulation Mechanism (KEM)", definition: "A protocol for agreeing on a shared secret; ML-KEM is the FIPS 203 standard." },
  { term: "Module-LWE", definition: "The lattice problem ML-KEM's security rests on; unbroken as of publication." },
  { term: "Fujisaki-Okamoto transform", definition: "The re-encrypt-and-compare construction that makes ML-KEM CCA-secure." },
  { term: "Implicit rejection", definition: "On a re-encryption mismatch, decapsulation returns a pseudorandom secret, not an error." },
  { term: "KyberSlash", definition: "A December 2023 secret-dependent division-timing leak in reference Kyber code." },
  { term: "Clangover", definition: "A compiler-introduced secret branch that defeats source-level constant-time code." },
  { term: "Plaintext-checking oracle", definition: "A pass-or-fail side-channel leak that reconstructs a reused secret key." },
  { term: "Constant-time execution", definition: "Behavior independent of secret data; a property of the shipped binary, not the source." },
  { term: "Masking", definition: "Splitting secrets into t+1 shares to resist power and electromagnetic analysis." },
  { term: "Carry Your Fault", definition: "A fault on the A2B carry chain that defeats masking of arbitrary order." }
]} questions={[
  { q: "Why does a machine-checked security proof not prevent key recovery?", a: "The proof constrains values, not execution; keys leak through timing, compiler output, power, or faults." },
  { q: "What single surface underlies KyberSlash, Clangover, and the PC-oracle attacks?", a: "The Fujisaki-Okamoto re-encrypt-and-compare step, which must run on secret-derived data." },
  { q: "Does masking alone protect an embedded device?", a: "No; it stops power and EM analysis but not faults, so fault countermeasures are also required." },
  { q: "What deployment hedges both a math break and an implementation bug?", a: "A hybrid handshake such as X25519MLKEM768, which stays secure if either component holds." }
]} />
