From 40c15463419a9fdf9cd58b09bb6dea9405172ab1 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Thu, 29 Oct 2026 12:50:21 -0600 Subject: [PATCH] ctutils: constant-time hardening for non-zero comparisons Extends `cargo checkct` coverage to cover non-zero comparisons via `Choice`, uncovering some non-constant-time code generation on `riscv32` and Thumb-1 targets. Addresses the problem with a new `nonzero_primitive!` macro and selective use of `black_box` on these targets, using `cargo checkct` to verify this addresses the issue. This should not have an impact on codegen on other platforms, which were already constant-time. --- ctutils/checkct/driver/src/driver.rs | 1 + ctutils/checkct/driver/src/driver/nonzero.rs | 64 ++++++++++++++++++++ ctutils/src/choice.rs | 8 +-- ctutils/src/primitives.rs | 37 ++++++++--- 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 ctutils/checkct/driver/src/driver/nonzero.rs diff --git a/ctutils/checkct/driver/src/driver.rs b/ctutils/checkct/driver/src/driver.rs index b02d1393..c3c91257 100644 --- a/ctutils/checkct/driver/src/driver.rs +++ b/ctutils/checkct/driver/src/driver.rs @@ -14,6 +14,7 @@ macro_rules! private_integer { mod comparisons; mod composition; mod negation; +mod nonzero; mod predicates; mod primitives; diff --git a/ctutils/checkct/driver/src/driver/nonzero.rs b/ctutils/checkct/driver/src/driver/nonzero.rs new file mode 100644 index 00000000..b512cea4 --- /dev/null +++ b/ctutils/checkct/driver/src/driver/nonzero.rs @@ -0,0 +1,64 @@ +//! Nonzero constructors with caller range information still visible. + +use super::*; + +macro_rules! nonzero_checks { + ($module:ident, $ty:ty, $nz:ident) => { + mod $module { + use super::*; + + fn private() -> $ty { + black_box(PublicRng.next_u32()); + private_integer!($ty) + } + + #[checkct] + pub fn unrestricted() { + black_box(Choice::$nz(private()).to_u8()); + } + + #[checkct] + pub fn shifted() { + // The u64 case previously branched on Thumb-1. + black_box(Choice::$nz(private() >> 1).to_u8()); + } + + #[checkct] + pub fn sign_bit_clear() { + black_box(Choice::$nz(private() & (<$ty>::MAX >> 1)).to_u8()); + } + } + }; +} + +nonzero_checks!(u8_ops, u8, from_u8_nz); +nonzero_checks!(u16_ops, u16, from_u16_nz); +nonzero_checks!(u32_ops, u32, from_u32_nz); +nonzero_checks!(u64_ops, u64, from_u64_nz); +nonzero_checks!(u128_ops, u128, from_u128_nz); + +macro_rules! positive_nonzero_check { + ($name:ident, $signed:ty, $unsigned:ty, $nz:ident) => { + #[checkct] + pub fn $name() { + black_box(PublicRng.next_u32()); + let value = private_integer!($signed); + black_box( + Choice::$nz((value & !(value >> (<$signed>::BITS - 1))) as $unsigned).to_u8(), + ); + } + }; +} + +positive_nonzero_check!(positive_i8, i8, u8, from_u8_nz); +positive_nonzero_check!(positive_i16, i16, u16, from_u16_nz); +positive_nonzero_check!(positive_i32, i32, u32, from_u32_nz); + +// On Thumb-1, the wide signed-mask argument itself can branch before ctutils +// receives it. Keep this composition regression on targets where the argument +// is constant-time; the shifted probes above exercise all widths on Thumb-1. +// Callers needing a signed comparison should bias the sign bit and use from_u*_lt. +#[cfg(not(all(target_arch = "arm", not(target_has_atomic = "ptr"))))] +positive_nonzero_check!(positive_i64, i64, u64, from_u64_nz); +#[cfg(not(all(target_arch = "arm", not(target_has_atomic = "ptr"))))] +positive_nonzero_check!(positive_i128, i128, u128, from_u128_nz); diff --git a/ctutils/src/choice.rs b/ctutils/src/choice.rs index 340ccef4..a6314199 100644 --- a/ctutils/src/choice.rs +++ b/ctutils/src/choice.rs @@ -1,4 +1,4 @@ -use crate::primitives::{bitle, bitnz}; +use crate::primitives::bitle; use crate::{CtAssign, CtAssignSlice, CtEq, CtEqSlice, CtSelectUsingCtAssign}; use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; @@ -138,7 +138,7 @@ impl Choice { #[inline] #[must_use] pub const fn from_u8_nz(value: u8) -> Self { - Self::from_u8_lsb(bitnz!(value, u8::BITS)) + crate::primitives::nz_u8(value) } // u16 @@ -183,7 +183,7 @@ impl Choice { #[inline] #[must_use] pub const fn from_u16_nz(value: u16) -> Self { - Self::from_u16_lsb(bitnz!(value, u16::BITS)) + crate::primitives::nz_u16(value) } // u32 @@ -323,7 +323,7 @@ impl Choice { #[inline] #[must_use] pub const fn from_u128_nz(value: u128) -> Self { - Self::from_u128_lsb(bitnz!(value, u128::BITS)) + crate::primitives::nz_u128(value) } // diff --git a/ctutils/src/primitives.rs b/ctutils/src/primitives.rs index beb8febc..4f088b61 100644 --- a/ctutils/src/primitives.rs +++ b/ctutils/src/primitives.rs @@ -45,7 +45,34 @@ macro_rules! bitnz { }; } -pub(crate) use {bitborrow, bitle, bitlt, bitnz}; +pub(crate) use {bitborrow, bitle, bitlt}; + +// Shared by every nonzero constructor, including the widths which do not use +// comparison_primitives. A result barrier alone does not prevent specialization +// of the arithmetic which computes the predicate. +macro_rules! nonzero_primitive { + ($ty:ty, $nz:ident, $from_lsb:ident) => { + #[inline(always)] + pub(crate) const fn $nz(value: $ty) -> Choice { + // Hide caller range information before computing the predicate. + // For example, a shifted value or signed mask can otherwise turn + // the nonzero test into a secret-dependent comparison branch. + #[cfg(any( + target_arch = "riscv32", + all(target_arch = "arm", not(target_has_atomic = "ptr")) + ))] + let value = black_box(value); + let bit = bitnz!(value, <$ty>::BITS); + #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))] + let bit = black_box(bit); + Choice::$from_lsb(bit) + } + }; +} + +nonzero_primitive!(u8, nz_u8, from_u8_lsb); +nonzero_primitive!(u16, nz_u16, from_u16_lsb); +nonzero_primitive!(u128, nz_u128, from_u128_lsb); // 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 @@ -75,13 +102,7 @@ macro_rules! comparison_primitives { 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 = black_box(bit); - Choice::$from_lsb(bit) - } + nonzero_primitive!($ty, $nz, $from_lsb); }; }