Conversation
…t_eq_bytes and friends Survey of other libraries (from their current upstream sources): libsodium's `sodium_memcmp` uses a `volatile` accumulator and volatile input pointers, and subtle's `Choice::from` is an `#[inline(never)]` `read_volatile`, so both place the barrier inside the loop per byte. `constant_time_eq` and RustCrypto's `cmov` chunk into machine words and apply an inline-asm barrier (or `cmov`/`csel`) per word, which is the shape adopted here. Graviola writes the whole byte loop in inline asm on x86_64 and aarch64. OpenSSL's `CRYPTO_memcmp` reads through volatile pointers only, and BoringSSL's uses no barrier at all. Assembly was inspected by Claude from release builds on x86_64, i686, thumbv7em (Cortex-M4), riscv32imac, wasm32, msp430 (16-bit) and avr-none (8-bit): in every case the loop is load, xor, or, one native-width store to a stack slot and one reload, with a byte-wise tail, no `bcmp`/`memcmp` call and no data-dependent branch, and the final `== 0` tests the volatile-loaded value. Integration tests now sweep every length from 0 to 40 with a single bit flipped at every position, covering the word and tail paths on both sides of every boundary for 2, 4 and 8-byte words. Co-Authored-By: Claude Fable 5
d3ebc7c to
e89d8fd
Compare
|
Analysis courtesy of Opus 5. Some of these errors are extremely odd if this PR was generated using Fable - the LLMs do lend more weight to comments/text than code by default which could explain the mutant issue, but I'm not sure about the rest. 1. A killable mutant survives:
|
|
|
||
| for i in 0..LEN { | ||
| out[i] = core::hint::black_box(a[i] & mask) | core::hint::black_box(b[i] & !mask); | ||
| out[i] = (a[i] & mask) | (b[i] & !mask); |
There was a problem hiding this comment.
This looks like select().
There was a problem hiding this comment.
It does look like the same core code as Condition.select().
I guess you're suggesting to use Condition<>.select() here rather than duplicate?
| // read back is initialised and nothing is leaked or double-dropped. | ||
| unsafe { | ||
| core::ptr::write_volatile(&mut mask, mask); | ||
| mask = core::ptr::read_volatile(&mask); |
There was a problem hiding this comment.
That seems too much for my taste.
OpenSSL defines select()
static ossl_inline unsigned int constant_time_select(unsigned int mask,
unsigned int a,
unsigned int b)
{
return (value_barrier(mask) & a) | (value_barrier(~mask) & b);
}where the value_barrier() is defined:
static ossl_inline unsigned int value_barrier(unsigned int a)
{
#if !defined(OPENSSL_NO_ASM) && defined(__GNUC__)
unsigned int r;
__asm__("" : "=r"(r) : "0"(a));
#else
volatile unsigned int r = a;
#endif
return r;
}Its empty inline assembly uses tied input/output operands to hide the mask’s value from the optimizer.
If asm block is not an option, then core::ptr::read_volatile should be enough.
I suggest implementing a value_barrier helper.
Survey of other libraries (from their current upstream sources): libsodium's
sodium_memcmpuses avolatileaccumulator and volatile input pointers, and subtle'sChoice::fromis an#[inline(never)]read_volatile, so both place the barrier inside the loop per byte.constant_time_eqand RustCrypto'scmovchunk into machine words and apply an inline-asm barrier (orcmov/csel) per word, which is the shape adopted here. Graviola writes the whole byte loop in inline asm on x86_64 and aarch64. OpenSSL'sCRYPTO_memcmpreads through volatile pointers only, and BoringSSL's uses no barrier at all.Assembly was inspected by Claude from release builds on x86_64, i686, thumbv7em (Cortex-M4), riscv32imac, wasm32, msp430 (16-bit) and avr-none (8-bit): in every case the loop is load, xor, or, one native-width store to a stack slot and one reload, with a byte-wise tail, no
bcmp/memcmpcall and no data-dependent branch, and the final== 0tests the volatile-loaded value.Integration tests now sweep every length from 0 to 40 with a single bit flipped at every position, covering the word and tail paths on both sides of every boundary for 2, 4 and 8-byte words.
Co-Authored-By: Claude Fable 5
Closes #128