diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 77a33e0d96c3c..488e95c11d1c8 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -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 { diff --git a/crates/uv-pep440/src/version_ranges.rs b/crates/uv-pep440/src/version_ranges.rs index 2fc090764eb6d..22f34a8c8587a 100644 --- a/crates/uv-pep440/src/version_ranges.rs +++ b/crates/uv-pep440/src/version_ranges.rs @@ -621,4 +621,31 @@ mod tests { let v = "0.12.0.post1".parse::().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::() + .is_err() + ); + assert!( + "==18446744073709551615.*" + .parse::() + .is_err() + ); + + // u64::MAX - 1 is still accepted. + assert!( + "~=18446744073709551614.0" + .parse::() + .is_ok() + ); + assert!( + "==18446744073709551614.*" + .parse::() + .is_ok() + ); + } } diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index 155585fb2cee2..ae1411637affd 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -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(); + } } diff --git a/crates/uv-pep508/src/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs index 8a170202ea180..e9a138ff13104 100644 --- a/crates/uv-pep508/src/marker/algebra.rs +++ b/crates/uv-pep508/src/marker/algebra.rs @@ -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() + ); + } } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index c30e8d1c31a26..38b498312209a 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -33368,3 +33368,33 @@ fn lock_check_multiple_default_indexes_explicit_assignment_dependency_group() -> 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(()) +}