Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions crates/socket-patch-core/src/patch/vendor/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,24 @@ async fn cargo_service_copy(
);
}
let _ = tokio::fs::remove_file(copy_dir.join(".cargo-checksum.json")).await;
// Verify the EXTRACTED TREE, not just the archive bytes: the SRI
// proves the download is intact, but an unexpected internal
// layout (the single `{name}-{version}/` strip leaving an extra
// wrapper, or an over-strip) lands the patched files at the wrong
// paths and the caller would synthesize success from
// `record.files` while the copy is wrong. Fail closed → `auto`
// falls back to the local build. (Mirrors composer_lock.rs.)
if !copy_matches_after_hashes(copy_dir, &record.files).await {
let _ = remove_tree(copy_dir).await;
return miss(
warnings,
"vendor_prebuilt_layout_mismatch",
format!(
"prebuilt crate for {name} extracted to an unexpected \
layout (patched files absent at their recorded paths)"
),
);
}
warnings.push(VendorWarning::new(
"vendor_prebuilt_downloaded",
format!(
Expand Down
124 changes: 124 additions & 0 deletions crates/socket-patch-core/src/patch/vendor/composer_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,30 @@ async fn composer_service_copy(
format!("cannot extract the prebuilt dist zip: {e}"),
);
}
// Verify the EXTRACTED TREE, not just the archive bytes. The
// archive-bytes SRI (checked in fetch_verified_archive) proves
// the download is intact, but says nothing about whether the
// internal layout lands the patched files at the paths the
// record names: a zip with an unexpected wrapper dir (the
// single-level `strip_first` leaves an extra `pkg-<sha>/`
// segment) or a root-level `src/…` (over-stripped) extracts
// "successfully" with every file at the WRONG path. Without
// this check the caller synthesized success purely from
// `record.files` and shipped a copy missing its patched files
// (exit 0, empty copy_dir on disk). Fail closed here and let
// the `auto` source fall back to the local build.
if !copy_matches_after_hashes(copy_dir, &record.files).await {
let _ = remove_tree(copy_dir).await;
return miss(
warnings,
"vendor_prebuilt_layout_mismatch",
format!(
"prebuilt dist zip for {pkg} extracted to an \
unexpected layout (patched files absent at their \
recorded paths)"
),
);
}
warnings.push(VendorWarning::new(
"vendor_prebuilt_downloaded",
format!(
Expand Down Expand Up @@ -1501,6 +1525,106 @@ mod tests {
.any(|w| w.code == "vendor_prebuilt_downloaded"));
}

/// Wrong internal layout (double wrapper → the single-level strip
/// misplaces the patched file) must NOT be reported as success from
/// `record.files` alone. Under `service` mode it hard-fails
/// `vendor_prebuilt_layout_mismatch`; the file is not at the expected
/// path. Regression for the exit-0-empty-copy incident (run 29040958337).
#[tokio::test]
async fn service_wrong_layout_service_mode_hard_fails() {
let lock = lock_value("psr/log", "3.0.2", false);
let (dir, blobs, installed, record) = fixture(&lock).await;
let root = dir.path();
// A double wrapper: single strip_first leaves `extra/src/...`, so the
// patched file never lands at `copy_dir/src/LoggerInterface.php`.
let zip = make_dist_zip(
"outer-wrapper",
&[("extra/src/LoggerInterface.php", PATCHED)],
);
let sri = sri_sha512(&zip);
let server = wiremock::MockServer::start().await;
mount_composer_granted(&server, &sri, &zip).await;

let outcome = vendor_with_service(
root,
&blobs,
&installed,
&record,
&composer_service_cfg(&server.uri(), VendorSource::Service, false),
)
.await;
// Service mode has no fallback, so `miss()` surfaces the uniform
// `vendor_prebuilt_required` code (same as an integrity mismatch);
// the layout diagnosis rides in the detail. The point of the
// regression is that it REFUSES rather than synthesizing success —
// and the copy dir does not hold the file at its recorded path.
match outcome {
VendorOutcome::Refused { code, detail } => {
assert_eq!(code, "vendor_prebuilt_required");
assert!(
detail.contains("unexpected layout"),
"the refusal must diagnose the layout mismatch: {detail}"
);
}
other => panic!("expected a refusal, got {other:?}"),
}
assert!(
tokio::fs::metadata(root.join(copy_rel()).join("src/LoggerInterface.php"))
.await
.is_err(),
"the misplaced service copy must not be left at the recorded path"
);
}

/// Same wrong-layout archive under `auto`: the guard trips and the local
/// build takes over, producing a correct copy — success WITHOUT the bad
/// service bytes.
#[tokio::test]
async fn service_wrong_layout_auto_falls_back_to_build() {
let lock = lock_value("psr/log", "3.0.2", false);
let (dir, blobs, installed, record) = fixture(&lock).await;
let root = dir.path();
let zip = make_dist_zip(
"outer-wrapper",
&[("extra/src/LoggerInterface.php", PATCHED)],
);
let sri = sri_sha512(&zip);
let server = wiremock::MockServer::start().await;
mount_composer_granted(&server, &sri, &zip).await;

let (result, entry, warnings) = unwrap_done(
vendor_with_service(
root,
&blobs,
&installed,
&record,
&composer_service_cfg(&server.uri(), VendorSource::Auto, false),
)
.await,
);
assert!(
result.success,
"auto must fall back to the local build when the service layout \
is wrong: {:?}",
result.error
);
assert!(entry.is_some());
// The copy holds the patched bytes at the RIGHT path (from the local
// build, not the misplaced service extract).
assert_eq!(
tokio::fs::read(root.join(copy_rel()).join("src/LoggerInterface.php"))
.await
.unwrap(),
PATCHED
);
assert!(
warnings
.iter()
.any(|w| w.code == "vendor_prebuilt_layout_mismatch"),
"the fallback must record why the service copy was rejected: {warnings:?}"
);
}

/// `service` mode + integrity mismatch hard-fails, nothing extracted.
#[tokio::test]
async fn service_integrity_mismatch_service_mode_hard_fails() {
Expand Down
18 changes: 18 additions & 0 deletions crates/socket-patch-core/src/patch/vendor/gem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,24 @@ async fn gem_service_copy(
format!("cannot write the stub gemspec into the vendored dir: {e}"),
);
}
// Verify the EXTRACTED data.tar.gz tree, not just the .gem bytes: the
// SRI proves the download is intact, but an unexpected internal layout
// lands the patched files at the wrong paths and the caller would
// synthesize success from `record.files` while the copy is wrong. (The
// stub gemspec we just wrote is not in record.files, so it is not part
// of this check.) Fail closed → `auto` falls back to the local build.
// (Mirrors composer_lock.rs.)
if !copy_matches_after_hashes(copy_dir, &record.files).await {
let _ = remove_tree(uuid_dir).await;
return miss(
warnings,
"vendor_prebuilt_layout_mismatch",
format!(
"prebuilt .gem for {name} extracted to an unexpected layout \
(patched files absent at their recorded paths)"
),
);
}
warnings.push(VendorWarning::new(
"vendor_prebuilt_downloaded",
format!(
Expand Down
20 changes: 20 additions & 0 deletions crates/socket-patch-core/src/patch/vendor/golang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,26 @@ async fn go_service_redirect(
format!("cannot synthesize go.mod for the copy: {e}"),
);
}
// Verify the EXTRACTED TREE before wiring the consumer's go.mod:
// the SRI proves the zip bytes are intact, but an unexpected
// internal layout (the `{module}@{version}/` prefix strip
// mismatching) lands the patched files at the wrong paths, and
// the caller would synthesize success from `record.files` while
// the copy is wrong. Fail closed → `auto` falls back to the
// local build; do it BEFORE editing go.mod so nothing points at
// a bad copy. (Mirrors composer_lock.rs.)
if !copy_matches_after_hashes(copy_dir, &record.files).await {
teardown_failed_service_copy(project_root, base_rel, module, wired).await;
return miss(
warnings,
"vendor_prebuilt_layout_mismatch",
format!(
"prebuilt module zip for {module} extracted to an \
unexpected layout (patched files absent at their \
recorded paths)"
),
);
}
if let Err(e) =
go_mod_edit::ensure_replace_entry(project_root, module, version, base_rel, false)
.await
Expand Down
Loading