Reject u64::MAX in version segments to prevent overflow (#17985)

Version segments (release, epoch, pre/post/dev) are now limited to
`u64::MAX - 1` (18446744073709551614) to prevent overflow in `segment +
1` (e.g., `~=` upper bound, `==*`/`!=*` upper bound, `>` with dev/post
releases, `python_version` marker algebra).

The check is placed in `parse_number` (not `parse_u64`) so it only
applies to version segments parsed through the PEP 440 version parser,
not to local version segment numbers, for which we don't need to compute
an above bound.
This commit is contained in:
konsti
2026-02-12 18:24:22 +01:00
committed by GitHub
parent 6663f8899d
commit 7cd6738def
5 changed files with 105 additions and 2 deletions
+15 -2
View File
@@ -2266,7 +2266,19 @@ impl<'a> Parser<'a> {
if digits.is_empty() {
return Ok(None);
}
Ok(Some(parse_u64(digits)?))
let n = parse_u64(digits)?;
// Reject `u64::MAX` to prevent arithmetic overflow in downstream code
// that computes `segment + 1` (e.g., `~=` upper bound, `==*` upper
// bound, `python_version` marker algebra). This only applies to version
// segments (release, epoch, pre/post/dev), not local version segments
// which don't undergo arithmetic.
if n == u64::MAX {
return Err(ErrorKind::NumberTooBig {
bytes: digits.to_vec(),
}
.into());
}
Ok(Some(n))
}
/// Turns whatever state has been gathered into a `VersionPattern`.
@@ -2581,7 +2593,7 @@ impl std::fmt::Display for VersionParseError {
f,
"expected number less than or equal to {}, \
but number found in {string:?} exceeds it",
u64::MAX,
u64::MAX - 1,
)
}
ErrorKind::NoLeadingNumber => {
@@ -4182,6 +4194,7 @@ mod tests {
assert_eq!(p("10a"), Err(ErrorKind::InvalidDigit { got: b'a' }.into()));
assert_eq!(p("10["), Err(ErrorKind::InvalidDigit { got: b'[' }.into()));
assert_eq!(p("10/"), Err(ErrorKind::InvalidDigit { got: b'/' }.into()));
// u64::MAX + 1 is rejected (overflow during parsing).
assert_eq!(
p("18446744073709551616"),
Err(ErrorKind::NumberTooBig {
+27
View File
@@ -621,4 +621,31 @@ mod tests {
let v = "0.12.0.post1".parse::<Version>().unwrap();
assert!(!range.contains(&v), "should exclude 0.12.0.post1");
}
/// Do not panic with `u64::MAX` causing an `u64::MAX + 1` overflow.
#[test]
fn u64_max_version_segments_rejected_at_parse_time() {
assert!(
"~=18446744073709551615.0"
.parse::<VersionSpecifier>()
.is_err()
);
assert!(
"==18446744073709551615.*"
.parse::<VersionSpecifier>()
.is_err()
);
// u64::MAX - 1 is still accepted.
assert!(
"~=18446744073709551614.0"
.parse::<VersionSpecifier>()
.is_ok()
);
assert!(
"==18446744073709551614.*"
.parse::<VersionSpecifier>()
.is_ok()
);
}
}
+12
View File
@@ -2051,4 +2051,16 @@ Failed to parse version: Unexpected end of version specifier, expected operator.
"The ~= operator requires at least two segments in the release version"
);
}
/// Do not panic with `u64::MAX` causing an `u64::MAX + 1` overflow.
#[test]
fn bounding_specifiers_u64_max_rejected_at_parse_time() {
assert!(VersionSpecifier::from_str("~=3.18446744073709551615.0").is_err());
assert!(VersionSpecifier::from_str("~=18446744073709551615.0").is_err());
// u64::MAX - 1 is accepted and bounding_specifiers does not overflow.
let specifier = VersionSpecifier::from_str("~=3.18446744073709551614.0").unwrap();
let tilde = TildeVersionSpecifier::from_specifier(specifier).unwrap();
let (_lower, _upper) = tilde.bounding_specifiers();
}
}
+21
View File
@@ -1814,4 +1814,25 @@ mod tests {
let b = m().and(not_x86, windows);
assert_eq!(m().or(a, b), windows);
}
/// Do not panic with `u64::MAX` causing an `u64::MAX + 1` overflow.
#[test]
fn python_version_marker_u64_max() {
// The parse error is converted to a warning and the condition is ignored.
assert_eq!(
MarkerExpression::from_str("python_version > '3.18446744073709551615'").unwrap(),
None,
);
assert_eq!(
MarkerExpression::from_str("python_version <= '3.18446744073709551615'").unwrap(),
None,
);
// `u64::MAX - 1` accepted
assert!(
MarkerExpression::from_str("python_version > '3.18446744073709551614'")
.unwrap()
.is_some()
);
}
}
+30
View File
@@ -33416,3 +33416,33 @@ async fn lock_check_multiple_default_indexes_explicit_assignment_dependency_grou
Ok(())
}
/// Do not panic with `u64::MAX` causing an `u64::MAX + 1` overflow.
#[test]
fn lock_tilde_equal_version_u64_max_rejected() -> Result<()> {
let context = uv_test::test_context!("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bar~=18446744073709551615.0"]
"#})?;
uv_snapshot!(context.filters(), context.lock(), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to build `foo @ file://[TEMP_DIR]/`
Failed to parse metadata from built wheel
expected number less than or equal to 18446744073709551614, but number found in "18446744073709551615" exceeds it
bar ~=18446744073709551615.0
^^^^^^^^^^^^^^^^^^^^^^^^
"#);
Ok(())
}