diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d9f126945..a715d24262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,15 @@ You may also find the [Upgrade Guide](https://rust-random.github.io/book/update. ## [Unreleased] +### Fixes +- Fix possible memory safety violation due to deserialization of `UniformChar` from bad source ([#1790]) + ### Changes - Document required output order of fn `partial_shuffle` and apply `#[must_use]` ([#1769]) - Avoid usage of `unsafe` in contexts where non-local memory corruption could invalidate contract ([#1791]) [#1769]: https://github.com/rust-random/rand/pull/1769 +[#1790]: https://github.com/rust-random/rand/pull/1790 [#1791]: https://github.com/rust-random/rand/pull/1791 ## [0.10.1] — 2026-02-11 diff --git a/src/distr/uniform_int.rs b/src/distr/uniform_int.rs index 865d63f69d..96ac8cfdac 100644 --- a/src/distr/uniform_int.rs +++ b/src/distr/uniform_int.rs @@ -74,6 +74,15 @@ pub struct UniformInt { macro_rules! uniform_int_impl { ($ty:ty, $uty:ty, $sample_ty:ident) => { + impl UniformInt<$ty> { + /// Get the maximum possible value + #[allow(unused)] + #[inline] + pub(crate) fn max(&self) -> $ty { + self.range.wrapping_sub(1).wrapping_add(self.low) + } + } + impl SampleUniform for $ty { type Sampler = UniformInt<$ty>; } @@ -693,6 +702,7 @@ mod tests { let r = Uniform::try_from(2u32..7).unwrap(); assert_eq!(r.0.low, 2); assert_eq!(r.0.range, 5); + assert_eq!(r.0.max(), 6); } #[test] @@ -707,6 +717,7 @@ mod tests { let r = Uniform::try_from(2u32..=6).unwrap(); assert_eq!(r.0.low, 2); assert_eq!(r.0.range, 5); + assert_eq!(r.0.max(), 6); } #[test] diff --git a/src/distr/uniform_other.rs b/src/distr/uniform_other.rs index 25b4d9b83e..b69283e631 100644 --- a/src/distr/uniform_other.rs +++ b/src/distr/uniform_other.rs @@ -33,9 +33,24 @@ impl SampleUniform for char { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct UniformChar { + #[cfg_attr(feature = "serde", serde(deserialize_with = "deser_sampler"))] sampler: UniformInt, } +#[cfg(feature = "serde")] +fn deser_sampler<'de, D>(d: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let sampler = as serde::Deserialize>::deserialize(d)?; + if sampler.max() > char::MAX as u32 - CHAR_SURROGATE_LEN { + return Err(serde::de::Error::custom( + "bad sampler range for UniformChar", + )); + } + Ok(sampler) +} + /// UTF-16 surrogate range start const CHAR_SURROGATE_START: u32 = 0xD800; /// UTF-16 surrogate range size @@ -298,6 +313,24 @@ mod tests { } } + #[test] + #[cfg(feature = "serde")] + fn test_char_bad_deser() { + let json = r#"{"sampler":{"low":4294967200,"range":0,"thresh":0}}"#; + let result = serde_json::from_str::>(json); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + + #[cfg(feature = "alloc")] + { + assert_eq!( + alloc::string::ToString::to_string(&err), + "bad sampler range for UniformChar at line 1 column 51" + ); + } + } + #[test] #[cfg_attr(miri, ignore)] // Miri is too slow fn test_durations() { diff --git a/src/seq/slice.rs b/src/seq/slice.rs index cf742275fc..267733a827 100644 --- a/src/seq/slice.rs +++ b/src/seq/slice.rs @@ -823,6 +823,7 @@ mod test { #[test] #[cfg(feature = "std")] + #[cfg_attr(miri, ignore)] // Miri is too slow fn test_multiple_weighted_distributions() { use super::*;