Jan 15, 2026·6 min read·71 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 uncontrolled resource consumption vulnerability exists in the Scala-based http4s-blaze-server package of the http4s/blaze library. The vulnerability allows remote, unauthenticated attackers to cause an Out of Memory Error (OOM) and JVM crash by streaming a continuous sequence of small or empty WebSocket continuation frames with the FIN bit set to 0. This bypasses typical payload size checks because of the JVM's per-object allocation overhead, leading to rapid heap exhaustion with minimal network bandwidth.
A critical path traversal vulnerability has been identified in the OpenList Go-based backend package. The vulnerability exists within the batch rename handler because the application does not validate the source filename parameter before constructing filesystems paths. This omission allows authenticated users to escape their designated directory and rename files in sibling paths.
OpenList version 4.2.3 and prior is vulnerable to an authorization bypass and metadata leakage. When configured with the Bleve search engine backend, OpenList fails to perform separator-aware path matching when validating tenant containment. This allows authenticated users to access sibling directories sharing similar name prefixes. Furthermore, the search backend returns unfiltered global result counts, leaking existence verification data of unauthorized files via side-channel analysis.
An authorization bypass vulnerability in OpenList version 4.2.3 and below allows authenticated users to read arbitrary files outside of their designated base directories due to an insecure path prefix check using Go's standard strings.HasPrefix function.
A security policy bypass vulnerability exists in the AWS API MCP Server (awslabs-aws-api-mcp-server) from version 0.2.13 through 1.3.46. When the server fails to load the read-only operations index during startup (due to transient network failures, file permission issues, or other exceptions), it logs a warning but continues running in an insecure, degraded state. Under this condition, the security policy engine fails open, silently skipping all subsequent security checks and consent prompts for the lifetime of the process. This permits unauthorized mutating AWS CLI commands to execute via indirect prompt injection attacks.
An incomplete escaping vulnerability in the npm package 'shescape' allows unauthenticated users to trigger dynamic shell expansions, absolute path disclosure, and command block break-outs on Unix and Windows systems.