Skip to content

Drop the zero padding of tar names that end in incomplete UTF-8 - #232

Merged
ericmj merged 1 commit into
mainfrom
ericmj/tar-parse-string-padding
Oct 4, 2026
Merged

ericmj merged 1 commit into
mainfrom
ericmj/tar-parse-string-padding

Conversation

@ericmj

@ericmj ericmj commented Oct 4, 2026

Copy link
Copy Markdown
Member

hex_erl_tar's parse_string/1 reads a header field up to the first NUL and decodes it as UTF-8. When a name ends partway through a UTF-8 sequence, the {incomplete, ...} branch falls back to binary_to_list/1 on the whole field instead of the part before the NUL, so the name keeps the field's zero padding. A package whose inner tarball has the ustar name café in ISO Latin-1 (last byte 16#E9) unpacked to a 100 character name: "caf", 233 and 96 zeros. hex_tarball never writes such names, since non-ASCII names go into PAX headers, so this only affects tarballs built by other tools.

The fallback now converts only the bytes before the NUL, listed as change 9 in the hex_erl_tar header. The same fix is up for OTP in erlang/otp#11764.

The new test builds a package, rewrites the inner tarball's first ustar name to those bytes, recomputes CHECKSUM and checks the name hex_tarball:unpack/2 returns.

hex_erl_tar's parse_string/1 reads a header field up to the first NUL
and decodes it as UTF-8. When a name ended partway through a UTF-8
sequence, it fell back to the bytes of the whole field instead of the
part before the NUL, so the name kept the zero padding. A package whose
inner tarball has the ustar name "café" in ISO Latin-1 unpacked to a 100
character name: "caf", 233 and 96 zeros.

The fallback now converts only the bytes before the NUL. The same fix is
proposed for OTP in erlang/otp#11764.
@ericmj
ericmj merged commit 7024b57 into main Oct 4, 2026
10 checks passed
@ericmj
ericmj deleted the ericmj/tar-parse-string-padding branch October 4, 2026 14:35
ericmj added a commit to hexpm/hex that referenced this pull request Oct 6, 2026
This brings in hexpm/hex_core#232,
hexpm/hex_core#233,
hexpm/hex_core#234 and
hexpm/hex_core#236:

  * Repository credentials are resolved without the :global lock
    unless an OAuth token has to be exchanged or refreshed. Every
    registry and tarball request took the lock, and requests that lost
    the race retried after random sleeps that grew exponentially.
  * Unpacking writes files and file metadata with raw file operations
    instead of through the file server process.
  * The temporary directory used while unpacking is created inside the
    output directory, or in the system temporary directory when there's
    no output directory, instead of in the current working directory.
  * Tar names that end partway through a UTF-8 sequence are no longer
    padded with zeros.

mix deps.get for a project with the 100 Hex packages in hexpm's
mix.lock went from 2.72 s to 2.15 s, the median of 15 interleaved runs
with an empty Hex cache and no deps directory, on macOS against
repo.hex.pm.
ericmj added a commit to hexpm/hex that referenced this pull request Oct 6, 2026
* Update vendored hex_core to 4fbc5d1

This brings in hexpm/hex_core#232,
hexpm/hex_core#233,
hexpm/hex_core#234 and
hexpm/hex_core#236:

  * Repository credentials are resolved without the :global lock
    unless an OAuth token has to be exchanged or refreshed. Every
    registry and tarball request took the lock, and requests that lost
    the race retried after random sleeps that grew exponentially.
  * Unpacking writes files and file metadata with raw file operations
    instead of through the file server process.
  * The temporary directory used while unpacking is created inside the
    output directory, or in the system temporary directory when there's
    no output directory, instead of in the current working directory.
  * Tar names that end partway through a UTF-8 sequence are no longer
    padded with zeros.

mix deps.get for a project with the 100 Hex packages in hexpm's
mix.lock went from 2.72 s to 2.15 s, the median of 15 interleaved runs
with an empty Hex cache and no deps directory, on macOS against
repo.hex.pm.

* Unpack packages in parallel while they are fetched

Mix calls Hex.SCM.update/1 for one dependency at a time, and each call
unpacked the package from the cache into deps/<app>. For the 100 Hex
packages in hexpm's mix.lock that was about 750 ms of serial unpacking
after the downloads had finished.

The fetch job started by Hex.SCM.prefetch/1 now also unpacks the
package into deps/.<app>.hex-staging. update/1 verifies the checksums
of the unpacked package against the registry, removes deps/<app> and
renames the staging directory into its place. deps/<app> is still only
changed by update/1, so Mix writes mix.lock after the same steps as
before. If the job fails to unpack the package, update/1 unpacks it
into the staging directory again to report the error.

Since the checksums are verified before deps/<app> is removed, a
package that fails to unpack or doesn't match the registry no longer
removes the previously fetched dependency.

This depends on the vendored hex_core creating its temporary unpack
directory inside the output directory. With a relative temporary
directory, Mix changing the working directory to load a rebar
dependency while a job unpacked left temporary directories in other
dependencies.

mix deps.get for the 100 Hex packages in hexpm's mix.lock went from
2.10 s to 1.61 s, the median of 15 interleaved runs with an empty Hex
cache and no deps directory, on macOS against repo.hex.pm. Peak RSS
went from 222 MB to 227 MB.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant