diff --git a/ctutils/checkct/driver/src/driver.rs b/ctutils/checkct/driver/src/driver.rs index 14f2a590..ca2a809d 100644 --- a/ctutils/checkct/driver/src/driver.rs +++ b/ctutils/checkct/driver/src/driver.rs @@ -11,6 +11,7 @@ macro_rules! private_integer { }}; } +mod predicates; mod primitives; macro_rules! scalar_checks { diff --git a/ctutils/checkct/driver/src/driver/predicates.rs b/ctutils/checkct/driver/src/driver/predicates.rs new file mode 100644 index 00000000..16b25173 --- /dev/null +++ b/ctutils/checkct/driver/src/driver/predicates.rs @@ -0,0 +1,135 @@ +//! Comparison specialization and loop-carried predicates used by crypto-bigint. + +use super::*; + +macro_rules! predicate_checks { + ($module:ident, $ty:ty, $eq:ident, $le:ident, $lt:ident, $nz:ident, $select:ident, + $leading:ident, $trailing:ident) => { + mod $module { + use super::*; + + // Inline these operations into the driver: an artificial function boundary + // would hide the caller specialization which previously introduced branches. + fn private() -> $ty { + black_box(PublicRng.next_u32()); + private_integer!($ty) + } + + #[checkct] + pub fn lt_constant() { + black_box(Choice::$lt(2, private() & 63).to_u8()); + } + + #[checkct] + pub fn le_constant() { + black_box(Choice::$le(2, private() & 63).to_u8()); + } + + #[checkct] + pub fn eq_constant() { + black_box(Choice::$eq(1, private() & 63).to_u8()); + } + + #[checkct] + pub fn nz() { + black_box(Choice::$nz(private()).to_u8()); + } + + #[checkct] + pub fn min_constant() { + let value = private() & 63; + black_box(Choice::$lt(2, value).$select(value, 2)); + } + + #[checkct] + pub fn and_loop() { + let mut all_zero = Choice::TRUE; + for value in [private(), private(), private()] { + // A raw byte predicate also covers callers that do not use a + // comparison constructor, such as crypto-bigint's Limb helpers. + let bit = (value | value.wrapping_neg()) >> (<$ty>::BITS - 1); + let zero = Choice::from_u8_lsb(bit as u8).not(); + all_zero = all_zero.and(zero); + } + black_box(all_zero.to_u8()); + } + + fn limbs() -> [$ty; 16] { + let mut limbs = [0; 16]; + for limb in &mut limbs { + *limb = private(); + } + limbs + } + + #[checkct] + pub fn leading_zeros_choice_loop() { + black_box(leading_zeros(&limbs())); + } + + // Keep the whole limb scan intact, as in a crypto-bigint method. Each + // scalar zero count remains inlined into the loop. + #[inline(never)] + fn leading_zeros(limbs: &[$ty; 16]) -> u32 { + let mut count = 0; + let mut all_zero = Choice::TRUE; + let mut i = limbs.len(); + while i > 0 { + i -= 1; + let value = limbs[i]; + count += all_zero.select_u32(0, ctutils::primitives::$leading(value)); + // Match callers with their own scalar predicate helpers: do not + // rely on the comparison constructor's separate hardening here. + let bit = (value | value.wrapping_neg()) >> (<$ty>::BITS - 1); + all_zero = all_zero.and(Choice::from_u8_lsb(bit as u8).not()); + } + count + } + + #[checkct] + pub fn trailing_zeros_choice_loop() { + black_box(trailing_zeros(&limbs())); + } + + #[inline(never)] + fn trailing_zeros(limbs: &[$ty; 16]) -> u32 { + let mut count = 0; + let mut all_zero = Choice::TRUE; + let mut i = 0; + while i < limbs.len() { + let value = limbs[i]; + count += all_zero.select_u32(0, ctutils::primitives::$trailing(value)); + // Match callers with their own scalar predicate helpers: do not + // rely on the comparison constructor's separate hardening here. + let bit = (value | value.wrapping_neg()) >> (<$ty>::BITS - 1); + all_zero = all_zero.and(Choice::from_u8_lsb(bit as u8).not()); + i += 1; + } + count + } + } + }; +} + +predicate_checks!( + u32_ops, + u32, + from_u32_eq, + from_u32_le, + from_u32_lt, + from_u32_nz, + select_u32, + leading_zeros_u32, + trailing_zeros_u32 +); +predicate_checks!( + u64_ops, + u64, + from_u64_eq, + from_u64_le, + from_u64_lt, + from_u64_nz, + select_u64, + leading_zeros_u64, + trailing_zeros_u64 +); diff --git a/ctutils/src/choice.rs b/ctutils/src/choice.rs index e5e3f3c3..7b0be369 100644 --- a/ctutils/src/choice.rs +++ b/ctutils/src/choice.rs @@ -1,34 +1,10 @@ +use crate::primitives::{bitle, bitlt, bitnz}; use crate::{CtAssign, CtAssignSlice, CtEq, CtEqSlice, CtSelectUsingCtAssign}; use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; #[cfg(feature = "subtle")] use crate::CtSelect; -/// Bitwise less-than-or equal: returns `1` if `x <= y`, and otherwise returns `0`. -/// -/// See "Hacker's Delight" 2nd edition, section 2-12 (Comparison predicates) -macro_rules! bitle { - ($x:expr, $y:expr, $bits:expr) => { - (((!$x) | $y) & (($x ^ $y) | !($y.wrapping_sub($x)))) >> ($bits - 1) - }; -} - -/// Bitwise less-than: returns `1` if `x < y`, and otherwise returns `0`. -/// -/// See "Hacker's Delight" 2nd edition, section 2-12 (Comparison predicates) -macro_rules! bitlt { - ($x:expr, $y:expr, $bits:expr) => { - (((!$x) & $y) | (((!$x) | $y) & $x.wrapping_sub($y))) >> ($bits - 1) - }; -} - -/// Bitwise non-zero: returns `1` if `x != 0`, and otherwise returns `0`. -macro_rules! bitnz { - ($value:expr, $bits:expr) => { - ($value | $value.wrapping_neg()) >> ($bits - 1) - }; -} - /// Constant-time analogue of `bool` providing a "best effort" optimization barrier. /// /// This type attempts to hint to the compiler and its codegen backends that optimizations should @@ -56,7 +32,15 @@ impl Choice { #[inline] #[must_use] pub const fn and(self, rhs: Choice) -> Choice { - Self(self.0 & rhs.0) + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + { + // Keep loop-carried predicates from becoming a short-circuiting boolean. + Self(self.to_u8() & rhs.to_u8()) + } + #[cfg(not(all(target_arch = "arm", not(target_has_atomic = "ptr"))))] + { + Self(self.0 & rhs.0) + } } /// Apply an `or` conditional to the given [`Choice`]s. @@ -199,7 +183,7 @@ impl Choice { #[inline] #[must_use] pub const fn from_u32_le(x: u32, y: u32) -> Self { - Self::from_u32_lsb(bitle!(x, y, u32::BITS)) + crate::primitives::le_u32(x, y) } /// Initialize from the least significant bit of a `u32`. @@ -213,14 +197,14 @@ impl Choice { #[inline] #[must_use] pub const fn from_u32_lt(x: u32, y: u32) -> Self { - Self::from_u32_lsb(bitlt!(x, y, u32::BITS)) + crate::primitives::lt_u32(x, y) } /// Returns [`Choice::TRUE`] if `value != 0`, and [`Choice::FALSE`] otherwise. #[inline] #[must_use] pub const fn from_u32_nz(value: u32) -> Self { - Self::from_u32_lsb(bitnz!(value, u32::BITS)) + crate::primitives::nz_u32(value) } // u64 @@ -236,7 +220,7 @@ impl Choice { #[inline] #[must_use] pub const fn from_u64_le(x: u64, y: u64) -> Self { - Self::from_u64_lsb(bitle!(x, y, u64::BITS)) + crate::primitives::le_u64(x, y) } /// Initialize from the least significant bit of a `u64`. @@ -250,14 +234,14 @@ impl Choice { #[inline] #[must_use] pub const fn from_u64_lt(x: u64, y: u64) -> Self { - Self::from_u64_lsb(bitlt!(x, y, u64::BITS)) + crate::primitives::lt_u64(x, y) } /// Returns [`Choice::TRUE`] if `value != 0`, and [`Choice::FALSE`] otherwise. #[inline] #[must_use] pub const fn from_u64_nz(value: u64) -> Self { - Self::from_u64_lsb(bitnz!(value, u64::BITS)) + crate::primitives::nz_u64(value) } // u128 diff --git a/ctutils/src/primitives.rs b/ctutils/src/primitives.rs index 8943ae50..c4bcac78 100644 --- a/ctutils/src/primitives.rs +++ b/ctutils/src/primitives.rs @@ -9,6 +9,74 @@ //! Native zero counts are used when supported and advertised by the target configuration. use crate::Choice; +/// Bitwise less-than-or equal: returns `1` if `x <= y`, and otherwise returns `0`. +/// +/// See "Hacker's Delight" 2nd edition, section 2-12 (Comparison predicates) +macro_rules! bitle { + ($x:expr, $y:expr, $bits:expr) => { + (((!$x) | $y) & (($x ^ $y) | !($y.wrapping_sub($x)))) >> ($bits - 1) + }; +} + +/// Bitwise less-than: returns `1` if `x < y`, and otherwise returns `0`. +/// +/// See "Hacker's Delight" 2nd edition, section 2-12 (Comparison predicates) +macro_rules! bitlt { + ($x:expr, $y:expr, $bits:expr) => { + (((!$x) & $y) | (((!$x) | $y) & $x.wrapping_sub($y))) >> ($bits - 1) + }; +} + +/// Bitwise non-zero: returns `1` if `x != 0`, and otherwise returns `0`. +macro_rules! bitnz { + ($value:expr, $bits:expr) => { + ($value | $value.wrapping_neg()) >> ($bits - 1) + }; +} + +pub(crate) use {bitle, bitlt, bitnz}; + +// Keep the comparison bit opaque before narrowing it to Choice's byte representation. +// ARM targets without pointer-width atomics cover Thumb-1 on Rust 1.87, which does not +// advertise `thumb2` as a target feature. The other targets retain the original lowering. +macro_rules! comparison_primitives { + ($ty:ty, $le:ident, $lt:ident, $nz:ident, $from_lsb:ident) => { + #[inline(always)] + pub(crate) const fn $le(lhs: $ty, rhs: $ty) -> Choice { + let bit = bitle!(lhs, rhs, <$ty>::BITS); + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + let bit = core::hint::black_box(bit); + Choice::$from_lsb(bit) + } + + #[cfg_attr( + all(target_arch = "arm", not(target_has_atomic = "ptr")), + inline(never) + )] + #[cfg_attr( + not(all(target_arch = "arm", not(target_has_atomic = "ptr"))), + inline(always) + )] + pub(crate) const fn $lt(lhs: $ty, rhs: $ty) -> Choice { + let bit = bitlt!(lhs, rhs, <$ty>::BITS); + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + let bit = core::hint::black_box(bit); + Choice::$from_lsb(bit) + } + + #[inline(always)] + pub(crate) const fn $nz(value: $ty) -> Choice { + let bit = bitnz!(value, <$ty>::BITS); + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + let bit = core::hint::black_box(bit); + Choice::$from_lsb(bit) + } + }; +} + +comparison_primitives!(u32, le_u32, lt_u32, nz_u32, from_u32_lsb); +comparison_primitives!(u64, le_u64, lt_u64, nz_u64, from_u64_lsb); + /// Count leading zero bits, returning 32 if `value` is zero. #[inline] #[must_use] diff --git a/ctutils/tests/proptests.rs b/ctutils/tests/proptests.rs index baf3522b..95a2eaa1 100644 --- a/ctutils/tests/proptests.rs +++ b/ctutils/tests/proptests.rs @@ -61,6 +61,22 @@ macro_rules! int_proptests { int_proptests!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128); proptest::proptest! { + #[test] + fn from_u32_comparison(x in any::(), y in any::()) { + prop_assert_eq!(x == y, Choice::from_u32_eq(x, y).to_bool()); + prop_assert_eq!(x <= y, Choice::from_u32_le(x, y).to_bool()); + prop_assert_eq!(x < y, Choice::from_u32_lt(x, y).to_bool()); + prop_assert_eq!(x != 0, Choice::from_u32_nz(x).to_bool()); + } + + #[test] + fn from_u64_comparison(x in any::(), y in any::()) { + prop_assert_eq!(x == y, Choice::from_u64_eq(x, y).to_bool()); + prop_assert_eq!(x <= y, Choice::from_u64_le(x, y).to_bool()); + prop_assert_eq!(x < y, Choice::from_u64_lt(x, y).to_bool()); + prop_assert_eq!(x != 0, Choice::from_u64_nz(x).to_bool()); + } + #[test] fn from_u128_comparison( x in any::(),