I built a source distribution with 6,000 small files in it and installed it with uv pip install. With uv 0.13.6, the median time across five runs was 2.53 seconds. With the same binary told to use the old extraction code, the median was 3.55 seconds. With the actual pre-change release, 0.12.8, it was 3.76 seconds.
That's the headline, and it's a real number from a real install, not a microbenchmark of tar extraction in isolation. The part I didn't expect going in was what happened when I fed the new backend a source distribution with a character device node baked into it. The old backend installed it anyway and silently threw the device file away. The new one refuses the whole install and tells you exactly why.
uv is Astral's Rust-based replacement for pip, venv and friends. Version 0.13.0, released on 9 October 2026, swapped the library it uses to extract .tar.gz archives from astral-tokio-tar to a newer one called tar-codec. The release notes say the new backend "applies stricter validation when reading archives" and warns that archives with hard links or unsupported tar extensions that older versions accepted may now be rejected. There's an escape hatch: setting UV_LEGACY_TAR_BACKEND=1 restores the old code path. That one environment variable made this easy to test properly, because I could hold the binary constant and flip only the backend.
The install-time number
I generated a package called bigpkg with 6,000 text files totalling 48MB uncompressed (36MB as a gzipped tarball), gave it a trivial pyproject.toml using the setuptools backend, and installed it with --no-deps into a fresh virtual environment each time, with a fresh cache directory each time. Five runs per configuration:
| Configuration | Run times (s) | Median | Fastest |
|---|---|---|---|
| uv 0.13.6, default (tar-codec) | 2.50, 2.52, 2.53, 2.61, 2.75 | 2.53s | 2.50s |
uv 0.13.6, UV_LEGACY_TAR_BACKEND=1 | 3.07, 3.46, 3.55, 3.76, 4.84 | 3.55s | 3.07s |
| uv 0.12.8 (pre-change release) | 3.21, 3.42, 3.76, 3.93, 5.12 | 3.76s | 3.21s |
That's a 29% drop in median time against the same binary with the old flag set, and 33% against the actual old release. The fastest-run comparison is less dramatic — 2.50s against 3.07s and 3.21s, around 18-22% — because the slowest outliers in the legacy runs inflate the median more than the minimum. Either way the direction is consistent across all fifteen runs: tar-codec never lost to the legacy path on this workload.
I deliberately used a package with many small files rather than one large file, because that's where tar's per-entry overhead shows up. When I first tried this with a package containing 400 files and 8MB of data, the three configurations landed within noise of each other — 0.96 to 2.6 seconds with no clear ordering. The fixed cost of spinning up a Python subprocess to run the setuptools build backend was swamping the extraction time. Scaling up to 6,000 files is what made the effect visible. If you're testing this yourself and see "no difference," that's very likely your workload being too small, not the feature being inert.
What it refuses, and what it used to do silently
The release notes mention hard links and "unsupported tar extensions." I built four adversarial archives with Python's tarfile module: one with a hard link pointing at ../../../../../../etc/passwd, one with an absolute symlink to /etc/passwd, one with a GNU sparse-file entry missing its required header, and one with a character device node.
| Archive | uv 0.13.6 default | uv 0.13.6 legacy flag | uv 0.12.8 |
|---|---|---|---|
| Hard link escaping the extraction root | rejected | rejected | rejected |
| Absolute symlink | rejected | rejected | rejected |
| Malformed sparse entry | rejected | rejected | rejected |
| Character device node | rejected | installed | installed |
Three of the four are a wash. This is the finding that argues against my own headline: the path-escape protections for hard links and symlinks aren't new in 0.13. The old backend already refused them, with almost the same wording:
# uv 0.12.8 (old backend) error: Failed to extract archive: file:///.../malicious/evil_hardlink.tar.gz Caused by: Invalid tar file Caused by: failed to unpack `.../evilpkg-0.0.1/evilpkg/escape_hardlink` Caused by: trying to unpack outside of destination path: ... Caused by: Invalid argument # uv 0.13.6 (new backend, default) error: Failed to extract archive: file:///.../malicious/evil_hardlink.tar.gz cause: Invalid tar file cause: at byte 2560: unsafe hard-link target "../../../../../../etc/passwd": escapes the destination root Same outcome, better message — the new one names the exact byte offset and says plainly what's wrong, instead of surfacing a bare OS-level "Invalid argument." But if you were hoping 0.13 closed a hard-link escape that 0.12 left open, it didn't need to: that door was already shut.
The real difference is the character device. Here's the full output of installing a package that embeds one:
$ uv pip install evil_chardev.tar.gz --no-deps error: Failed to extract archive: file:///.../malicious/evil_chardev.tar.gz cause: Invalid tar file cause: at byte 2560: cannot extract unsupported member type CharacterDevice at evilpkg-0.0.1/evilpkg/weird_device With the legacy flag, or with 0.12.8, the same archive installs cleanly: "Resolved 1 package", "Installed 1 package", no warning anywhere. I checked the resulting site-packages directory for the device node and it isn't there either — the old backend didn't create it, it just dropped the tar entry on the floor without telling anyone. The install reports success either way. That's the actual behaviour change: not new rejections of things that were dangerous, but a loud failure in place of a silent, undocumented truncation of package contents. For a package that legitimately shouldn't contain device nodes, fine. For a package that had something unusual in it for a reason, the new behaviour at least tells you your install isn't what you think it is.
What I got wrong on the way
I wanted to check a boring, legitimate use of hard links too — a package shipping the same file twice via a hard link instead of duplicating it, which is a normal thing for sdists to do. I built one and installed it with all three configurations. All three succeeded with no extraction errors, which looked like confirmation that ordinary hard links are unaffected.
Then I went looking for the actual file in the installed package and it wasn't there — neither the original nor the hard-linked copy. I'd forgotten that setuptools, without a MANIFEST.in or explicit package_data, only packages .py files into the wheel it builds from an sdist. My .txt data files were silently excluded by the build backend, nothing to do with the tar extraction step at all. The extraction succeeded; the wheel build just never looked at those files. I had to go back and read the DEBUG-level build log to confirm the hard link itself caused no extraction error, which it didn't — it's a genuine non-finding, but I almost reported it as "hard links work fine" based on output that was actually testing setuptools' packaging defaults.
What an upgrade costs you once
The 0.13.0 changelog says the cache entry format changed, so uv "may download or rebuild dependencies after upgrading." I populated a cache directory with 0.12.8, then pointed 0.13.6 at the same directory and installed the identical archive again:
DEBUG Identified uncached distribution: bigpkg @ file:///.../bigpkg-0.0.1.tar.gz Despite the exact same tarball already having been built into that cache directory seconds earlier, the new version treated it as uncached and rebuilt it. The cache directory grew from 52MB to 104MB rather than staying flat. That's a one-time cost on the first run after upgrading, not a recurring one, but it's worth knowing before you time a CI pipeline right after bumping the uv version — your first post-upgrade run won't reflect your steady-state numbers.
Two smaller checks
uv 0.13 also stabilised a change where it stops writing _virtualenv.py and _virtualenv.pth into new virtual environments on Python 3.10 and newer, matching what virtualenv itself does from 21.6.0. I confirmed it directly: uv venv with 0.12.8 on Python 3.11 produces 15 files in the environment, including those two; 0.13.6 produces 13. Two fewer files, every time, on every new environment.
I also ran eight uv pip install processes against the same big archive concurrently, sharing one cache directory, to see whether the new extraction path handles contention any worse than the old one. All eight finished successfully with no lock errors or corrupted installs, in 25 seconds total wall time. I didn't find anything interesting here — it held up — so I'm reporting that in one line rather than dressing it up as a finding.
Run it yourself
This is the core of the timing test, trimmed to the essentials. It needs uv 0.13.x and 0.12.x installed into separate locations (I used pip install "uv==0.13.6" and pip install "uv==0.12.8" into two throwaway virtualenvs) and a synthetic sdist:
# build a 6,000-file sdist called bigpkg-0.0.1.tar.gz first, then: run_install() { label=$1; uvbin=$2; extra_env=$3 for i in 1 2 3 4 5; do cachedir=$(mktemp -d); venvdir=$(mktemp -d) python3 -I -m venv "$venvdir/venv" >/dev/null 2>&1 t0=$(date +%s.%N) env $extra_env UV_CACHE_DIR="$cachedir" "$uvbin" pip install \ --python "$venvdir/venv/bin/python" ./bigpkg-0.0.1.tar.gz --no-deps -q t1=$(date +%s.%N) echo "$label run $i: $(echo "$t1 - $t0" | bc)s" rm -rf "$cachedir" "$venvdir" done } run_install "new_default" ./venv_new/bin/uv "" run_install "new_legacy" ./venv_new/bin/uv "UV_LEGACY_TAR_BACKEND=1" run_install "old_uv" ./venv_old/bin/uv "" For the character-device check, build an archive with Python's tarfile module, set a TarInfo's type to tarfile.CHRTYPE, and add it alongside a normal pyproject.toml and PKG-INFO. Installing it with and without UV_LEGACY_TAR_BACKEND=1 is enough to see both behaviours side by side.
If you're installing sdists in CI — your own private packages, vendored dependencies, anything that isn't a prebuilt wheel — upgrading to 0.13 is a straightforward win on install time, with no downside I found for ordinary archives. The one thing worth checking first is whether anything in your build pipeline relies on tar entries that aren't plain files, symlinks or regular hard links; those are now a hard failure instead of a silent gap, which is very likely what you want, but it will surface as a new error on the next build rather than a difference you'd notice by reading a diff.


