VDB
KO
MEDIUM

GHSA-3rjw-m598-pq24

Cmov/CmovEq on aarch64 can produce wrong results if high-bits of registers are set

Details

### 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](https://doc.rust-lang.org/reference/inline-assembly.html#r-asm.register-operands.smaller-value)

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 << 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](https://github.com/RustCrypto/utils/blob/dad5e3b9e66d929e86144fe7c8f25371892e35f3/cmov/src/backends/aarch64.rs#L4-L19) and [here for CmovEq](https://github.com/RustCrypto/utils/blob/dad5e3b9e66d929e86144fe7c8f25371892e35f3/cmov/src/backends/aarch64.rs#L60-L72).

The following function: ```rust #[unsafe(no_mangle)] pub fn cmovz_wrong_output(left: &mut i32, right: i32, condition: u32) { left.cmovz(&right, condition as u8); } ``` produces the assembly: ```asm cmovz_wrong_output: .cfi_startproc ldr w8, [x0] //APP cmp w2, #0 csel w8, w1, w8, eq //NO_APP str w8, [x0] ret ```

which compares the 32-bits of the condition value against 0, instead of the intended 8.

Similarly, the following function using `cmoveq` ```rust #[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: ```asm 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] ret ``` where 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](https://github.com/RustCrypto/utils/blob/dad5e3b9e66d929e86144fe7c8f25371892e35f3/cmov/src/lib.rs#L119) 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) ```

```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](https://github.com/RustCrypto/utils/pull/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!`.

Are you affected?

Enter the version of the package you're using.

Affected packages

crates.io / cmov
Introduced in: 0.1.1 Fixed in: 0.5.4

Upgrade cmov to 0.5.4 or newer (ecosystem crates.io).

References