fix(vendor): verify the extracted tree after a service-download, not just the archive bytes#122
Merged
Mikola Lysenko (mikolalysenko) merged 1 commit intoJul 10, 2026
Conversation
…just the archive bytes The extract-into-dir service paths (composer, cargo, golang, gem) verified only the downloaded archive's sha512 SRI, then synthesized a success ApplyResult from `record.files` alone — never checking that the patched files actually landed at their recorded paths on disk. A self-consistent archive whose internal layout differs from the assumed single wrapper dir (an extra `pkg-<sha>/` level, or a root-level `src/…` that the single-level strip over-strips) extracts "successfully" with every file at the WRONG path, and `vendor` exits 0 with a green envelope over an effectively empty copy dir. That is the exit-0-empty-copy shape behind the flaky composer e2e (depscan run 29040958337): the assertion downstream only tripped one line later on a bare ENOENT, with the envelope and on-disk tree unrecoverable. Each backend now runs copy_matches_after_hashes(copy_dir, &record.files) immediately after extraction (composer/cargo before the success return; golang before wiring the consumer go.mod; gem after the stub gemspec is written). On mismatch it routes through the backend's existing `miss` closure: `auto` falls back to the local build (loud warning `vendor_prebuilt_layout_mismatch`), `--vendor-source=service` hard-fails rather than shipping a wrong copy. npm/pypi are unaffected — they persist a single integrity-verified packed artifact (.tgz/.whl), not an extracted tree, so they have no layout-strip step. Regression tests (composer_lock): a double-wrapper zip that the single-level strip misplaces now (a) hard-fails under service mode with the copy absent at its recorded path, and (b) falls back to a correct local build under auto. Both were green-exit-empty-copy before this fix. Assisted-by: Claude Code:claude-fable-5
Jeppe Hasseriis (cenobitedk)
approved these changes
Jul 10, 2026
Jeppe Hasseriis (cenobitedk)
left a comment
There was a problem hiding this comment.
LGTM, but I'm not an expert in rust, so I might have overlooked important details
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The extract-into-dir service-download paths (composer, cargo, golang, gem) verified only the downloaded archive's sha512 SRI, then reported success synthesized from
record.filesalone — they never checked that the patched files actually landed at their recorded paths on disk. This adds a post-extraction disk verification so a wrong internal layout fails closed (fallback underauto, hard-fail under--vendor-source=service) instead of shipping an empty/misplaced copy withvendorexiting 0.The bug
extract_zip(..., strip_first=true)(and the golang/cargo/gem equivalents) assume the archive has exactly one wrapper directory. A prebuilt dist zip whose layout differs — an extra wrapper (outer/pkg-<sha>/src/…→ single strip leavespkg-<sha>/src/…) or a root-levelsrc/…(single strip over-strips toLoggerInterface.php) — extracts "successfully" with every file at the wrong path. TheUsedarm then maps toalready_patched_result(purl, ©_dir, &record.files), which builds a success result from the record keys without readingcopy_dir. Net:vendor --jsonexits 0, envelopestatus:success/failed:0, and the copy dir is missing its patched files. Archive-bytes SRI verification (fetch_verified_archive) can't catch this — a self-consistent archive with the wrong internal shape passes it.This is the root fail-open behind depscan's flaky
94_socket-patch-vendor-composere2e (run 29040958337): the CLI reported success and the test tripped one line later on a bare ENOENT.Fix
After extraction, each backend calls the existing shared
copy_matches_after_hashes(copy_dir, &record.files)and, on mismatch, routes through its ownmissclosure:go.mod(so nothing points at a bad copy).record.files, so it isn't part of the check).auto→ warnvendor_prebuilt_layout_mismatch+ fall back to the local build;--vendor-source=service→ hard-fail (no silent wrong copy). npm/pypi are intentionally untouched — they persist a single integrity-verified packed artifact (.tgz/.whl), not an extracted tree, so there's no layout-strip step to get wrong.Tests
Two new
composer_lockregression tests build a double-wrapper zip that the single-level strip misplaces:service_wrong_layout_service_mode_hard_fails: refuses (copy absent at the recorded path) instead of synthesizing success.service_wrong_layout_auto_falls_back_to_build: falls back to a correct local build, with thevendor_prebuilt_layout_mismatchwarning recorded.Both were green-exit-empty-copy before the fix.
cargo test -p socket-patch-core --lib vendor::(546 tests) + clippy pass.Pairs with
depscan PR fix/composer-vendor-e2e-flake, which independently hardens the e2e assertion (a zero-event vendor no longer passes as success) and captures the envelope + on-disk tree on failure, plus makes the
patches/viewendpoint fail closed on an unreadable blob.