Working draft

Tar file extraction test suite

This project provides an extensive test suite and documentation for tar file extraction, with the goal of ensuring that implementations extract tar archives the same way.

There are many implementations of tar, from command line tools to libraries for programming languages. Because the tar file format leaves room for interpretation, different implementations can extract certain crafted tar files in different ways.

This is a security concern for file exchange: for example, when a security scanner and the extractor use different tar implementations, the scanner could inspect other files than the ones that end up on disk.

By assigning identifiers to these ambiguities, and providing test cases and usage data, this project aims for tar implementations to converge on strict parsing and identical behavior.

This project proposes a default policy that rejects the vast majority of ambiguities in tar files that could make implementations extract different files, while ensuring that nearly every source tarball already published can still be extracted. Of the 82,586 source tarballs of Spack, nixpkgs, Guix and live-bootstrap, and samples of PyPI, crates.io and RubyGems, it accepts 99.77%.

Comparison of different tar implementations

This table shows how different tar implementations behave on each test case. A green ✓ is the strict behavior that the default policy asks for; click a row for details about the case.

What implementations make of every case of the test suiteGNU tarbsdtarBusyBox tar7-ZipPython tarfile, filter=data, as in pipPython tarfile, filter=tarPython tarfile, filter=fully_trustednode-tarnode-tar, stricttar-fsJava Commons Compress.NET System.Formats.TarPHP PharData, as in ComposerGo containerd archive.ApplyRust tarRust astral-tokio-tarRust tar-codec, as in uvRubyGemsR untar, internalPerl Archive::TarHaskell tarJulia Tar.jllive-bootstrap untarlive-bootstrap untar, --non-strictall 78 casesGNU tar: 26 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept26GNU tar: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10GNU tar: 42 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it42bsdtar: 28 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept28bsdtar: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10bsdtar: 40 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it40BusyBox tar: 15 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept15BusyBox tar: 9 of 78, accepts a tolerated case and extracts its tree, which its verdict allows9BusyBox tar: 53 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it53BusyBox tar: 1 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on it7-Zip: 19 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept197-Zip: 9 of 78, accepts a tolerated case and extracts its tree, which its verdict allows97-Zip: 49 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it497-Zip: 1 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on itPython tarfile, filter=data, as in pip: 25 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept25Python tarfile, filter=data, as in pip: 9 of 78, accepts a tolerated case and extracts its tree, which its verdict allows9Python tarfile, filter=data, as in pip: 1 of 78, rejects a tolerated case, which its verdict allows tooPython tarfile, filter=data, as in pip: 43 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it43Python tarfile, filter=tar: 24 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept24Python tarfile, filter=tar: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10Python tarfile, filter=tar: 44 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it44Python tarfile, filter=fully_trusted: 22 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept22Python tarfile, filter=fully_trusted: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10Python tarfile, filter=fully_trusted: 43 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it43Python tarfile, filter=fully_trusted: 3 of 78, writes outside the destinationnode-tar: 14 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept14node-tar: 9 of 78, accepts a tolerated case and extracts its tree, which its verdict allows9node-tar: 54 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it54node-tar: 1 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on itnode-tar, strict: 25 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept25node-tar, strict: 9 of 78, accepts a tolerated case and extracts its tree, which its verdict allows9node-tar, strict: 1 of 78, rejects a tolerated case, which its verdict allows toonode-tar, strict: 43 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it43tar-fs: 26 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept26tar-fs: 8 of 78, accepts a tolerated case and extracts its tree, which its verdict allows8tar-fs: 2 of 78, rejects a tolerated case, which its verdict allows tootar-fs: 42 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it42Java Commons Compress: 23 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept23Java Commons Compress: 9 of 78, accepts a tolerated case and extracts its tree, which its verdict allows9Java Commons Compress: 45 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it45Java Commons Compress: 1 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on it.NET System.Formats.Tar: 45 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept45.NET System.Formats.Tar: 8 of 78, accepts a tolerated case and extracts its tree, which its verdict allows8.NET System.Formats.Tar: 2 of 78, rejects a tolerated case, which its verdict allows too.NET System.Formats.Tar: 23 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it23PHP PharData, as in Composer: 18 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept18PHP PharData, as in Composer: 7 of 78, accepts a tolerated case and extracts its tree, which its verdict allows7PHP PharData, as in Composer: 1 of 78, rejects a tolerated case, which its verdict allows tooPHP PharData, as in Composer: 49 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it49PHP PharData, as in Composer: 3 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on itGo containerd archive.Apply: 26 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept26Go containerd archive.Apply: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10Go containerd archive.Apply: 42 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it42Rust tar: 22 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept22Rust tar: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10Rust tar: 46 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it46Rust astral-tokio-tar: 31 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept31Rust astral-tokio-tar: 9 of 78, accepts a tolerated case and extracts its tree, which its verdict allows9Rust astral-tokio-tar: 1 of 78, rejects a tolerated case, which its verdict allows tooRust astral-tokio-tar: 37 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it37Rust tar-codec, as in uv: 55 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept55Rust tar-codec, as in uv: 5 of 78, accepts a tolerated case and extracts its tree, which its verdict allowsRust tar-codec, as in uv: 5 of 78, rejects a tolerated case, which its verdict allows tooRust tar-codec, as in uv: 13 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it13RubyGems: 19 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept19RubyGems: 7 of 78, accepts a tolerated case and extracts its tree, which its verdict allows7RubyGems: 2 of 78, rejects a tolerated case, which its verdict allows tooRubyGems: 47 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it47RubyGems: 3 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on itR untar, internal: 17 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept17R untar, internal: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10R untar, internal: 51 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it51Perl Archive::Tar: 19 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept19Perl Archive::Tar: 8 of 78, accepts a tolerated case and extracts its tree, which its verdict allows8Perl Archive::Tar: 1 of 78, rejects a tolerated case, which its verdict allows tooPerl Archive::Tar: 48 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it48Perl Archive::Tar: 2 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on itHaskell tar: 32 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept32Haskell tar: 6 of 78, accepts a tolerated case and extracts its tree, which its verdict allowsHaskell tar: 3 of 78, rejects a tolerated case, which its verdict allows tooHaskell tar: 36 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it36Haskell tar: 1 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on itJulia Tar.jl: 38 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept38Julia Tar.jl: 10 of 78, accepts a tolerated case and extracts its tree, which its verdict allows10Julia Tar.jl: 30 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it30live-bootstrap untar: 19 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept19live-bootstrap untar: 8 of 78, accepts a tolerated case and extracts its tree, which its verdict allows8live-bootstrap untar: 40 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it40live-bootstrap untar: 8 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on it8live-bootstrap untar: 3 of 78, writes outside the destinationlive-bootstrap untar, --non-strict: 10 of 78, as the verdict says: rejects a case to reject, extracts the tree of a case to accept10live-bootstrap untar, --non-strict: 8 of 78, accepts a tolerated case and extracts its tree, which its verdict allows8live-bootstrap untar, --non-strict: 49 of 78, accepts a case to reject, which a strict parser rejects, or crashes on it49live-bootstrap untar, --non-strict: 8 of 78, rejects a case to accept, or extracts another tree than that of the case, or crashes on it8live-bootstrap untar, --non-strict: 3 of 78, writes outside the destinationto acceptF17pax-global-comment✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✗F31pax-xattr-not-utf8✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✗N01gnu-long-name✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✓✓✓✓✗✗N02name-fills-field✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✗N03pax-long-name✓✓✓✓✓✓✓✓✓✓✓✓✗✓✓✓✓✗✓✗✓✓✗✗N04ustar-prefix✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✗S01tree-dir-after-contents✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓S02tree-duplicate-dir✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓toleratedF01number-leading-space++++++++++++++++−+++++++F02end-data-after+++++++++−++++++++++−+++F05magic-version-nuls+++++++++++−+++−−+++−+++F14pax-size-disagrees++✗+++++++++✗++++✗+✗✗+✗✗F18pax-global-comment-after-members++++++++++++++++++++++✗✗F32checksum-leading-space++++++++++++++++−+++++++N05name-dot++++++++++++−++++−++++++N06name-leading-dot-slash++++++++++++++++++++++++N07symlink-absolute+++✗−++✗−−✗−✗+++−−+−−+++N15name-control-character++++++++++++++++−+++++++to reject: framingF03end-lone-zero-block!!!!!!!!!!!!!!!!✓!!!✓!!!F04end-missing!!!✓!!!!!!!!!!!!✓!!!✓!✓✓F06checksum-signed!!!!!!!✓✓✓!!✓!✓✓✓!!!✓✓!!F07end-archive-after!!!!!!!!!!!!!!!!!!!!✓!!!F08end-truncated-in-member✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓F09end-zero-block-then-member!!!!!!!!!!!!!✓!!✓!!!✓!!!F10extended-pax-size-before-gnu-long!!!!!!!!!!!✓!!!!✓!!!!!✓✓F11magic-odd-version!!!!!!!!!!!✓!!!!✓!!!✓✓!!F12number-size-gap!✓!!✓✓✓!!!✓✓✓✓✓✓✓✓!!!!!!F13pax-newline-in-path!!!!!!!!!!!✓!!!!✓!!!!!!!F15pax-size-on-dir!!!!!!!!!!!✓!!!!✓!!!!✓!!F16type-unknown!!✓!✓!!!!✓!✓!✓!!✓!✓!!✓!!F19pax-global-between-extended-and-member!!!!!!!!!!!✓!!!!✓!!!!!!!F20pax-global-size!✓!!!!!!!!!!!!!!✓!!!!!!!F21pax-size-empty✓!!!!!!!!!!!!!!✓✓!!!!✓!!F22pax-size-negative✓✓!!!!!!!✓✓✓!✓!✓✓!!!!✓!!F23pax-size-plus✓!!!!!!!!!!!!!!✓✓!!!!✓!!F24pax-size-leading-space✓!!!!!!!!!✓!!✓!✓✓!!!!!!!F25pax-size-not-ascii-digits✓!!!!!!!!✓!✓!✓!✓✓!!!!✓!!F26pax-size-leading-zero!!!!!!!!!!!!!!!!!!!!!!!!F27pax-size-too-large✓✓!!✓✓✓✓✓✓✓✓!✓!✓✓!!!!✓!!F28pax-newline-in-comment!!!!!!!!!!!✓!!!!!!!!!!!!F29number-size-base256-negative✓✓!✓✓✓✓!!✓✓✓✓✓✓✓✓✓✓!!✓✓✓F30number-size-base256-too-large✓✓!✓✓✓✓!✓✓✓✓✓✓!!✓✓✓!!✓✓✓F33size-on-dir!!!!!!!!!!!✓!!!!✓!!!!✓!!F34size-on-hardlink!!!!!!!!!!!✓!!!!!!!!!✓✓!F35pax-size-on-hardlink!!!!!!!!!!!✓!!!!!!!!!✓✓!F36pax-record-length-short✓✓!!✓✓✓!!!✓✓!✓!✓✓!!!!✓!!to reject: namesN08extended-pax-path-and-gnu-long-name!!!!!!!!!!!!!!!✓✓!!!✓!✓✓N09extended-two-pax-headers!✓!!!!!!!!!✓!!✓✓✓!!!✓!!!N10format-star!!!!!!!!!!!!!!!!!!!✓!!!!N11gnu-prefix-field!!!!!!!!!!!!!!!!!✓!!!!!!N12name-absolute!!!!!!↑!✓!✓✓!!!!✓✓!✓✓✓↑↑N13name-backslash!!!!!!!!!!!!!!!!✓!!!!!!!N14name-bytes-after-nul!!!!!!!!!!!!!!!!!✓!!!!!!N16name-dotdot✓✓!!✓✓↑!✓!✓✓!!!!✓✓✓✓✓✓↑↑N17name-empty✓✓✓!✓✓✓✓✓✓✓✓✓!!!✓✓✓✓✓✓!!N18name-not-utf8!!!!!!!!!!!!!!!!✓!!!!!✓✓N19pax-nul-in-path!!!!✓✓✓!!✓✓!!✓✓✓✓!!!✓✓!!N20pax-repeated-key!!!!!!!!!!!✓!!!!✓!!!!!!!N21pax-global-path!✓!!!!!!!!!!!!!!✓!!!!!!!N22pax-path-empty!!✓!✓✓✓!✓!!✓!!!✓✓!✓!✓✓!!N23extended-gnu-long-then-pax!!!!!!!!!!!!!!!!✓!!!!!✓✓N24pax-hdrcharset-binary!!!!!!!!!!!!!!!!✓!!!!!!!N25symlink-empty-target✓✓!!!!!✓✓!!✓!✓✓✓✓✓!✓✓✓✓!N26pax-global-path-then-empty!✓!!✓✓✓!!!!!!!!!✓!!!!✓!!to reject: kindsK01kind-trailing-slash-file!!!!!!!!!✓!✓!!!!✓!!!✓✓!!K02linkname-on-file!!!!!!!✓✓!!!!!!!!!!!!✓!!K03symlink-trailing-slash!!!!!!!!!✓!✓!!!!✓!!✓✓✓✓!to reject: order and stateS03hardlink-missing-target✓✓✓✓✓✓✓!✓✓!✓✓✓✓✓✓!!✓✓✓✓!S04hardlink-to-itself!✓!✓!!!!✓✓!✓!✓✓✓✓!!!!!✓!S05hardlink-to-later-member✓✓!!✓✓✓!✓✓!✓✓✓✓✓✓!!!!✓✓!S06hardlink-to-symlink!!!✓!!!!✓!!!!!!!✓!!!✓✓✓!S07tree-duplicate-file-differs!!!!!!!!!!!✓!!!!!!!!!!!!S08tree-duplicate-file-dir!!!✓✓✓✓!!✓✓✓!!✓✓!✓!✓✓!!!S09tree-duplicate-file-identical!!!!!!!!!!!✓!!!!!!!!!!!!S10tree-symlink-then-file!!!✓!!!!!!!✓!!!!!!!!✓!!!S11tree-through-symlink✓✓✓✓✓✓↑!✓✓✓✓✓!✓✓✓✓✓✓✓✓↑↑S12tree-under-file✓✓✓✓✓✓✓!✓✓✓✓✓✓✓✓✓✓✓✓✓!!!S13hardlink-empty-target✓✓!!!!!!✓!!✓✓✓✓✓✓!!✓✓!✓!✓as the verdict says: rejects a case to reject, extracts the tree of a case to accept+accepts a tolerated case and extracts its tree, which its verdict allows−rejects a tolerated case, which its verdict allows too!accepts a case to reject, which a strict parser rejects, or crashes on it✗rejects a case to accept, or extracts another tree than that of the case, or crashes on it↑writes outside the destination

