Описание
Cmov/CmovEq on aarch64 can produce wrong results if high-bits of registers are set
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:
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:
produces the assembly:
which compares the 32-bits of the condition value against 0, instead of the intended 8.
Similarly, the following function using cmoveq
compiles to:
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 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.
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!.
Пакеты
cmov
>= 0.1.1, < 0.5.4
0.5.4
Связанные уязвимости
RustCrypto CMOV provides conditional move CPU intrinsics which are guaranteed on major platforms to execute in constant-time and not be rewritten as branches by the compiler. From 0.1.1 until 0.5.4, the aarch64 implementations of Cmov and CmovEq in cmov/src/backends/aarch64.rs assume high bits are zero-extended when loading values smaller than a register, so set high bits such as [8..] in a Cmov selector or [16..] of self or other in the u16 and i16 CmovEq implementations can cause left.cmovz(&right, condition) to produce incorrect output. This issue is fixed in version 0.5.4.
RustCrypto CMOV provides conditional move CPU intrinsics which are guaranteed on major platforms to execute in constant-time and not be rewritten as branches by the compiler. From 0.1.1 until 0.5.4, the aarch64 implementations of Cmov and CmovEq in cmov/src/backends/aarch64.rs assume high bits are zero-extended when loading values smaller than a register, so set high bits such as [8..] in a Cmov selector or [16..] of self or other in the u16 and i16 CmovEq implementations can cause left.cmovz(&right, condition) to produce incorrect output. This issue is fixed in version 0.5.4.
RustCrypto CMOV provides conditional move CPU intrinsics which are gua ...