Jan 15, 2026·6 min read·82 visits
The Rust compiler is usually your friend, but in this case, it was the mole. On specific 32-bit ARM chips (Cortex-M0), LLVM optimized the `cmov` crate's constant-time logic into a conditional branch (`bne`). This introduced a timing side-channel into foundational cryptography libraries, allowing attackers to recover private keys from embedded devices simply by watching how long the CPU takes to think.
A critical side-channel vulnerability in the Rust `cmov` crate where LLVM optimizations inadvertently introduced conditional branches into constant-time logic on ARM Cortex-M0 targets, exposing cryptographic secrets.
We trust Rust. We trust it to yell at us when we borrow variables wrong. We trust it to stop buffer overflows. And in the world of cryptography, we trust libraries like cmov to handle the one thing that memory safety doesn't cover: Constant-Time Execution.
cmov (Conditional Move) is a tiny, foundational crate in the RustCrypto ecosystem. Its job is simple: move data from A to B based on a condition, but take exactly the same amount of time regardless of whether the condition is true or false. This is the bedrock of side-channel resistance. If your crypto code runs faster when a key bit is 0 vs 1, you might as well tweet your private key.
But here's the kicker: The code was correct. The logic was sound. The developer did everything right. But on January 14, 2026, we found out that the compiler—our supposed ally—decided to get "clever." On specifically thumbv6m targets (like the ubiquitous Cortex M0), LLVM looked at our beautiful constant-time bit-twiddling and said, "You know what would be faster? A jump instruction." And just like that, the safety guarantee evaporated.
To understand the breakage, you have to look at how we emulate conditional moves on architectures that don't support them natively. The Cortex-M0 is a stripped-down processor; it lacks the CMOV instruction found in x86 or higher-end ARMs. So, cmov uses a bitwise trick.
The logic relies on a macro called bitnz!. It takes a value and turns it into a 1 if the value is non-zero, or 0 if it is zero, using purely bitwise math:
macro_rules! bitnz {
($value:expr, $bits:expr) => {
($value | $value.wrapping_neg()) >> ($bits - 1)
};
}This is a standard constant-time idiom. val | -val propagates the sign bit if the value is non-zero. Shifting it down gives you a clean boolean integer. No if statements, no jumps. Just math.
However, LLVM's Value Range Analysis is too smart for its own good. It looked at this macro and realized: "Hey, the output of this is always strictly 0 or 1." Since the target architecture (Thumb-v6M) makes bitwise math slightly expensive but branching cheap, the optimizer rewrote the assembly. It inserted a bne (Branch if Not Equal) instruction to skip the operation if the value was zero.
Congratulations, optimization pass: you saved one CPU cycle and destroyed the security model of the entire application.
Let's look at the crime scene. We can see exactly where the transformation happens by comparing the intended Rust logic against the generated Assembly.
The Vulnerable Rust Code:
// This is supposed to be constant time
a.cmovnz(&b, c);The Generated Assembly (Thumb-v6M):
cmp r2, #0 @ Check if the condition is zero
beq .LBB0_2 @ <--- THE FATAL FLAW: Branch if Equal
mov r0, r1 @ Perform the move only if condition met
.LBB0_2:
@ Continue executionDo you see the beq? That's a conditional branch. If the CPU takes the branch, it flushes the pipeline (or simply takes fewer cycles depending on the microarchitecture). If it doesn't take the branch, it executes the move.
The Execution Flow Difference:
CMP -> BEQ (Jump) -> Done. (Fast)CMP -> BEQ (No Jump) -> MOV -> Done. (Slower)This timing difference is measurable. If cmov is used to check a password or a cryptographic signature, an attacker can guess the key byte-by-byte simply by measuring how long the device takes to reject the guess.
Exploiting this on a Cortex-M0 is terrifyingly reliable because these chips are often simple, in-order execution pipelines with minimal noise.
The Scenario:
Imagine a smart door lock using RustCrypto to verify an HMAC-SHA256 signature. The comparison function uses cmov to accumulate differences between the user's input and the real signature in a variable res. At the end, it checks if res == 0.
The Attack:
0x00....beq instruction injected by LLVM, the loop handling the comparison will have slightly different timing characteristics depending on when the bytes mismatch.0x01..., 0x02....cmov (or lack thereof) to trigger a different branch path. The power trace spikes or the timing shifts by a few clock cycles.Because the jitter on these embedded devices is low, the signal-to-noise ratio is huge. You don't need a million requests; you might only need a few hundred.
The fix required a two-step approach: first, a slap on the wrist for the compiler, and second, a restraining order.
Phase 1: The Black Box (v0.4.4)
The immediate mitigation involved wrapping the calculation in core::hint::black_box. This intrinsic is essentially a way of telling LLVM, "This is a magical value. Do not analyze it. Do not optimize it. Just pass it through."
macro_rules! bitnz {
($value:expr, $bits:expr) => {
// "Don't look at me!"
black_box(($value | $value.wrapping_neg()) >> ($bits - 1))
};
}Phase 2: The Nuclear Option (v0.4.5)
While black_box works, it relies on compiler heuristics that technically could change. The developers of cmov decided not to take chances. In version 0.4.5, they replaced the Rust logic entirely with Inline Assembly for ARM targets.
By writing the assembly manually, the developers bypass the optimizer entirely. They explicitly write the logical AND/OR/NOT instructions, guaranteeing that no branch instructions can ever be inserted. It's the only way to be sure.
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
cmov RustCrypto | < 0.4.4 | 0.4.5 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-208 (Observable Timing Discrepancy) |
| Attack Vector | Network / Physical (Side-Channel) |
| CVSS v4.0 | 8.9 (High) |
| Architecture | ARM Thumb-v6M (32-bit) |
| Root Cause | LLVM Optimization (Value Range Analysis) |
| Impact | Key Extraction via Timing Analysis |
The product performs a calculation or other operation in a way that allows an attacker to gain information about the internal state or data by measuring the time it takes to execute.
An unauthenticated Denial of Service vulnerability exists in the s2n-quic library's CryptoStream reassembler due to a lack of buffer limits on out-of-order cryptographic frames. An attacker can transmit a crafted CRYPTO frame with an extremely high offset and nominal payload, forcing the receiver to execute unbounded memory allocations and causing service crashes.
A JNDI Injection and Deserialization Gadget vulnerability exists in mchange-commons-java prior to version 0.6.0. The com.mchange.v2.naming.JavaBeanObjectFactory component permits arbitrary class instantiation and setter invocation, allowing attackers to perform Server-Side Request Forgery (SSRF) and remote class loading.
SurrealDB versions supporting element-level SELECT permissions on arrays are vulnerable to a logical authorization bypass. Due to an index-shifting error during array filtration, restricted elements can skip permission checks and leak to unauthorized record users.
CVE-2026-12243 is a path traversal vulnerability in the Natural Language Toolkit (NLTK) version 3.9.4. The flaw exists because the input validation routine fails to account for percent-encoded directory traversal sequences like '..%2f' before passing them to urllib.request.url2pathname(), which decodes them into active traversal sequences.
CVE-2026-73654 is a high-severity prototype pollution vulnerability in Trigger.dev. The flaw occurs during the handling of run-metadata updates through the PUT /api/v1/runs/:runId/metadata endpoint. Because user-supplied keys are parsed directly by the @jsonhero/path library without sanitization, an authenticated attacker with low privileges can pollute the global Object.prototype. This causes database queries via Prisma ORM to fail validation and induces unhandled exceptions in the Prometheus metrics client, resulting in a process-wide denial of service.
CVE-2026-73559 is an uncontrolled resource consumption vulnerability in the vLLM engine, specifically within the /v1/completions API endpoint, allowing authenticated attackers to cause application-level denial of service via unbounded prompt arrays.