From 5ed98be36cc09badf4646030e10feacccafe5cf6 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Thu, 22 Oct 2026 21:29:07 -0600 Subject: [PATCH] ctutils: add `borrowing_sub_*` primitives Followup to #1570 which added `overflowing_add_*` primitives which return `Choice`. This adds the analogous subtraction functionality. --- .../checkct/driver/src/driver/primitives.rs | 52 ++++++++++++++ ctutils/src/primitives.rs | 69 ++++++++++++++++++- ctutils/tests/primitives.rs | 18 ++++- 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/ctutils/checkct/driver/src/driver/primitives.rs b/ctutils/checkct/driver/src/driver/primitives.rs index f017db14..1f701fd1 100644 --- a/ctutils/checkct/driver/src/driver/primitives.rs +++ b/ctutils/checkct/driver/src/driver/primitives.rs @@ -93,3 +93,55 @@ macro_rules! addition_checks { addition_checks!(u64_add, u64, overflowing_add_u64, to_u64_mask); addition_checks!(u128_add, u128, overflowing_add_u128, to_u128_mask); + +macro_rules! subtraction_checks { + ($module:ident, $ty:ty, $sub:ident, $mask:ident) => { + mod $module { + use super::*; + + #[checkct] + pub fn borrowing_sub() { + black_box(PublicRng.next_u32()); + let incoming = Choice::from_u8_lsb(private_integer!(u8)); + let (difference, borrow) = + primitives::$sub(private_integer!($ty), private_integer!($ty), incoming); + black_box((difference, borrow.to_u8())); + } + + #[checkct] + pub fn borrowing_sub_zero_borrow() { + black_box(PublicRng.next_u32()); + let (difference, borrow) = + primitives::$sub(private_integer!($ty), private_integer!($ty), Choice::FALSE); + black_box((difference, borrow.to_u8())); + } + + #[checkct] + pub fn borrowing_sub_limbs() { + black_box(PublicRng.next_u32()); + let lhs = [private_integer!($ty), private_integer!($ty)]; + let rhs = [private_integer!($ty), private_integer!($ty)]; + let mut borrow = Choice::from_u8_lsb(private_integer!(u8)); + let mut difference = [0; 2]; + for i in 0..2 { + (difference[i], borrow) = primitives::$sub(lhs[i], rhs[i], borrow); + } + black_box((difference, borrow.to_u8())); + } + + /// Division conditionally adds back the divisor using the outgoing borrow mask. + #[checkct] + pub fn borrowing_sub_add_back() { + black_box(PublicRng.next_u32()); + let lhs = private_integer!($ty); + let rhs = private_integer!($ty); + let incoming = Choice::from_u8_lsb(private_integer!(u8)); + let (difference, borrow) = primitives::$sub(lhs, rhs, incoming); + black_box(difference.wrapping_add(rhs & borrow.$mask())); + } + } + }; +} + +subtraction_checks!(u32_sub, u32, borrowing_sub_u32, to_u32_mask); +subtraction_checks!(u64_sub, u64, borrowing_sub_u64, to_u64_mask); diff --git a/ctutils/src/primitives.rs b/ctutils/src/primitives.rs index c4bcac78..f1060a57 100644 --- a/ctutils/src/primitives.rs +++ b/ctutils/src/primitives.rs @@ -18,12 +18,19 @@ macro_rules! bitle { }; } +/// Outgoing borrow bit for a wrapping `x - y - borrow_in` result. +macro_rules! bitborrow { + ($x:expr, $y:expr, $result:expr, $bits:expr) => { + (((!$x) & $y) | (((!$x) | $y) & $result)) >> ($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) + $crate::primitives::bitborrow!($x, $y, ($x).wrapping_sub($y), $bits) }; } @@ -34,7 +41,7 @@ macro_rules! bitnz { }; } -pub(crate) use {bitle, bitlt, bitnz}; +pub(crate) use {bitborrow, 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 @@ -77,6 +84,29 @@ macro_rules! comparison_primitives { comparison_primitives!(u32, le_u32, lt_u32, nz_u32, from_u32_lsb); comparison_primitives!(u64, le_u64, lt_u64, nz_u64, from_u64_lsb); +macro_rules! borrowing_sub_primitives { + ($ty:ty, $sub:ident, $from_lsb:ident) => { + /// Subtract `rhs` and an incoming borrow from `lhs`. + /// + /// Returns the wrapping difference and [`Choice::TRUE`] if the subtraction underflows. + /// The incoming `borrow` subtracts one when true and zero when false. + #[inline] + #[must_use] + pub const fn $sub(lhs: $ty, rhs: $ty, borrow: Choice) -> ($ty, Choice) { + let difference = lhs.wrapping_sub(rhs).wrapping_sub(borrow.to_u8() as $ty); + // Keep all arithmetic in the operand type: widening can trigger incorrect + // optimization on older compilers: https://github.com/rust-lang/rust/issues/149522 + // Keep the full-width bit opaque before narrowing it to Choice, so a caller's + // masked add-back cannot be folded into a secret-dependent branch. + let borrow = core::hint::black_box(bitborrow!(lhs, rhs, difference, <$ty>::BITS)); + (difference, Choice::$from_lsb(borrow)) + } + }; +} + +borrowing_sub_primitives!(u32, borrowing_sub_u32, from_u32_lsb); +borrowing_sub_primitives!(u64, borrowing_sub_u64, from_u64_lsb); + /// Count leading zero bits, returning 32 if `value` is zero. #[inline] #[must_use] @@ -236,6 +266,12 @@ mod tests { // Keep all public entrypoints usable by const callers such as crypto-bigint. const _: () = { + assert!(borrowing_sub_u32(0, 0, Choice::TRUE).0 == u32::MAX); + assert!(borrowing_sub_u32(0, 0, Choice::TRUE).1.to_bool()); + assert!(borrowing_sub_u64(0, 0, Choice::TRUE).0 == u64::MAX); + assert!(borrowing_sub_u64(0, 0, Choice::TRUE).1.to_bool()); + assert!(!borrowing_sub_u32(1, 0, Choice::TRUE).1.to_bool()); + assert!(!borrowing_sub_u64(1, 0, Choice::TRUE).1.to_bool()); assert!(leading_zeros_u32(0) == 32); assert!(trailing_zeros_u32(0) == 32); assert!(leading_zeros_u64(0) == 64); @@ -270,6 +306,35 @@ mod tests { } } + macro_rules! subtraction_tests { + ($test:ident, $ty:ty, $sub:ident) => { + #[test] + fn $test() { + let half = 1 << (<$ty>::BITS >> 1); + let high = 1 << (<$ty>::BITS - 1); + let values = [0, 1, half - 1, half, half + 1, high - 1, high, <$ty>::MAX]; + for lhs in values { + for rhs in values { + for incoming in [false, true] { + let (expected, first_borrow) = lhs.overflowing_sub(rhs); + let (expected, second_borrow) = + expected.overflowing_sub(<$ty>::from(incoming)); + let (difference, borrow) = + $sub(lhs, rhs, Choice::from_u8_lsb(u8::from(incoming))); + assert_eq!( + (difference, borrow.to_bool()), + (expected, first_borrow | second_borrow) + ); + } + } + } + } + }; + } + + subtraction_tests!(sub_u32, u32, borrowing_sub_u32); + subtraction_tests!(sub_u64, u64, borrowing_sub_u64); + macro_rules! addition_tests { ($test:ident, $ty:ty, $add:ident) => { #[test] diff --git a/ctutils/tests/primitives.rs b/ctutils/tests/primitives.rs index fc053072..0e56af3f 100644 --- a/ctutils/tests/primitives.rs +++ b/ctutils/tests/primitives.rs @@ -1,9 +1,25 @@ //! Differential tests against the core integer operations. -use ctutils::primitives::*; +use ctutils::{Choice, primitives::*}; use proptest::prelude::*; proptest! { + #[test] + fn sub_u32(lhs in any::(), rhs in any::(), incoming in any::()) { + let (expected, first_borrow) = lhs.overflowing_sub(rhs); + let (expected, second_borrow) = expected.overflowing_sub(u32::from(incoming)); + let (difference, borrow) = borrowing_sub_u32(lhs, rhs, Choice::from_u8_lsb(u8::from(incoming))); + prop_assert_eq!((difference, borrow.to_bool()), (expected, first_borrow | second_borrow)); + } + + #[test] + fn sub_u64(lhs in any::(), rhs in any::(), incoming in any::()) { + let (expected, first_borrow) = lhs.overflowing_sub(rhs); + let (expected, second_borrow) = expected.overflowing_sub(u64::from(incoming)); + let (difference, borrow) = borrowing_sub_u64(lhs, rhs, Choice::from_u8_lsb(u8::from(incoming))); + prop_assert_eq!((difference, borrow.to_bool()), (expected, first_borrow | second_borrow)); + } + #[test] fn zero_counts_u32(value in any::()) { prop_assert_eq!(leading_zeros_u32(value), value.leading_zeros());