Check dist name to handle bogus redirect (#12917)
Some checks failed
CI / Determine changes (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / cargo shear (push) Has been cancelled
CI / typos (push) Has been cancelled
CI / mkdocs (push) Has been cancelled
CI / cargo clippy | ubuntu (push) Has been cancelled
CI / cargo clippy | windows (push) Has been cancelled
CI / cargo dev generate-all (push) Has been cancelled
CI / check windows trampoline | i686 (push) Has been cancelled
CI / cargo test | ubuntu (push) Has been cancelled
CI / cargo test | macos (push) Has been cancelled
CI / cargo test | windows (push) Has been cancelled
CI / check windows trampoline | aarch64 (push) Has been cancelled
CI / check system | alpine (push) Has been cancelled
CI / check windows trampoline | x86_64 (push) Has been cancelled
CI / test windows trampoline | i686 (push) Has been cancelled
CI / test windows trampoline | x86_64 (push) Has been cancelled
CI / check system | python on macos aarch64 (push) Has been cancelled
CI / build binary | linux libc (push) Has been cancelled
CI / build binary | linux musl (push) Has been cancelled
CI / build binary | macos aarch64 (push) Has been cancelled
CI / build binary | macos x86_64 (push) Has been cancelled
CI / build binary | windows x86_64 (push) Has been cancelled
CI / build binary | windows aarch64 (push) Has been cancelled
CI / cargo build (msrv) (push) Has been cancelled
CI / build binary | freebsd (push) Has been cancelled
CI / ecosystem test | pydantic/pydantic-core (push) Has been cancelled
CI / ecosystem test | prefecthq/prefect (push) Has been cancelled
CI / ecosystem test | pallets/flask (push) Has been cancelled
CI / smoke test | linux (push) Has been cancelled
CI / smoke test | macos (push) Has been cancelled
CI / smoke test | windows x86_64 (push) Has been cancelled
CI / smoke test | windows aarch64 (push) Has been cancelled
CI / integration test | conda on ubuntu (push) Has been cancelled
CI / integration test | deadsnakes python3.9 on ubuntu (push) Has been cancelled
CI / integration test | free-threaded on linux (push) Has been cancelled
CI / integration test | free-threaded on windows (push) Has been cancelled
CI / integration test | pypy on ubuntu (push) Has been cancelled
CI / integration test | pypy on windows (push) Has been cancelled
CI / integration test | graalpy on ubuntu (push) Has been cancelled
CI / integration test | graalpy on windows (push) Has been cancelled
CI / integration test | github actions (push) Has been cancelled
CI / integration test | free-threaded python on github actions (push) Has been cancelled
CI / integration test | determine publish changes (push) Has been cancelled
CI / integration test | uv publish (push) Has been cancelled
CI / integration test | uv_build (push) Has been cancelled
CI / check system | homebrew python on macos aarch64 (push) Has been cancelled
CI / check cache | ubuntu (push) Has been cancelled
CI / check cache | macos aarch64 (push) Has been cancelled
CI / check system | python on debian (push) Has been cancelled
CI / check system | python on fedora (push) Has been cancelled
CI / check system | python on ubuntu (push) Has been cancelled
CI / check system | python3.12 via chocolatey (push) Has been cancelled
CI / check system | python on opensuse (push) Has been cancelled
CI / check system | python on rocky linux 8 (push) Has been cancelled
CI / check system | python on rocky linux 9 (push) Has been cancelled
CI / check system | pypy on ubuntu (push) Has been cancelled
CI / check system | pyston (push) Has been cancelled
CI / check system | python on macos x86-64 (push) Has been cancelled
CI / check system | python3.10 on windows x86-64 (push) Has been cancelled
CI / check system | python3.10 on windows x86 (push) Has been cancelled
CI / check system | python3.13 on windows x86-64 (push) Has been cancelled
CI / check system | x86-64 python3.13 on windows aarch64 (push) Has been cancelled
CI / check system | windows registry (push) Has been cancelled
CI / check system | python3.9 via pyenv (push) Has been cancelled
CI / check system | python3.13 (push) Has been cancelled
CI / check system | conda3.11 on macos aarch64 (push) Has been cancelled
CI / check system | conda3.8 on macos aarch64 (push) Has been cancelled
CI / check system | conda3.11 on linux x86-64 (push) Has been cancelled
CI / check system | conda3.8 on linux x86-64 (push) Has been cancelled
CI / check system | conda3.11 on windows x86-64 (push) Has been cancelled
CI / check system | conda3.8 on windows x86-64 (push) Has been cancelled
CI / check system | amazonlinux (push) Has been cancelled
CI / check system | embedded python3.10 on windows x86-64 (push) Has been cancelled
CI / benchmarks (push) Has been cancelled

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(())
}