Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `Uniform<char>` deserialization accepting full-range and wrapping samplers that then panic in `sample` ([#1829])

[#1808]: https://github.com/rust-random/rand/pull/1808
[#1829]: https://github.com/rust-random/rand/pull/1829

## [0.10.2] — 2026-07-02

Expand Down
36 changes: 35 additions & 1 deletion src/distr/uniform_other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,18 @@ where
D: serde::Deserializer<'de>,
{
let sampler = <UniformInt<u32> as serde::Deserialize>::deserialize(d)?;
if sampler.max() > char::MAX as u32 - CHAR_SURROGATE_LEN {
// `range == 0` is `UniformInt`'s full-u32-range marker. That is a valid
// state for `Uniform<u32>`, but a char sampler is always built from a
// bounded range, so here it only comes from a crafted payload that would
// sample arbitrary u32 (non-char) values. Reject it, together with any
// (low, range) whose inclusive max overflows or leaves the char range:
// those clear a wrapping `low + range - 1` and later panic in `sample`.
let in_char_range = sampler
.range
.checked_sub(1)
.and_then(|r| r.checked_add(sampler.low))
.is_some_and(|max| max <= char::MAX as u32 - CHAR_SURROGATE_LEN);
if !in_char_range {
return Err(serde::de::Error::custom(
"bad sampler range for UniformChar",
));
Expand Down Expand Up @@ -329,6 +340,29 @@ mod tests {
"bad sampler range for UniformChar at line 1 column 51"
);
}

// The `range == 0` full-range marker and payloads whose inclusive
// `low + range - 1` wraps also clear the old guard and then sample
// non-char code points, so they must be rejected too. See #1827.
for json in [
r#"{"sampler":{"low":5,"range":0,"thresh":0}}"#,
r#"{"sampler":{"low":4294967280,"range":32,"thresh":0}}"#,
] {
assert!(serde_json::from_str::<Uniform<char>>(json).is_err());
}
}

#[test]
#[cfg(feature = "serde")]
fn test_char_deser_roundtrip() {
let mut rng = crate::test::rng(892);
let distr = Uniform::new_inclusive('a', 'z').unwrap();
let de_distr: Uniform<char> =
serde_json::from_str(&serde_json::to_string(&distr).unwrap()).unwrap();
for _ in 0..100 {
let c = de_distr.sample(&mut rng);
assert!(c.is_ascii_lowercase());
}
}

#[test]
Expand Down