Jan 15, 2026·6 min read·55 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 improper authentication vulnerability (CWE-287) exists in the legacy, deprecated Internet Key Exchange version 1 (IKEv1) key exchange protocol implementation in Check Point Security Gateways. The vulnerability is caused by a logic flow weakness during the certificate validation process for Remote Access VPN and Mobile Access (SSL VPN) connections. An unauthenticated remote attacker can exploit this weakness to bypass user authentication entirely, establishing a fully functional Remote Access VPN connection without a valid password.
GeoNode versions prior to 4.4.5 and 5.0.2 are vulnerable to Server-Side Request Forgery (SSRF) in the service registration endpoint. Authenticated attackers with low privileges can exploit insufficient input validation in the Web Map Service (WMS) registration module to force the application server to make outbound network queries to loopback addresses, private RFC1918 subnets, link-local scopes, and cloud metadata endpoints. This technical report details the mechanics of the vulnerability, the underlying architectural flaw, and how to effectively remediate and mitigate the associated security risks.
CVE-2022-0492 is a high-severity missing authorization vulnerability in the Linux kernel's Control Groups (cgroups) v1 implementation. The flaw resides within the cgroup_release_agent_write function in kernel/cgroup/cgroup-v1.c, where the kernel fails to validate if the process writing to the release_agent file possesses administrative capabilities in the initial user namespace. This allows a local attacker inside a container with root privileges (UID 0) to abuse user namespaces, mount a cgroups v1 directory, modify the release_agent parameter, and execute arbitrary commands on the host system as host root, effectively achieving a complete container escape.
NocoDB is subject to an insufficient session expiration vulnerability where OAuth access and refresh tokens are not invalidated or revoked during security-sensitive actions such as password changes, forgot-password requests, or password resets. This allows an attacker possessing an active OAuth token to maintain unauthorized persistence.
A vulnerability in the vantage6 federated learning framework allows unauthenticated remote attackers to gain administrative control of the server via hardcoded default credentials (root/root) when deployed under default configurations in versions 4.2.3 and below.
An improper access control vulnerability in the vantage6 node component allows concurrently running algorithm containers to read and modify sensitive input and output files of other tasks. The lack of strict workspace directory isolation exposes a significant attack surface in multi-tenant or federated environments where untrusted algorithms are executed.