Each case has a permanent identifier, such as F15, whose letter is its category:

A case's page shows its archive block by block, what every implementation makes of it, and how common it is in release tarballs. An implementation's page, linked from its name above the matrix, lists the cases where it does not do as the policy says.

Policy

This section defines a working draft of the default policy1, which accepts, tolerates or rejects each case below, based on data from the 82,586 distinct release tarballs of Spack, nixpkgs, Guix and live-bootstrap, and samples of PyPI, crates.io and RubyGems.

The prevalence column is the share of these tarballs that have a case: a first-order approximation of how common it is in general.

1 A policy can be stricter and also reject the tolerated cases, at the cost of rejecting more published tarballs: some tolerated cases are common, such as numbers padded with spaces (F01), and others rare. That suits special-purpose tools, such as an extractor for a single ecosystem, where it can be checked against every tarball ever published there.

To accept

identifiernamewhat it isprevalence
F17pax-global-commentThe global pax header with only a comment, as git archive writes it.not measured
F31pax-xattr-not-utf8An xattr in a pax header whose value is not UTF-8, as libarchive and star write them.0.04%
N01gnu-long-nameA name over 100 bytes, in a GNU long name.not measured
N02name-fills-fieldA name of exactly 100 bytes, which fills the name field without a NUL.9.53%
N03pax-long-nameA name over 100 bytes, in a pax path.not measured
N04ustar-prefixA long name split between the ustar prefix and name fields.7.65%
S01tree-dir-after-contentsA directory after its contents.1.22%
S02tree-duplicate-dirThe same directory twice, as tar cf x.tar d d/sub writes.0.19%

