up:: Migration Architecture MOC
Compiler-Introduced Side Channels
A compiler-introduced side channel is leakage that exists in a compiled binary but not in the source code it was built from, created when the compiler lowers that source into machine instructions. The C function can be branch-free, table-free, and constant-time by every rule a reviewer applies, and the compiler can still emit split loads, extra stores, and register spills that expose secret-dependent structure on the memory bus. Reading the source tells you nothing about whether the shipped binary leaks, because the leaking instructions were never written by a person.
The short version:
- The leak lives between the source and the silicon. Constant-time C is a property of the source, and side-channel resistance is a property of the compiled binary on the target processor.
- Register spilling is the main mechanism. When a value will not stay in a register, the compiler writes it to the stack and reads it back, and on many embedded processors that memory traffic is physically observable.
- Optimization level changes the attack surface, and the faster build can be the leakier one. In the 2026 attack on HQC, compiling for speed rather than size spilled both halves of every secret word where the smaller build spilled one.
- Sparse secrets amplify it. When a secret is mostly zeros, telling a zero machine word from a nonzero one is nearly as good as reading bits, so code-based schemes with fixed-weight secrets carry more exposure than lattice ones.
- The control is binary-level review plus a pinned build chain. Compiler, version, flags, and target processor become part of the cryptographic configuration.
Think about a translator working from a carefully written speech. The author chose every word so nothing sensitive is revealed, and the translator, who is only trying to work efficiently, keeps a notepad. Whenever a phrase is too long to hold in mind, they jot it down, glance back at it, and jot it again. The speech is still clean. The notepad is now covered in the very material the author was careful about, and anyone watching the desk rather than listening to the speech learns what the author would not say. A compiler is that translator, the stack is the notepad, and register spilling is the jotting.
Can a compiler introduce a side channel that is not in the source code?
Yes, and this is the part of implementation security that source-level review structurally cannot reach. A compiler’s job is to produce a fast or small instruction sequence with the same observable input-output behavior as the source. Timing, power draw, and electromagnetic emission are not part of that observable behavior in any compiler’s model, so nothing in the optimizer’s correctness contract stops it from turning a leak-free source construct into a leaky instruction sequence.
Three transformations do most of the damage:
- Register spilling. A function with more live values than the processor has registers forces the compiler to park some of them on the stack and reload them. Each spill stores secret data and each reload loads it, and on many embedded cores load and store instructions leak the Hamming weight of whatever crosses the memory bus. The source contains no assignment at all at that point.
- Splitting wide operations. A 64-bit value on a 32-bit processor is not moved in one piece. The compiler emits two 32-bit transfers, and the two halves need not be handled identically, so the leakage granularity of the compiled code can be finer than the word size the programmer reasoned about.
- Instruction selection and strength reduction. Substituting one instruction sequence for another can replace a fixed-latency operation with a data-dependent one, which is the classical route to a timing channel.
A constant-time audit of C establishes that the programmer did not write a secret-dependent branch or a secret-indexed table lookup. It does not establish that the binary is free of secret-dependent memory traffic, because that traffic is chosen after the review by a tool the review never examined.
How does register spilling leak a secret?
Register spilling leaks because a spill converts a value that was living quietly inside the processor into a value that travels across the memory bus, where its Hamming weight is physically observable. A register-resident intermediate is comparatively silent. A stored one is announced. The number of times a secret crosses that bus, which the programmer never chose, is set by the compiler’s register allocator.
The mechanism sharpens when the spilling is uneven. Banegas, Smith, and Zahreddine analyzed the assembly GCC generates from the HQC reference implementation’s polynomial-multiplication routine at the size-optimized setting and found a 32-bit core transferring a 64-bit word as two register operations, with the compiler additionally spilling the low half to the stack under -Os, then additionally spilling the low half to the stack. The low 32 bits of every secret word therefore incur a load and a store, while the high 32 bits incur only a load, so the two halves of the same secret word leak at measurably different strengths. The practical result is that an attacker gets 32-bit resolution on a value the source treats as a single 64-bit word.
Source: G. Banegas, B. Smith, J. Zahreddine, “Exploiting Load/Store Leakage of Sparse Vectors for Key Recovery in HQC,” arXiv:2607.19109, submitted 21 July 2026. The abstract describes the leakage surface as one “in which the low and high 32-bit halves of each 64-bit word leak with different strengths, due to compiler-generated register spilling.”
Why does a higher optimization level make the leak worse?
Because more aggressive optimization means more register pressure, and more register pressure means more spills. The instinct that an optimization flag is a performance choice with no security dimension is backwards here.
The same authors compiled the same routine for speed rather than size and found a new store of the high 32-bit half appearing alongside the existing low-half spill. Where the size-optimized build leaked one half strongly and one weakly, the speed-optimized build spills both, which the authors describe as almost doubling the leakage of the secret value, and they name it as a case where speed optimizations increase side-channel leakage. The unevenness that made the first build interesting to analyze narrows, and the total exposed surface grows.
This is why an optimization flag belongs in the cryptographic configuration rather than in a build engineer’s discretion. Two builds of byte-identical source, differing in one flag, have different side-channel properties, and neither the source repository nor the algorithm name records which one shipped.
Why are sparse secrets the worst case?
Because when a secret is overwhelmingly zeros, the crude question a memory-bus observer can answer, is this machine word zero or not, is close to the question the attacker actually wants answered. For a secret drawn from a wide distribution, learning the approximate Hamming weight of a loaded word is weak information. For a secret that is a fixed-weight binary vector, it is close to positional information about where the few nonzero entries live.
HQC is the worked case. Its private vector is binary and deliberately sparse for security reasons, carrying 66 ones among 17,669 bits at the lowest parameter set. At 32-bit granularity an expected 88.7% of the machine words encoding it are identically zero, and at 64-bit granularity 78.7% are. A distinguisher that only separates zero words from nonzero ones therefore eliminates the overwhelming majority of coordinates in one pass, and every eliminated coordinate is a known-zero hint that shrinks the decoding problem an attacker has to solve.
Source: Banegas, Smith, Zahreddine, arXiv:2607.19109, abstract and contributions. The percentages and the code parameters are the paper’s own.
The contrast with lattice schemes is the useful generalization. In ML-KEM and its relatives, secret coefficients come from a distribution over small integers, so the Hamming weight of a loaded machine word says little about any individual coefficient, which is why load and store leakage has drawn comparatively little attention in that literature. The structural warning is aimed at schemes that keep fixed-weight secret vectors, and the authors flag checking whether the same leakage affects other post-quantum schemes relying on sparse secrets as open work.
What did the HQC attack actually achieve?
It carried compiler-introduced leakage all the way through to recovery of the long-term private key, which is what separates it from a leakage observation. The chain has three links.
- A zero-word distinguisher. The authors profile the target by running the multiplication routine on an all-zero vector to build baseline templates, then apply a statistical test at each loop iteration against the real execution. A positive result marks that word nonzero. Because the loop is deterministic and its iterations map to positions in the secret, every word is classified in parallel from one batch of measurements.
- Measurements, counted. On an STM32F446RE board running a Cortex-M4 at 30 MHz, the stronger low-half channel separates in approximately 500 electromagnetic traces and the weaker high-half channel in approximately 5,000, so roughly 5,000 traces is the worst case for full classification. The count is the same across all three HQC parameter sets, because all three call the same multiplication routine.
- Hints into decoding. The recovered zero positions are exactly the known-error-free-position hints that decoding-with-hints techniques consume. At 32-bit granularity, key recovery at the lowest parameter set falls to approximately 2^46 bit operations, which the authors’ implementation solves in about 40 seconds on a 48-core machine. At 64-bit granularity the same measurements yield approximately 2^60 operations, which they characterize as several days on the same hardware.
The measured chain, at HQC’s lowest parameter set:
| What was measured | Value | Scope of the figure |
|---|---|---|
| Zero machine words of the secret, 32-bit granularity | 88.7% expected | Lowest parameter set |
| Zero machine words of the secret, 64-bit granularity | 78.7% expected | Lowest parameter set |
| Traces, stronger low-half channel | ≈ 500 | STM32F446RE, Cortex-M4 at 30 MHz |
| Traces, weaker high-half channel | ≈ 5,000 | Worst case for classifying every word |
| Key recovery from 32-bit hints | ≈ 2^46 bit operations, about 40 seconds on 48 cores | Lowest parameter set |
| Key recovery from 64-bit hints | ≈ 2^60 bit operations, described as several days | Lowest parameter set |
| Trace count across parameter sets | Identical for all three | All three share the multiplication routine |
Source: Banegas, Smith, Zahreddine, arXiv:2607.19109. Abstract, verified against the primary record: “at 32-bit granularity an expected 88.7% of the machine words of y are zero for HQC-1, cutting the decoding to ≈ 2^46 bit operations,” and “approximately 500 traces for the stronger low-half channel and 5,000 for the weaker high-half channel.”
The load-bearing detail sits between those last two numbers. The same measurements feed both cases, and the difference between an impractical decoding and a routine one is entirely the 32-bit granularity the compiler created by splitting the word. The compiler’s choice, rather than the quality of the attacker’s measurements, is what makes the attack practical.
How do you defend against it?
The defenses split into fixing the code and controlling the build chain, and only the second generalizes past any one attack.
- Remove the property the leak depends on. For HQC specifically, the authors propose never storing the secret in plain sparse form: sample a fresh random value at each decapsulation, store the secret combined with it, and compute the product in two parts. Both operands are then indistinguishable from random, so the zero-word rate the attack relies on disappears. The stated cost is two full-length polynomial multiplications instead of one, roughly doubling the bottleneck operation.
- Represent the secret where it is not sparse. Their second proposal stores the secret in a transform domain, since the transform of a very sparse vector is necessarily dense. The authors are explicit that this is unfinished, and that a complete implementation and security evaluation remain open, so treat it as a research direction rather than an available control.
- Review the compiler output, not the source. Capture the assembly for key generation and decapsulation and look for split loads and stores on secret data, register spills of secret words, and differences in leakage granularity across optimization levels. This is the only one of these that transfers to the next scheme and the next processor.
- Pin and monitor the build chain. Supported compilers, versions, optimization profiles, and target processors become part of the validated configuration, and changing any of them triggers re-evaluation, because the leakage properties were measured for one combination and hold for one combination.
Masking in the general sense is the same instinct as the first defense and inherits the same caution: a masked design whose shares get spilled to the stack has moved the problem rather than solved it, so the binary-level check applies to the countermeasure as much as to the original code.
Does FIPS 140-3 validation cover this?
Not reliably, and treating a validation certificate as the answer here is worth naming in vendor conversations. FIPS 140-3 draws its non-invasive-attack testing from an international standard that specifies generic leakage tests against example attack paths, and that testing is not mandatory at the lower assurance levels at all.
The mismatch is one of specificity. This attack is a targeted electromagnetic analysis of one routine’s compiler-generated load and store sequence, on one microcontroller, with a leakage model built for the occasion. A generic conformance test run against a module implementing many algorithms is not aimed at that routine, its split loads, or its register spills. Published critiques of the non-invasive testing standard make the general version of this point, arguing its prescribed tests can miss specific leakage classes, and Banegas and colleagues cite that critique in exactly that role.
The usable formulation is that a validation certificate establishes the module passed a defined battery of generic leakage tests, and says nothing about whether anyone examined the compiled binary for the specific scheme in question.
What is the status of the affected code?
The paper targets the HQC reference implementation, and states the authors analyzed reference code because no side-channel-resistant HQC implementation is publicly available. It records that the latest version at the time of writing left all three of the analyzed functions unchanged. The reading is that the reference code remained unprotected against this attack as of publication.
Two boundaries govern how far that statement travels. The authors did not survey deployed HQC products, so there is no basis for a claim about what any particular vendor ships. And they evaluated one processor, one compiler version, and specific optimization flags, naming broader evaluation as future work, so the defensible claim is that this class of leak is plausible on a given platform and needs to be checked there, never that this exact leak exists on it. See PQC Implementation Pitfalls for why “we use the reference implementation” describes a risk rather than retiring one.
Common misconceptions
- “Our C is constant-time, so we’re safe.” Constant-time C is a source property. The compiler chooses the loads, stores, and spills a physical attacker actually measures, and it makes those choices after the review. The HQC case is an attack on a routine that is constant-time at the source level.
- “Compiling for speed is a performance decision.” It’s also a security decision. Built for speed, the HQC routine spills both halves of each secret word instead of one, which the authors describe as almost doubling the leakage.
- “Masking solves it.” Masking removes the correlation the attack exploits when it’s applied correctly and the shares stay protected, and a masked implementation whose shares are spilled to the stack has relocated the leak rather than closed it. The countermeasure needs the same binary-level verification as the original code.
- “This is an HQC problem.” HQC is the demonstrated case because its secret is extremely sparse, and the mechanism, a compiler creating memory traffic the source never specified, is scheme-independent. Any implementation manipulating structured secrets on a constrained processor deserves the same check.
- “The module is FIPS 140-3 validated, so side channels are covered.” Non-invasive-attack testing is generic, isn’t required at the lower assurance levels, and isn’t designed to hunt for a specific routine’s compiler-generated spill pattern.
Questions people ask
What is a compiler-introduced side channel? Leakage created by the compiler’s translation of source code into machine instructions rather than by the algorithm or by anything a programmer wrote. Split loads, extra stores, and register spills all move secret data across the memory bus at points the source never specifies, and on many embedded processors that traffic is physically observable.
How is this different from a normal constant-time bug? A normal constant-time bug is something a developer wrote, like a secret-dependent branch or a division whose timing varies, so a source review can find it. This class is invisible in the source, because the leaking instructions appear only after compilation, which means the review has to run against the binary.
Which algorithms are most exposed? Schemes whose secrets are sparse fixed-weight vectors, because the zero-versus-nonzero question a memory-bus observer can answer is close to the positional question the attacker wants answered. HQC is the measured case. Lattice schemes are structurally less exposed to this particular mechanism, since their secret coefficients aren’t overwhelmingly zero.
How many measurements does the HQC attack need? Approximately 500 for the stronger channel and approximately 5,000 for the weaker one on an STM32F446RE, with roughly 5,000 as the worst case for classifying every word. The count is the same for all three HQC parameter sets, since they share the multiplication routine.
Does the attack work against a server? The measured attack doesn’t, since it needs a near-field electromagnetic probe on the chip package. The transferable concern for server-side code is the general one, that a compiler can create secret-dependent memory traffic the source doesn’t contain, which other channels may reach. The paper doesn’t test that, and extending its result to servers would be extrapolation rather than evidence.
What should we require from a vendor? Evidence that someone reviewed the compiled binary on the target hardware for load and store leakage of secret data and for compiler-introduced spills, plus a stated and pinned compiler, version, and optimization profile. An algorithm name and a validation certificate answer a different question. See PQC Procurement and RFP Language.
Sources
- G. Banegas, B. Smith, J. Zahreddine, “Exploiting Load/Store Leakage of Sparse Vectors for Key Recovery in HQC,” arXiv:2607.19109, submitted 21 July 2026. Preprint, not peer-reviewed; all figures on this page are stated in its abstract and were verified against the primary arXiv record on 29 July 2026. One author is affiliated with a commercial side-channel-analysis firm, disclosed here as context.
- NIST, FIPS 140-3, Security Requirements for Cryptographic Modules, for the validation regime and its non-invasive-attack testing provisions.
- NIST, FIPS 203, Module-Lattice-Based Key-Encapsulation Mechanism Standard, §3.3, for the principle that conformance to a standard does not by itself produce a secure implementation.
Related notes
- Side-Channel and Fault Attacks on PQC: the post-quantum implementation-attack surface this mechanism sits inside.
- Side-Channel Analysis, Differential Power Analysis: the general toolkit and the statistics behind it.
- PQC Implementation Pitfalls: the neighboring class of bugs, and the reference-implementation pitfall this attack grounds.
- HQC (Hamming Quasi-Cyclic): the scheme measured, and where its parameter-set figures live.
- Constrained-Device PQC: why 32-bit processors split 64-bit secrets in the first place.
- Masking (Cryptographic Countermeasure): the countermeasure family, and why it needs the same binary-level check.
- FIPS 140-3: the validation regime that doesn’t settle this question.
- Testing as the Control Surface: where binary-level side-channel review belongs in an assurance program.
Everything here is the map, given freely. When your team needs to know whether the post-quantum implementations it’s about to ship were ever examined below the source level, that’s the work I do.
Last verified 2026-07-29 · Updated 2026-08-25 · Maintained by Addie LaMarr, LaMarr Labs.