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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user