Skip to content
Open
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
1 change: 1 addition & 0 deletions crates/socket-patch-cli/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
| GoDrift::WrongReplacePath { purl, .. }
| GoDrift::ResolvedVersionMismatch { purl, .. }
| GoDrift::UnprovenSelection { purl, .. }
| GoDrift::UnprovenStaleSum { purl, .. }
| GoDrift::ReplaceTargetOnly { purl, .. } => purl.clone(),
GoDrift::OrphanReplace { module } => module.clone(),
};
Expand Down
5 changes: 5 additions & 0 deletions crates/socket-patch-cli/src/commands/vex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,11 @@ async fn generate_vex(
{version} is ignored and the build links the unpatched module",
entry.module
),
Some(Selection::Unproven) => format!(
"nothing shows the build selects {} {version}: {}",
entry.module,
socket_patch_core::vendor::go_build_list::UNPROVEN_REASON
),
Some(Selection::Unknown)
if socket_patch_core::vendor::go_workspace::resolve(&root)
!= socket_patch_core::vendor::go_workspace::GoWork::None =>
Expand Down
167 changes: 167 additions & 0 deletions crates/socket-patch-cli/tests/e2e_golang_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2215,3 +2215,170 @@ fn goflags_modfile_keeps_the_go_mod_replace_from_the_build() {

chmod_writable(tmp.path());
}

// ── a go.sum line a downgrade left behind (#1862) ──