Tolerated

identifiernamewhat it isprevalence
F01number-leading-spaceNumeric fields padded with spaces rather than zeros, as old tars wrote them.0.99%
F02end-data-afterData after the end-of-archive marker that is not a header, which is ignored.0.03%
F05magic-version-nulsThe POSIX magic with NULs for its version, ustar\0\0\0, as some Java archivers write.0.07%
F14pax-size-disagreesA pax size that disagrees with the header's, so that a reader of the header's misses a member.none
F18pax-global-comment-after-membersA global pax header with only a comment, after the first member.0.10%
F32checksum-leading-spaceA checksum padded with spaces rather than zeros, as Perl's Archive::Tar writes it.0.99%
N05name-dotA member named ./, the root of the tree, as tar cf x.tar . writes first.0.08%
N06name-leading-dot-slashThe name ./a, which is the path a.0.30%
N07symlink-absoluteA symlink to an absolute path, created as it is.0.11%
N15name-control-characterA control character in a name, here a newline, created as it is.0.01%

To reject: framing

identifiernamewhat it isprevalence
F03end-lone-zero-blockAn end-of-archive marker of one zero block, not two.<0.01%
F04end-missingNo end-of-archive marker: the archive ends after a complete member.0.03%
F06checksum-signedA checksum that only matches as the signed sum of the header.none
F07end-archive-afterAnother archive after the end-of-archive marker.<0.01%
F08end-truncated-in-memberAn archive that ends inside the data of a member.not measured
F09end-zero-block-then-memberA zero block, then another member: the end of the archive, or not?<0.01%
F10extended-pax-size-before-gnu-longA pax size followed by a GNU long name: which of them does the size apply to?none
F11magic-odd-versionThe POSIX magic with a version that is neither 00 nor NULs.none
F12number-size-gapA size with a space among its digits, which reads as two sizes.none
F13pax-newline-in-pathA newline in a pax path, which split on lines is another record.none
F15pax-size-on-dirA pax size on a directory, which takes the next header for its data.0.01%
F16type-unknownAn unknown typeflag with a size: data, or no data?none
F19pax-global-between-extended-and-memberA global pax header between an extended header and its member.none
F20pax-global-sizeA global pax header with a size, which frames every member after it, or none.none
F21pax-size-emptyAn empty pax size, which in POSIX removes the size rather than giving one.none
F22pax-size-negativeA negative pax size, which a member cannot have.none
F23pax-size-plusA pax size with a plus sign, +512, which conversions of strings to numbers accept.none
F24pax-size-leading-spaceA pax size with a leading space, 512, which conversions of strings to numbers skip.none
F25pax-size-not-ascii-digitsA pax size in Arabic-Indic digits, ٥١٢, which some conversions of strings read as 512.none
F26pax-size-leading-zeroA pax size with a leading zero, 0777: 777 in decimal, as POSIX says, or 511 in octal.none
F27pax-size-too-largeA pax size of 2^64 + 512, which wraps around to 512 in 64 bits.none
F28pax-newline-in-commentA newline in a pax comment, which split on lines leaves a size record.none
F29number-size-base256-negativeA negative base-256 size in the header, -512, which a member cannot have.none
F30number-size-base256-too-largeA base-256 size of 2^64 + 512 in the header, which wraps around to 512 in 64 bits.none
F33size-on-dirA size in the header of a directory, which takes the next header for its data.none
F34size-on-hardlinkA size in the header of a hard link, in a pax archive, which takes the next header for its data.none
F35pax-size-on-hardlinkA pax size on a hard link, which takes the next header for its data.none
F36pax-record-length-shortA pax record whose length is one byte short, so that it ends just before its newline.none

