Why now

On 2026-10-27, Astral's William Woodruff published tar-codec: a fast and secure tar parser for the Rust ecosystem and announced that starting with uv 0.13, source distributions (sdists, which are tar archives) are parsed by a new in-house parser, tar-codec. It is about twice as fast as the previous astral-tokio-tar, but the post is really about how to eliminate parser differentials. On Lobsters it drew 93 points and 15 comments, with debate over whether compatibility and differential-avoidance are fundamentally at odds, and whether you could define a "minimum tar" from real PyPI data (about 9.86 million tar.gz files) and accept only that (Lobsters thread).

The backdrop is TARmageddon (CVE-2025-62518) from October 2025: forks in the async-tar lineage (tokio-tar and others) mis-advanced the stream when PAX and UStar sizes disagreed, so entries of a hidden inner tar were extracted as entries of the outer archive (Edera's write-up). On top of that, Harmen Stoppels' tar-minefield lined up how implementations behave across 81 cases and made it visible that nearly every implementation diverges somewhere. The subject here is not tar itself but a design paradigm: reading an ambiguous format so that you never disagree with other implementations.

The problem: what is a parser differential?

A parser differential is when two parsers interpret the same input differently. As a Lobsters commenter pointed out, the term "parse tree differential attack" comes from the 2011 paper The Halting Problems of Network Stack Insecurity (Sassaman et al.), and the PKI Layer Cake (2010) is a well-known precedent.

The danger arises when the side that checks and the side that uses run different parsers:

The post distinguishes two kinds:

Crucially, this is not a memory-safety problem. Writing it in Rust does not help if your reading of the spec drifts. Conversely, the mitigations discussed here apply to implementations in any language.

How it works: why tar breeds differentials

1. A block format with holes you can fill

tar is a sequence of fixed 512-byte blocks with no global header. Each member is a header block followed by enough content blocks for its size, the last one padded. Nothing specifies what goes in the padding, so you can put non-NUL data there. That means if the read position slips by one block, attacker-prepared data is read as a header. Most differentials eventually come down to this desynchronization.

2. Three (and a half) format families coexist