Check dist name to handle bogus redirect (#12917)

When an index performs a bogus redirect or otherwise returns a different
distribution name than expected, uv currently hangs.

In the example case, requesting the simple index page for any package
returns the page for anyio. This mean querying the sniffio version map
returns only anyio entries, and the version maps resolves to an anyio
version. When the resolver makes a query for sniffio and waits for it to
resolve, the main thread finds an anyio and resolves only that in the
wait map, causing the hang.

We fix this by checking the name of the returned distribution against
the name of the requested distribution. For good measure, we add the
same check in `Request::Dist` and `Request::Installed`. For performance
and complexity reasons, we don't perform this check in the version map
itself, but only after a candidate distribution has been selected.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
konsti 2025-04-22 17:36:27 +02:00 committed by GitHub
parent 45910eb6d1
commit 473d7c75a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 2 deletions

View File

@ -124,6 +124,12 @@ pub enum ResolveError {
#[source]
name_error: InvalidNameError,
},
#[error("The index returned metadata for the wrong package: expected {request} for {expected}, got {request} for {actual}")]
MismatchedPackageName {
request: &'static str,
expected: PackageName,
actual: PackageName,
},
}
impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {

View File

@ -25,7 +25,7 @@ use uv_distribution::DistributionDatabase;
use uv_distribution_types::{
BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, DistributionMetadata,
IncompatibleDist, IncompatibleSource, IncompatibleWheel, IndexCapabilities, IndexLocations,
IndexMetadata, IndexUrl, InstalledDist, PythonRequirementKind, RemoteSource, Requirement,
IndexMetadata, IndexUrl, InstalledDist, Name, PythonRequirementKind, RemoteSource, Requirement,
ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef,
};
use uv_git::GitResolver;
@ -2261,12 +2261,32 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.boxed_local()
.await?;
if let MetadataResponse::Found(metadata) = &metadata {
if &metadata.metadata.name != dist.name() {
return Err(ResolveError::MismatchedPackageName {
request: "distribution metadata",
expected: dist.name().clone(),
actual: metadata.metadata.name.clone(),
});
}
}
Ok(Some(Response::Dist { dist, metadata }))
}
Request::Installed(dist) => {
let metadata = provider.get_installed_metadata(&dist).boxed_local().await?;
if let MetadataResponse::Found(metadata) = &metadata {
if &metadata.metadata.name != dist.name() {
return Err(ResolveError::MismatchedPackageName {
request: "installed metadata",
expected: dist.name().clone(),
actual: metadata.metadata.name.clone(),
});
}
}
Ok(Some(Response::Installed { dist, metadata }))
}
@ -2369,6 +2389,13 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
// Emit a request to fetch the metadata for this version.
if self.index.distributions().register(candidate.version_id()) {
let dist = dist.for_resolution().to_owned();
if &package_name != dist.name() {
return Err(ResolveError::MismatchedPackageName {
request: "distribution",
expected: package_name,
actual: dist.name().clone(),
});
}
let response = match dist {
ResolvedDist::Installable { dist, .. } => {

View File

@ -17,7 +17,6 @@ use wiremock::{
#[cfg(feature = "git")]
use crate::common::{self, decode_token};
use crate::common::{
build_vendor_links_url, download_to_disk, get_bin, uv_snapshot, venv_bin_path,
venv_to_interpreter, TestContext,
@ -11108,3 +11107,37 @@ fn pep_751_multiple_sources() -> Result<()> {
Ok(())
}
/// Test that uv doesn't hang if an index returns a distribution for the wrong package.
#[tokio::test]
async fn bogus_redirect() -> Result<()> {
let context = TestContext::new("3.12");
let redirect_server = MockServer::start().await;
// Configure a bogus redirect where for all packages, anyio is returned.
Mock::given(method("GET"))
.respond_with(
ResponseTemplate::new(302).insert_header("Location", "https://pypi.org/simple/anyio/"),
)
.mount(&redirect_server)
.await;
uv_snapshot!(
context
.pip_install()
.arg("--default-index")
.arg(redirect_server.uri())
.arg("sniffio"),
@r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The index returned metadata for the wrong package: expected distribution for sniffio, got distribution for anyio
"
);
Ok(())
}