Skip to content

Reject UniformDuration sampler state which can panic in Duration::new - #1841

Open
shoemoney wants to merge 2 commits into
rust-random:masterfrom
shoemoney:fix-uniform-duration-serde-panic
Open

shoemoney wants to merge 2 commits into
rust-random:masterfrom
shoemoney:fix-uniform-duration-serde-panic

Conversation

@shoemoney

@shoemoney shoemoney commented Sep 21, 2026

Copy link
Copy Markdown
  • Added a CHANGELOG.md entry

Summary

A deserialized UniformDuration can panic in Duration::new. This is the unguarded sibling of the Uniform<char> problem fixed in #1831: same threat model, same fix shape.

Motivation

UniformChar carries a validating deserializer on its sampler field (src/distr/uniform_other.rs:36, added by #1831):

#[cfg_attr(feature = "serde", serde(deserialize_with = "deser_sampler"))]
sampler: UniformInt<u32>,

UniformDuration (src/distr/uniform_other.rs:127) and the private UniformDurationMode use a bare derive(Serialize, Deserialize) with no validation, and sample feeds that state straight to Duration::new, which panics on overflow:

  • Small arm: Duration::new(secs, n), where n comes from a Uniform<u32> whose range is whatever was deserialized. If n >= 1_000_000_000 the carry can push secs past u64::MAX.
  • Large arm: Duration::new(s, n + self.offset). The rejection test only fires when s == max_secs, so a seconds sampler whose maximum exceeds max_secs is never rejected, and offset is never checked to be a valid subsecond count (it can also overflow the n + self.offset addition).

The UniformInt hardening in #1831 only fixed max() for the range == 0 case; it does not reject out-of-range low/range, so it is not a guard here.

This is only reachable by deserializing untrusted or corrupted sampler state. That is the same threat model the project already accepted when it hardened Uniform<char>, so I am filing it as a normal bug rather than through the security advisory process. It is not a memory-safety issue.

Details

Before, on master (9e7d328), deserializing and sampling:

let json = r#"{"mode":{"Small":{"secs":18446744073709551615,"nanos":{"low":1000000000,"range":1,"thresh":0}}},"offset":0}"#;
let distr: UniformDuration = serde_json::from_str(json).unwrap();
let _ = distr.sample(&mut rng);
thread '...' panicked at library/core/src/time.rs:201:18:
overflow in Duration::new

The new test test_duration_bad_deser, run against unmodified source:

test distr::uniform::other::tests::test_duration_bad_deser ... FAILED
---- distr::uniform::other::tests::test_duration_bad_deser stdout ----
panicked at src/distr/uniform_other.rs:346:13:
assertion failed: result.is_err()
test result: FAILED. 0 passed; 1 failed

The fix replaces the derived Deserialize for UniformDuration with one that checks the invariants new_inclusive establishes: offset is a valid subsecond nanosecond count, the Small nanos sampler cannot carry a second past u64::MAX, and the Large seconds sampler cannot exceed max_secs or carry past u64::MAX. Medium needs no check, since Duration::new(nanos / 1e9, nanos % 1e9) cannot overflow for any u64. Rejection produces a clean serde data error, matching the UniformChar behaviour and its test.

After:

test distr::uniform::other::tests::test_duration_bad_deser ... ok
test distr::uniform::other::tests::test_serialization_uniform_duration ... ok
test result: ok. 113 passed; 0 failed; 0 ignored

Baseline on master with --features serde was 112 passed, so the delta is exactly the one new test. Round-tripping a validly constructed UniformDuration is unaffected. cargo fmt --check and cargo clippy --features serde --all-targets are clean, and cargo build --no-default-features still builds.

sample() passes the deserialized sampler state directly to Duration::new,
which panics on overflow. UniformChar already validates its deserialized
UniformInt<u32> for the same reason (rust-random#1831); UniformDuration and
UniformDurationMode used a bare derive with no validation.

Replace the derived Deserialize for UniformDuration with one that checks
the invariants new_inclusive establishes: offset is a valid subsecond
nanosecond count, the Small nanos sampler cannot carry a second past
u64::MAX, and the Large seconds sampler cannot exceed max_secs or carry
past u64::MAX.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant