From 902028f348cff517b80de9993e912a68e4658724 Mon Sep 17 00:00:00 2001 From: Teddy Tennant Date: Sat, 11 Jul 2026 13:25:25 -0400 Subject: [PATCH 1/3] Fix Uniform::new_inclusive overflow on large finite float ranges Dividing high - low by 1 - EPSILON can round to infinity even when the range itself is finite (e.g. 0.0..=f64::MAX), yielding a spurious NonFinite error while Uniform::new and sample_single_inclusive both accept the same range. Clamp infinite lanes to the largest finite value and let new_bounded reduce scale as usual, so that samples still cannot exceed high. Noted by dhardy in #1603. --- CHANGELOG.md | 2 ++ src/distr/uniform_float.rs | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3085994c4d..0770e5ef09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ You may also find the [Upgrade Guide](https://rust-random.github.io/book/update. ### Fixes - Fix `WeightedIndex` panic when the sum of float weights is infinite; return `Error::Overflow` instead ([#1808]) +- Fix spurious `Error::NonFinite` from `Uniform::new_inclusive` on large finite float ranges such as `0.0..=f64::MAX` ([#1809]) [#1808]: https://github.com/rust-random/rand/pull/1808 +[#1809]: https://github.com/rust-random/rand/pull/1809 ## [0.10.2] — 2026-07-02 diff --git a/src/distr/uniform_float.rs b/src/distr/uniform_float.rs index 3e91c0f3a0..ec3520defc 100644 --- a/src/distr/uniform_float.rs +++ b/src/distr/uniform_float.rs @@ -126,10 +126,20 @@ macro_rules! uniform_float_impl { return Err(Error::EmptyRange); } + let range = high - low; + if !range.all_finite() { + return Err(Error::NonFinite); + } + let max_rand = <$ty>::splat(1.0 as $f_scalar - $f_scalar::EPSILON); - let scale = (high - low) / max_rand; + let mut scale = range / max_rand; if !scale.all_finite() { - return Err(Error::NonFinite); + // The division above may overflow to infinity even though + // `range` is finite (e.g. `low = 0.0`, `high = f64::MAX`). + // Replace infinite lanes with the largest finite value; + // `new_bounded` reduces `scale` as required to ensure that + // samples can never exceed `high`. + scale = scale.decrease_masked(scale.gt_mask(<$ty>::splat($f_scalar::MAX))); } Ok(Self::new_bounded(low, high, scale)) @@ -238,6 +248,8 @@ mod tests { (-<$f_scalar>::from_bits(7), -0.0), (0.1 * $f_scalar::MAX, $f_scalar::MAX), (-$f_scalar::MAX * 0.2, $f_scalar::MAX * 0.7), + (0.0, $f_scalar::MAX), + (-$f_scalar::MAX, 0.0), ]; for &(low_scalar, high_scalar) in v.iter() { for lane in 0..<$ty>::LEN { @@ -360,6 +372,10 @@ mod tests { #[test] fn test_float_overflow() { assert_eq!(Uniform::try_from(f64::MIN..f64::MAX), Err(Error::NonFinite)); + assert_eq!( + Uniform::try_from(f64::MIN..=f64::MAX), + Err(Error::NonFinite) + ); } #[test] From 2b1d0eb95550b2366bd7c1a989fdb98d1b07ad8c Mon Sep 17 00:00:00 2001 From: Teddy Tennant Date: Sat, 8 Aug 2026 12:28:30 -0400 Subject: [PATCH 2/3] Handle non-finite scale in new_bounded instead of special-casing it Per review: drop the explicit non-finite scale branch in new_inclusive and let new_bounded reduce the scale instead. gt_mask compares false for a non-finite product, so the loop would accept it silently; not_le_mask treats it as out of bounds and reduces the scale, which decrease_masked already handles for infinity. new_inclusive still rejects a non-finite range up front, so f64::MIN..=f64::MAX remains an error, matching the exclusive range. --- src/distr/uniform_float.rs | 19 +++++++++---------- src/distr/utils.rs | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/distr/uniform_float.rs b/src/distr/uniform_float.rs index ec3520defc..b4351db511 100644 --- a/src/distr/uniform_float.rs +++ b/src/distr/uniform_float.rs @@ -66,7 +66,10 @@ macro_rules! uniform_float_impl { let max_rand = <$ty>::splat(1.0 as $f_scalar - $f_scalar::EPSILON); loop { - let mask = (scale * max_rand + low).gt_mask(high); + // `not_le_mask` rather than `gt_mask` so that a non-finite + // product (which compares `false` under `>`) is also + // reduced instead of being silently accepted. + let mask = (scale * max_rand + low).not_le_mask(high); if !mask.any() { break; } @@ -132,15 +135,11 @@ macro_rules! uniform_float_impl { } let max_rand = <$ty>::splat(1.0 as $f_scalar - $f_scalar::EPSILON); - let mut scale = range / max_rand; - if !scale.all_finite() { - // The division above may overflow to infinity even though - // `range` is finite (e.g. `low = 0.0`, `high = f64::MAX`). - // Replace infinite lanes with the largest finite value; - // `new_bounded` reduces `scale` as required to ensure that - // samples can never exceed `high`. - scale = scale.decrease_masked(scale.gt_mask(<$ty>::splat($f_scalar::MAX))); - } + // This division may overflow to infinity even where `range` is + // finite (e.g. `low = 0.0`, `high = f64::MAX`). `new_bounded` + // reduces `scale` until samples cannot exceed `high`, which + // handles the infinite case in a single step. + let scale = range / max_rand; Ok(Self::new_bounded(low, high, scale)) } diff --git a/src/distr/utils.rs b/src/distr/utils.rs index d3cfbf6a0f..2ae32fd2f1 100644 --- a/src/distr/utils.rs +++ b/src/distr/utils.rs @@ -221,6 +221,11 @@ pub(crate) trait FloatSIMDUtils { type Mask; fn gt_mask(self, other: Self) -> Self::Mask; + // The negation of `<=`, which (unlike `gt_mask`) is also `true` for lanes + // which are not-a-number. This lets callers treat NaN like an + // out-of-bounds value rather than silently accepting it. + fn not_le_mask(self, other: Self) -> Self::Mask; + // Decrease all lanes where the mask is `true` to the next lower value // representable by the floating-point type. At least one of the lanes // must be set. @@ -298,6 +303,11 @@ macro_rules! scalar_float_impl { self > other } + #[inline(always)] + fn not_le_mask(self, other: Self) -> Self::Mask { + !(self <= other) + } + #[inline(always)] fn decrease_masked(self, mask: Self::Mask) -> Self { debug_assert!(mask, "At least one lane must be set"); @@ -361,6 +371,11 @@ macro_rules! simd_impl { self.simd_gt(other) } + #[inline(always)] + fn not_le_mask(self, other: Self) -> Self::Mask { + !self.simd_le(other) + } + #[inline(always)] fn decrease_masked(self, mask: Self::Mask) -> Self { // Casting a mask into ints will produce all bits set for From 2d5c5d04eb58aec2ba6617607878230df0d9340e Mon Sep 17 00:00:00 2001 From: Teddy Tennant Date: Sat, 8 Aug 2026 12:31:18 -0400 Subject: [PATCH 3/3] Remove now-unused gt_mask from FloatSIMDUtils not_le_mask replaced its only call site, so gt_mask was dead code and failed the -D warnings clippy job. --- src/distr/utils.rs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/distr/utils.rs b/src/distr/utils.rs index 2ae32fd2f1..1a74bdca1d 100644 --- a/src/distr/utils.rs +++ b/src/distr/utils.rs @@ -219,11 +219,10 @@ pub(crate) trait FloatSIMDUtils { fn all_finite(self) -> bool; type Mask; - fn gt_mask(self, other: Self) -> Self::Mask; - // The negation of `<=`, which (unlike `gt_mask`) is also `true` for lanes - // which are not-a-number. This lets callers treat NaN like an - // out-of-bounds value rather than silently accepting it. + // The negation of `<=`, which (unlike `>`) is also `true` for lanes which + // are not-a-number. This lets callers treat NaN like an out-of-bounds + // value rather than silently accepting it. fn not_le_mask(self, other: Self) -> Self::Mask; // Decrease all lanes where the mask is `true` to the next lower value @@ -298,11 +297,6 @@ macro_rules! scalar_float_impl { self.is_finite() } - #[inline(always)] - fn gt_mask(self, other: Self) -> Self::Mask { - self > other - } - #[inline(always)] fn not_le_mask(self, other: Self) -> Self::Mask { !(self <= other) @@ -366,11 +360,6 @@ macro_rules! simd_impl { self.is_finite().all() } - #[inline(always)] - fn gt_mask(self, other: Self) -> Self::Mask { - self.simd_gt(other) - } - #[inline(always)] fn not_le_mask(self, other: Self) -> Self::Mask { !self.simd_le(other)