install: fail extraction when libarchive discards a damaged tar header - #39724
install: fail extraction when libarchive discards a damaged tar header#39724robobun wants to merge 1 commit into
Conversation
libarchive discards a 512-byte header block whose checksum does not match, returns ARCHIVE_RETRY from archive_read_next_header, and resyncs on the next intact header. Every header loop in bun treated that status as "call again", so the member behind the damaged header was left out of the extracted tree and the command reported success. Buffered readers (Archiver::extract_to_dir, get_overwriting_file_list, ArchiveIterator) read from memory, where ARCHIVE_RETRY has no other meaning, so they now return the same error as ARCHIVE_FATAL. The streaming extractor also receives ARCHIVE_RETRY when its read callback runs out of input. It now counts the headers it is handed and compares them with archive_file_count(), which libarchive advances for every header it starts, including a discarded block, at each header and at EOF. A gap fails the extraction. The two tests that asserted the skip are replaced by tests that assert the failure on both extractors, plus one that streams intact pax 'g', pax 'x' and GNU 'L' headers in 64-byte chunks. Bun.Archive.extract() and bun publish <tarball> get a case each, since they share the changed loops.
|
Warning Review limit reachedYour included review limit has been reached. You’re in a promotional period — use the checkbox below to run this review for free:
On-demand reviews are free for the next 31 days. After that, they cost $0.25 per reviewed file. How can I continue?Run this review now using the option above, or comment You can also wait for the limit to reset (next review available in 3 minutes), then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
Comment |
|
Status: ready for review. Reproduced on bun 1.4.0 with a Running a self-review of the diff next. CI results will show up below. |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes package-extraction behavior (corrupted tarballs now fail instead of silently dropping members), replaces two existing regression tests, and the streaming-path fix depends on the archive_file_count() / read_header_in_progress interaction in the Bun-specific libarchive patch, a maintainer look would still be worthwhile.
What was reviewed:
- Verified in
patches/libarchive/nonblocking-read.patchthatfile_countis incremented inside the!read_header_in_progressguard and that the damaged-block RETRY clears the flag, so the header-count check inTarballStreamdistinguishes yields from discarded blocks as described. - Checked the three buffered header loops now treating
Retryas fatal all useread_open_memory(blocking source), so the onlyRetrythere is the discarded-block case. - Confirmed the two deleted tests' underlying invariants (state reset across retry, input buffer alive across it) are still driven by the new streaming failure tests, and the extension-header test guards against false positives from pax 'g'/'x' and GNU 'L' headers.
Extended reasoning...
Overview
This PR changes how bun install, Bun.Archive.extract(), bun publish <tgz>, and bun pm diff handle a tarball whose member header has a bad checksum. libarchive discards such a block and returns ARCHIVE_RETRY; both extractors previously treated that as "call again", so the install succeeded with the damaged member silently missing. The fix makes the buffered header loops (src/libarchive/lib.rs) treat Retry as Fatal, and adds a header-count comparison in the streaming extractor (src/install/TarballStream.rs) since its read callback also uses Retry for out-of-input yields. Six new test cases cover both paths plus a false-positive guard; two existing tests that asserted the old skip behavior are replaced.
Security risks
This is security-adjacent: package extraction integrity. The change is strictly defensive — it turns a silent drop into a hard failure, matching bsdtar/GNU tar. No new attack surface. The main risk would be a false positive (rejecting a valid tarball), which the extension-header test and the PR's out-of-tree probe address. I traced the archive_file_count() semantics through the vendored patch and they match the PR's claim: yields count once via read_header_in_progress, discarded blocks count separately.
Level of scrutiny
High. Package installation is a critical path, the streaming fix leans on a subtle invariant of a Bun-local libarchive patch, and two existing regression tests are deleted. Per the repo's review guidelines, deleted tests need a stated reason (provided) and the replacement must keep asserting the property the original protected — the PR argues the new streaming failure tests do this by still driving libarchive past the discarded block while more body arrives, but a maintainer should confirm that judgment.
Other factors
The PR is unusually well-documented (mechanism, false-positive probe across GNU/npm/bun-packed tarballs at multiple chunk sizes, related suites re-run). No bugs surfaced from the multi-agent review. The ArchiveIterator::next() change fans out to multiple consumers (bun publish, bun pm diff, Bun.Archive glob/count loops per the PR notes), which is another reason a maintainer should sign off on the behavioral shift rather than auto-approving.
|
Two facts for the points the automated review left to a maintainer. The replaced tests. The #37669 property (the input buffer stays alive while libarchive reads past a discarded block) is still exercised: the two streaming failure tests feed the body in 4 KiB chunks with a drain per chunk, so The iterator. |
Problem
bun installof a tarball with one corrupt byte in a member's tar header exits 0. The installed package lacks that member.ARCHIVE_RETRY. Both extractors took that as "call again":src/libarchive/lib.rs:1441(buffered) andsrc/install/TarballStream.rs:545(streaming).Fix
ARCHIVE_RETRYhas only this meaning. They now fail likeARCHIVE_FATAL.bun create,Bun.Archive.extract(),bun publish <tgz>andbun pm diffshare them.ARCHIVE_RETRY. So that extractor compares the headers it received witharchive_file_count()at each header and at EOF. libarchive counts a discarded block as a started header, so a gap means a dropped member.file:, git, GitHub and URL tarballs have no integrity value on the first install, so extraction is the only check, and bun already fails on every other corruption. bsdtar and GNU tar also exit non-zero (npm warns, see Notes).test/cli/install/bun-install-streaming-extract.test.ts(6 new cases, 5 fail on the released bun),test/js/bun/archive.test.ts,test/cli/install/bun-publish.test.ts.Background
ARCHIVE_RETRY, and the caller decides.patches/libarchive/nonblocking-read.patchlets the streaming read callback returnARCHIVE_RETRYwhen no bytes are buffered yet. Such a yield and its resume incrementarchive_file_count()once (read_header_in_progressinarchive_read.c).Notes
Source: fuzz ledger entry 16545 (14 of 166 mutation cells installed silently, on bun 1.3.14 and 1.4.0). GNU tar on the same bytes prints "Skipping to next header" and exits 2. The other header corruptions in the ledger hit
package.json: that member was dropped too, and the install then failed because the extracted package had nopackage.json.Repro used for this report:
bun pm packa package withbin.jsandindex.js, gunzip, flip one byte at offset 104, 134, or 150 of thepackage/bin.jsheader, gzip, install asfile:. On bun 1.4.0: exit 0,node_modules/pkholdsindex.jsandpackage.jsononly.bun publish pk.tgzlisted two members and went on to the auth step. npm 11 installs it and printsnpm warn tar TAR_ENTRY_INVALID checksum failurefour times. The extractor has no warning channel, so warn-and-continue would be a new mechanism for one corruption out of many that already fail.Why the streaming path does not fail on the
Retryitself: when the read callback runs dry, the gzip filter returns the output it already has, and the tar reader can hit the damaged block in that same call. So neither the callback state (archive_holds_reading) nor the error string (a paxWARNfrom earlier in the same member can still be set at a yield) tells the two cases apart.archive_file_count()does, one header later: upstream libarchive increments it for every header it starts, a discarded block included, and the patch keeps a yield plus its resume at one increment. The data blocks of the dropped member are scanned as headers in the meantime, as before this change; the result is the same failure.False positive probe: tarballs made by GNU tar (
--format=pax, pax with a global header,--format=gnu, default),npm pack, andbun pm pack, each with pax and long-name members, streamed in 7-, 512-, and 4096-byte chunks withBUN_INSTALL_STREAMING_DRAIN_THRESHOLD=1, all install. The pax global header variant also passed with 1-byte chunks. The committed 64-byte-chunk test keeps a small version of this.Left as is:
Bun.Archive's ownfiles(),count, and glob loops stop at any status other than OK/WARN,FATALincluded, and report what they have. That predates this change.readTarballinbun:internal-for-testingreads tarballs bun wrote itself. The temp directory a failed extraction leaves behind is the subject of #36542.Suites run on the debug build: the three files above in full, plus
test/cli/install/symlink-path-traversal.test.ts,GHSA-pfwx-36v6-832x.test.ts,bun-pm-diff.test.ts,bun-pack.test.ts.cargo clippyonbun_libarchiveandbun_installis clean.no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-publish.test.ts test/js/bun/archive.test.ts