To reject: names

identifiernamewhat it isprevalence
N08extended-pax-path-and-gnu-long-nameA pax path and a GNU long name for one member, and a third name in its header.none
N09extended-two-pax-headersTwo pax extended headers for one member, with different paths.none
N10format-starA ustar prefix whose end reads as star's times, with star's tar\0 at offset 508.<0.01%
N11gnu-prefix-fieldThe prefix field of a header with GNU magic, which GNU tar uses for times in incremental archives.none
N12name-absoluteAn absolute name, which points outside the directory the archive is extracted to.<0.01%
N13name-backslashA backslash in a name: a character on Unix, a separator on Windows.<0.01%
N14name-bytes-after-nulBytes after the NUL that ends a name.none
N16name-dotdotA name with .., which climbs out of the directory the archive is extracted to.<0.01%
N17name-emptyA member with an empty name.none
N18name-not-utf8A name that is not UTF-8.<0.01%
N19pax-nul-in-pathA NUL inside a pax path.none
N20pax-repeated-keyOne pax key twice in one extended header.<0.01%
N21pax-global-pathA global pax header with a path.none
N22pax-path-emptyAn empty pax path, which in POSIX removes the path rather than giving one.none
N23extended-gnu-long-then-paxA GNU long name, then a pax header, then the member: which of them does the long name apply to?none
N24pax-hdrcharset-binaryhdrcharset=BINARY, which declares a pax path that is not UTF-8 as bytes.none
N25symlink-empty-targetA symlink with an empty target.none
N26pax-global-path-then-emptyA global path, then a global header that removes it with an empty value.none

