RustCrypto cmov CVE-2026-50185
MEDIUMSeverity by source
AC:H reflects the specific narrowing-cast and non-zero-high-bit conditions required; C:L captures potential breach of constant-time guarantees in cryptographic output on aarch64 network-facing services.
Lifecycle Timeline
1DescriptionCVE.org
Summary
The aarch64 implementations of Cmov and CmovEq seem to assume that the high bits when loading a value of size smaller than a register into a register are zero-extended. However, this is not the case and these bits are unspecified. This can result in a left.cmovz(&right, condition) not moving right into left, even if condition == 0.
Details
The Rust reference for inline assembly states that: > If a value is of a smaller size than the register it is allocated in then the upper bits of that register will have an undefined value for inputs [..]. Reference
If the high bits [8..] of the selector loaded into a register in the Cmov implementation or the high bits [16..] of self or other for CmovEq (specifically the implementation for u16 and i16) are set, the inline asm compares will produce a different result than the Rust code expects based on the narrow types.
In other words, the following assert fails, even though condition as u8 is zero:
let condition: u32 = black_box(1 << 8);
let mut left = 1;
let right = 2;
left.cmovz(&right, condition as u8);
assert_eq!(left, right);Because the ninth bit is set in the original variable, this bit is also set when the truncated condition is loaded into the input register for the cmp, causing the csel to select the wrong value.
The problematic code is located in cmov/src/backends/aarch64.rs here for Cmov and here for CmovEq.
The following function:
#[unsafe(no_mangle)]
pub fn cmovz_wrong_output(left: &mut i32, right: i32, condition: u32) {
left.cmovz(&right, condition as u8);
}produces the assembly:
cmovz_wrong_output:
.cfi_startproc
ldr w8, [x0]
//APP
cmp w2, #0
csel w8, w1, w8, eq
//NO_APP
str w8, [x0]
retwhich compares the 32-bits of the condition value against 0, instead of the intended 8.
Similarly, the following function using cmoveq
#[unsafe(no_mangle)]
pub fn cmoveq_wrong_output(left: u32, right: u32, input: u8, output: &mut u8) {
(left as u16).cmoveq(&(right as u16), input, output);
}compiles to:
cmoveq_wrong_output:
.cfi_startproc
ldrb w8, [x3]
and w9, w2, #0xff
//APP
eor w10, w0, w1
cmp w10, #0
csel w8, w9, w8, eq
//NO_APP
strb w8, [x3]
retwhere 32 bits of left and right are compared instead of 16. The same happens for i16.
For CmovEq, it seems the u8/i8 impls are not affected, as they are calling u16::from in the implementation which causes the upper bits to be masked out.
PoC
The following two test cases fail on aarch64-unknown-linux-gnu when compiled with --release (the cmovz one even in debug) emulated with qemu.
> rustc --version
rustc 1.94.0 (4a4ef493e 2026-03-02)#[cfg(test)]
mod tests {
use core::hint::black_box;
use cmov::{Cmov, CmovEq};
#[test]
fn cmovz_wrong_output() {
// The black box is necessary here, as otherwise the compiler will
// provide a constant 0 to the csel
let condition: u32 = black_box(1 << 8);
let mut left = 1;
let right = 2;
// I added this debug_assert as a sanity check, but funnily it causes
// the wrong cmov behavior in debug as well (as opposed to only in release mode without the debug_assert)
debug_assert_eq!(0, condition as u8);
left.cmovz(&right, condition as u8);
assert_eq!(left, right);
}
#[test]
fn cmoveq_wrong_output() {
let input = 1;
let mut output = 0;
let left: u32 = black_box(1 << 16);
let right: u32 = black_box(1 << 17);
// asserting in release mode here would hide the bug, the debug_assert_eq is
// a sanity check that these values SHOULD be equal
debug_assert_eq!(left as u16, right as u16);
(left as u16).cmoveq(&(right as u16), input, &mut output);
assert_eq!(input, output);
}
}Impact
Under specific circumstances, this issue can cause Cmov/CmovEq to produce incorrect output on aarch64. However, whether this bug can actually manifest depends on the surrounding code that calls the relevant impls. In the PoC, a narrowing cast is required, which masks out the set bits from Rust's point of view, but which are then used in the inline assembly.
Additional Finding
PR #1299 fixed a different bug in the aarch64 backend but introduced a small error. The csel! macro expect a cmp expression as its first argument which is never used. The compare is always "cmp {0:w}, 0", even for csel64!, which intends to use cmp {0:x}, 0. Given the bug described above, this oversight actually reduces its impact slightly, as only the bits in position[8..32] can cause issues, even for those impls that use csel64!.
AnalysisAI
Incorrect conditional-move output in the RustCrypto cmov crate's aarch64 backend causes cryptographic constant-time operations to silently produce wrong results on aarch64 platforms. The inline assembly implementations of Cmov and CmovEq compare full 32-bit register contents instead of the intended narrowed type widths when evaluating conditions, because Rust's inline assembly specification explicitly leaves upper register bits undefined for inputs smaller than the register size. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Exploitation requires all of the following: (1) the target system must be running on an aarch64 architecture (ARM64 Linux servers, Apple Silicon, embedded aarch64 devices); (2) the calling application must perform a narrowing integer cast - specifically from a type wider than the target (e.g., `u32 as u8` for `Cmov`, or `u32 as u16` for `CmovEq`) - before passing the value to the affected function; and (3) the bits above the narrowed type's width in the original value must be non-zero at runtime (e.g., bits [8..] set for the `u8` condition, bits [16..] set for `u16` operands). … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | Real-world risk is conditional and highly context-dependent, primarily affecting cryptographic applications compiled and deployed on aarch64 targets. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An application using the `cmov` crate on an aarch64 server performs a narrowing cast from a wider integer (e.g., a `u32` with value `256`) to `u8` and passes the result as a condition to `cmovz`. Even though `256u32 as u8` equals zero in Rust semantics, the inline assembly `cmp w2, #0` evaluates the full 32-bit register value (`256 != 0`), causing the conditional move to be skipped. … |
| Remediation | The primary fix is to upgrade to the patched release of the `cmov` crate once published; the exact fixed version was not confirmed in the available advisory data, so check GHSA-3rjw-m598-pq24 (https://github.com/RustCrypto/utils/security/advisories/GHSA-3rjw-m598-pq24) directly for the confirmed release. … Detailed patch versions, workarounds, and compensating controls in full report. |
Threat intelligence, references, and detailed analysis are available after sign-in.
Same technique Information Disclosure
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-3rjw-m598-pq24