GitHub Advisoryadvisory · crates.io
GHSA-3rjw-m598-pq24 Cmov/CmovEq on aarch64 can produce wrong results if high-bits of registers are set
MEDIUM
Official page ↗
- Aliases
- CVE-2026-50185
- Ecosystem
- crates.io
- Packages
- cmov
- Severity
- Moderate
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P
- Weaknesses
- CWE-758
- Reviewed
- 2026-07-02
Affected packages
• crates.io cmov: >= 0.1.1, < 0.5.4 → fixed in 0.5.4
Description
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:
```rust
let condition: u32 = black_box(1 rustc --version
rustc 1.94.0 (4a4ef493e 2026-03-02)
```
```rust
[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!`.
Related
Alias · CVE (1)
- CVE-2026-50185 RustCrypto CMOV provides conditional move CPU intrinsics which are guaranteed on major platforms to execute in constant-time and not be… vulnerability
Weakness · CWE (1)
- CWE-758 Reliance on Undefined, Unspecified, or Implementation-Defined Behavior weakness