To reject: kinds

identifiernamewhat it isprevalence
K01kind-trailing-slash-fileA regular file whose name ends in a slash: a file, or a directory?<0.01%
K02linkname-on-fileA regular file with a link target.<0.01%
K03symlink-trailing-slashA symlink whose name ends in a slash.none

To reject: order and state

identifiernamewhat it isprevalence
S03hardlink-missing-targetA hardlink to a member that is not in the archive.none
S04hardlink-to-itselfA hardlink to itself.0.03%
S05hardlink-to-later-memberA hardlink to a member that comes later.none
S06hardlink-to-symlinkA hardlink to a symlink: a link to the symlink, or to its target?<0.01%
S07tree-duplicate-file-differsThe same file twice, with different contents: the first copy, or the last?not measured
S08tree-duplicate-file-dirA file, then a directory of the same name.none
S09tree-duplicate-file-identicalThe same file twice, with the same contents.0.11%
S10tree-symlink-then-fileA symlink, then a file of the same name: replace the link, or write through it?none
S11tree-through-symlinkA member written through a symlink, which points out of the destination.none
S12tree-under-fileA member under a file.<0.01%
S13hardlink-empty-targetA hardlink with an empty target.none

Data to support the default policy

The default policy accepts 99.77% of the 82,586 distinct release tarballs of Spack, nixpkgs, Guix and live-bootstrap, and samples of PyPI, crates.io and RubyGems. See the list of the 192 it rejects, and why.