/// A go 1.16 consumer of the upstream at `from`, downgraded to `to` by
/// `go get`, which lowers the require but keeps `from`'s go.sum lines
/// until `go mod tidy`.
fn downgraded_consumer(consumer: &Path, from: &str, to: &str, env: &[(&str, &str)]) {
std::fs::write(
consumer.join("go.mod"),
format!("module example.com/consumer\n\ngo 1.16\n\nrequire {UMOD} {from}\n"),
)
.unwrap();
for args in [vec!["mod", "tidy"], vec!["get", &format!("{UMOD}@{to}")]] {
let out = go(consumer, &args, env);
assert!(
out.status.success(),
"go {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
}
let go_mod = std::fs::read_to_string(consumer.join("go.mod")).unwrap();
assert!(go_mod.contains(&format!("{UMOD} {to}")), "{go_mod}");
let sum = std::fs::read_to_string(consumer.join("go.sum")).unwrap();
assert!(
sum.contains(&format!("{UMOD} {from} h1:")),
"go get must keep the stale zip line:\n{sum}"
);
}

/// Publish the upstream at `version` with a greeting of `greeting`.
fn publish_upstream(tmp: &Path, version: &str, greeting: &str) {
publish(
tmp,
UMOD,
version,
&[
("go.mod", &format!("module {UMOD}\n\ngo 1.16\n")),
(
"lib.go",
&format!(
"package upstream\n\nfunc Greeting() string {{ return \"{greeting}\" }}\n"
),
),
],
);
}

/// #1862 case 1: downgraded below the patched version, the build links
/// the older module. The stale zip line at the patched version must not
/// pass for proof it is built: apply refuses, and a replace written
/// before the downgrade fails `apply --check` (with or without the module
/// cache to read the graph from) and is left out of VEX.
#[test]
fn downgrade_below_the_patch_leaves_its_replace_unattested() {
if !golang_e2e_matrix::toolchain_ready("e2e_golang_build(downgrade-below)") {
return;
}
let tmp = tempfile::tempdir().unwrap();
let (consumer, modcache, proxy_url) = stage_with(tmp.path(), "1.16");
publish_upstream(tmp.path(), "v0.9.0", "OLDER");
let env = go_env(modcache.to_str().unwrap(), &proxy_url);
let readonly = readonly_env(&env);
let empty_cache = tmp.path().join("empty-modcache");
std::fs::create_dir_all(&empty_cache).unwrap();

// A fresh apply after the downgrade refuses and writes nothing.
downgraded_consumer(&consumer, UVER, "v0.9.0", &env);
write_patch(&consumer);
let (code, so, se) = apply_json(&consumer, &modcache);
assert_ne!(code, 0, "apply must refuse the inert replace:\n{so}\n{se}");
assert!(
so.contains(&format!("the build selects {UMOD} v0.9.0")),
"{so}\n{se}"
);
assert!(replace_lines(&consumer.join("go.mod")).is_empty());
// Applied before the downgrade: the replace is inert afterwards.
std::fs::write(
consumer.join("go.mod"),
format!("module example.com/consumer\n\ngo 1.16\n\nrequire {UMOD} {UVER}\n"),
)
.unwrap();
let tidy = go(&consumer, &["mod", "tidy"], &env);
assert!(tidy.status.success());
let (code, so, se) = apply_json(&consumer, &modcache);
assert_eq!(code, 0, "apply at the required version:\n{so}\n{se}");
assert!(go_out(&consumer, &readonly).contains("OUT: PATCHED"));
let socket_replace = replace_lines(&consumer.join("go.mod"));
assert_eq!(socket_replace.len(), 1, "{socket_replace:?}");
let get = go(&consumer, &["get", &format!("{UMOD}@v0.9.0")], &env);
assert!(
get.status.success(),
"{}",
String::from_utf8_lossy(&get.stderr)
);
// The directory replace dropped the original's go.sum lines; an
// untidied go.sum from before the apply still carries them.
let mut sum = std::fs::read_to_string(consumer.join("go.sum")).unwrap();
if !sum.contains(&format!("{UMOD} {UVER} h1:")) {
let pristine = tmp.path().join("pristine");
consumer_module(
&pristine,
"example.com/pristine",
modcache.to_str().unwrap(),
&proxy_url,
);
sum.push_str(&std::fs::read_to_string(pristine.join("go.sum")).unwrap());
std::fs::write(consumer.join("go.sum"), sum).unwrap();
}
assert_eq!(replace_lines(&consumer.join("go.mod")), socket_replace);
assert!(go_out(&consumer, &readonly).contains("OUT: OLDER"));

let (code, so, se) = check(&consumer, &modcache);
assert_eq!(
code, 1,
"apply --check must flag the inert replace:\n{so}\n{se}"
);
assert!(
format!("{so}{se}").contains("is not the version the build selects (v0.9.0)"),
"{so}\n{se}"
);
let (code, so, se) = check(&consumer, &empty_cache);
assert_eq!(code, 1, "without the cache too:\n{so}\n{se}");
assert!(
format!("{so}{se}").contains("nothing shows the build selects the patched version"),
"{so}\n{se}"
);
let r = VexRun {
offline: true,
product: Some("pkg:golang/example.com/consumer".into()),
..VexRun::default()
}
.env("GOMODCACHE", &modcache)
.env("GOFLAGS", "");
let out = run_vex(&vex_e2e_common::binary(), &consumer, &r);
assert_absent(out.doc.as_ref(), UPURL);
vex_e2e_common::assert_not_attested(&out.envelope, UPURL, "go_version_not_selected");

chmod_writable(tmp.path());
}

/// #1862 case 2: downgraded to the patched version, the build links it.
/// The stale zip line above it must not refuse the patch.
#[test]
fn downgrade_to_the_patch_applies_and_checks_in_sync() {
if !golang_e2e_matrix::toolchain_ready("e2e_golang_build(downgrade-to)") {
return;
}
let tmp = tempfile::tempdir().unwrap();
let (consumer, modcache, proxy_url) = stage_with(tmp.path(), "1.16");
publish_upstream(tmp.path(), "v1.1.0", "NEWER");
let env = go_env(modcache.to_str().unwrap(), &proxy_url);
let readonly = readonly_env(&env);

downgraded_consumer(&consumer, "v1.1.0", UVER, &env);
assert!(go_out(&consumer, &readonly).contains("OUT: PRISTINE"));
write_patch(&consumer);
let (code, so, se) = apply_json(&consumer, &modcache);
assert_eq!(
code, 0,
"apply must patch the selected version:\n{so}\n{se}"
);
assert!(go_out(&consumer, &readonly).contains("OUT: PATCHED"));
let (code, so, se) = check(&consumer, &modcache);
assert_eq!(code, 0, "apply --check:\n{so}\n{se}");

chmod_writable(tmp.path());
}
13 changes: 11 additions & 2 deletions crates/socket-patch-core/src/hosted/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,14 +536,23 @@ async fn read_go_workspace(
}
}
// Module mode: whether a module-to-module replace is in effect
// can turn on modules only the dependencies require (#1840).
// can turn on modules only the dependencies require (#1840),
// and which version the build selects on a go.sum zip line a
// downgrade may have left behind (#1862).
let mut module_graph = None;
if loaded == Loaded::None {
touched.push(root.join("go.mod"));
touched.push(root.join("go.sum"));
let go_mod = crate::utils::fs::read_regular_to_string(&root.join("go.mod")).await;
let go_sum = crate::utils::fs::read_regular_to_string(&root.join("go.sum"))
.await
.ok();
if let Ok(go_mod) = go_mod {
let go_mod = crate::vendor::go_mod_edit::normalize_for_read(&go_mod);
if crate::vendor::go_build_list::may_need_module_graph(&go_mod) {
if crate::vendor::go_build_list::may_need_module_graph(
&go_mod,
go_sum.as_deref(),
) {
touched.extend(
crate::vendor::go_build_list::module_graph_reads(&go_mod)
.into_iter()
Expand Down
26 changes: 26 additions & 0 deletions crates/socket-patch-core/src/patch/redirect/golang_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ pub enum Drift {
purl: String,
patched_version: String,
},
/// A go 1.16-or-older go.mod requires the module below a go.sum zip
/// line, and the module cache lacks a go.mod its graph needs. The line
/// is a dependency's raise or one `go get` left behind after a
/// downgrade, so nothing shows the build selects the patched version
/// (#1862).
UnprovenStaleSum {
purl: String,
patched_version: String,
},
/// go.mod builds the patched version only as the target of another
/// module's `replace` (`replace <via> => <module> <version>`). go
/// doesn't apply a replace to a replacement, so the Socket `replace` is
Expand Down Expand Up @@ -147,6 +156,15 @@ impl std::fmt::Display for Drift {
replacement directory nor the module cache (`go mod download` fetches it), \
and go may link the UNPATCHED module"
),
Drift::UnprovenStaleSum {
purl,
patched_version,
} => write!(
f,
"{purl}: nothing shows the build selects the patched version \
{patched_version}: {}; go may link the UNPATCHED module",
go_build_list::UNPROVEN_REASON
),
Drift::ReplaceTargetOnly {
purl,
patched_version,
Expand Down Expand Up @@ -724,6 +742,10 @@ pub async fn verify_go_redirect_state(
patched_version: version.to_string(),
})
}
Some(Selection::Unproven) => drifts.push(Drift::UnprovenStaleSum {
purl: purl.clone(),
patched_version: version.to_string(),
}),
Some(Selection::ReplaceTarget(via)) => drifts.push(Drift::ReplaceTargetOnly {
purl: purl.clone(),
patched_version: version.to_string(),
Expand Down Expand Up @@ -833,6 +855,10 @@ pub(crate) async fn selection_refusal(
replace to a replacement: its replace would be ignored and the build would \
link the unpatched module; nothing written"
)),
Selection::Unproven => Some(format!(
"nothing shows the build selects {module} {version}: {}; nothing written",
go_build_list::UNPROVEN_REASON
)),
Selection::Selected | Selection::Unknown => None,
}
}
Expand Down
16 changes: 14 additions & 2 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9740,6 +9740,8 @@ fn rewrite_golang_target(
"the go.work workspace selects"
} else if scan.required.as_deref() == Some(selected.as_str()) {
"go.mod requires"
} else if graph.as_ref().is_some_and(|g| g.complete) {
"the module graph selects"
} else {
"go.sum shows the build selects"
};
Expand Down Expand Up @@ -9805,6 +9807,7 @@ fn rewrite_golang_target(
return false;
} else if selection != go_build_list::Selection::Selected
&& (in_workspace && selection == go_build_list::Selection::Unknown
|| selection == go_build_list::Selection::Unproven
|| prior.is_none_or(|e| e.version.as_deref() != Some(dep.version.as_str())))
{
// Not selected by go.mod's require, no go.sum zip line at this
Expand All @@ -9813,10 +9816,19 @@ fn rewrite_golang_target(
// module cache). Its replace would be inert, and confirming it
// would attest a patch no build links. A workspace graph that
// could not be read proves nothing either way, so there even our
// own pin is no proof the build selects this version.
// own pin is no proof the build selects this version. Nor is it
// over a go.sum zip line above go.mod's require that a downgrade
// may have left behind (#1862).
result.warnings.push(RewriteWarning {
code: "redirect_golang_not_in_module_graph".into(),
detail: if in_workspace && selection == go_build_list::Selection::Unknown {
detail: if selection == go_build_list::Selection::Unproven {
format!(
"{fname}@{}: nothing shows the build selects this version — {}; \
nothing redirected",
dep.version,
go_build_list::UNPROVEN_REASON
)
} else if in_workspace && selection == go_build_list::Selection::Unknown {
format!(
"{fname}@{}: nothing shows the go.work workspace building this version \
— a go.mod its module graph needs is not in the module cache (`go mod \
Expand Down
Loading
Loading