ecosystemtarballsrejectedshare
Spack37,553700.19%
nixpkgs18,990780.41%
Guix10,168340.33%
PyPI10,000190.19%
crates.io5,0000none
RubyGems5,00060.12%
live-bootstrap1710none
total (distinct)82,5861920.23%

The tarballs by the median mtime of their members, a rough check that the data spans enough years, leaving out the 14,361 whose mtimes are fixed for reproducibility or missing:

Tarballs by the median mtime of their members, in bins of two years05,00010,00015,0001981–1989: 1 tarball1990–1991: 2 tarballs19901992–1993: 4 tarballs1994–1995: 18 tarballs1996–1997: 10 tarballs1998–1999: 34 tarballs2000–2001: 64 tarballs20002002–2003: 148 tarballs2004–2005: 283 tarballs2006–2007: 286 tarballs2008–2009: 662 tarballs2010–2011: 956 tarballs20102012–2013: 1,751 tarballs2014–2015: 3,847 tarballs2016–2017: 6,008 tarballs2018–2019: 8,440 tarballs2020–2021: 10,896 tarballs20202022–2023: 12,704 tarballs2024–2025: 14,107 tarballs2026–2027: 7,966 tarballs

Motivating examples

casethe archivewhat implementations make of it
N09two pax headers for one file, with path=first and path=secondGNU tar, BusyBox, 7-Zip, node-tar, tar-fs, Go, R and Julia extract second; Python and Java extract first; PHP, RubyGems, Perl and live-bootstrap extract header-name, the name in the ustar header; bsdtar, Rust, .NET and Haskell reject it
S10a symlink a to target, then a file amost replace the link with the file; Python's tarfile and live-bootstrap write through the link, to target; 7-Zip, .NET and Haskell reject it
K01a regular file named a/GNU tar, bsdtar, BusyBox, 7-Zip, node-tar, Java, Perl and live-bootstrap create a directory a; Python, Go, Rust's tar and astral-tokio-tar, PHP's PharData, RubyGems and R an empty file a; tar-fs, .NET, Haskell, Julia and tar-codec reject it
F12a size field of 000000 1000, with a spaceGNU tar, BusyBox, 7-Zip, node-tar, tar-fs, Perl, Haskell, Julia and live-bootstrap read 0, and find a second file smuggled in the data; R reads 512, and extracts it as the data of a; bsdtar, Python, Go, Rust, Java, .NET, PHP and RubyGems reject it

Contributing

This project is looking for contributions from people who work on package managers, registries and tar implementations, in the form of discussions in issues or pull requests on GitHub or by email to harmen at stoppels dot ch, so that the working draft of the default policy can become a reference.

You can also contribute by testing other implementations, or by crafting a tarball that is particularly hard to parse. A case that shows a vulnerability that is not yet disclosed is reported privately first: see SECURITY.md.

Contributors: