Skip to content

Commit 6d44a29

Browse files
committed
Close the in-process env race in the builder
Some test binaries set flag-bound SOCKET_* vars in their own process (apply_env_toggles, env save/restore helpers). The hermetic builder only removed names present when it swept the parent environment, so a var set by a parallel test before the spawn still reached the child, and suites patched that with their own, already diverging, removal lists. hermetic::command now removes every flag-bound var by name, so the per-file lists go away, and SOCKET_CWD joins the hostile seeds. A new contract test sets vars after the builder runs and checks none leak. Refs #823 Assisted-by: Claude Code:claude-opus-5-5
1 parent e974944 commit 6d44a29

6 files changed

Lines changed: 49 additions & 30 deletions

File tree

‎crates/socket-patch-cli/tests/cli/covgap_commands_list.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ fn write_empty_manifest(root: &Path) {
6464
/// fixture and the config layer re-enabled. `debug` adds the `--debug`
6565
/// flag under test.
6666
fn list_cmd(project: &Path, data_dir: &Path, debug: bool) -> Command {
67-
let mut cmd = common::hermetic::binary_command();
67+
let mut cmd = common::hermetic_command(&common::binary());
6868
cmd.arg("list");
6969
if debug {
7070
cmd.arg("--debug");

‎crates/socket-patch-cli/tests/common/hermetic.rs‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use std::process::Command;
3535
/// developer's ambient shell, which the suites can't rely on) turns the tests
3636
/// red immediately.
3737
const HOSTILE_SEEDS: &[(&str, &str)] = &[
38+
("SOCKET_CWD", "/nonexistent"),
3839
("SOCKET_GLOBAL", "true"),
3940
("SOCKET_GLOBAL_PREFIX", "/nonexistent"),
4041
("SOCKET_DRY_RUN", "true"),
@@ -48,11 +49,12 @@ const HOSTILE_SEEDS: &[(&str, &str)] = &[
4849
];
4950

5051
/// A `Command` for `bin` with the hermetic `SOCKET_*` environment: the
51-
/// hostile seeds scrubbed, every other ambient `SOCKET_*` removed (removing
52-
/// `SOCKET_API_TOKEN` also forces the public proxy), and the three opt-outs
53-
/// (config layer, update notifier, telemetry) forced on. Callers add args,
54-
/// cwd and their own env afterwards; caller env lands last, so explicit
55-
/// injections survive the scrub.
52+
/// hostile seeds scrubbed, every flag-bound `SOCKET_*` and every other
53+
/// ambient `SOCKET_*` removed (removing `SOCKET_API_TOKEN` also forces the
54+
/// public proxy), and the three opt-outs (config layer, update notifier,
55+
/// telemetry) forced on. Callers add args, cwd and their own env
56+
/// afterwards; caller env lands last, so explicit injections survive the
57+
/// scrub.
5658
pub fn command(bin: &Path) -> Command {
5759
let mut cmd = Command::new(bin);
5860
for (k, v) in HOSTILE_SEEDS {
@@ -61,6 +63,16 @@ pub fn command(bin: &Path) -> Command {
6163
for (k, _) in HOSTILE_SEEDS {
6264
cmd.env_remove(k);
6365
}
66+
// The flag-bound vars are removed by name, not only when the sweep below
67+
// finds them: in-process tests in the same binary set some of them
68+
// (`apply_env_toggles`, env save/restore helpers), and one set between
69+
// this sweep and the spawn would otherwise reach the child.
70+
for k in socket_patch_cli::args::GLOBAL_ARG_ENV_VARS
71+
.iter()
72+
.chain(socket_patch_cli::args::LOCAL_ARG_ENV_VARS)
73+
{
74+
cmd.env_remove(k);
75+
}
6476
cmd.env_remove("SOCKET_API_TOKEN");
6577
scrub_socket_vars(&mut cmd);
6678
// Belt-and-braces on top of the `.cargo/config.toml` `[env]` default:

‎crates/socket-patch-cli/tests/covgap_commands_rollback.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ fn readonly_dir_enforced(dir: &Path) -> bool {
248248

249249
/// Spawn the binary WITHOUT waiting (the lock-choreography tests mutate
250250
/// the fixture while the child is blocked on the apply lock), with the
251-
/// same hermetic environment as `common::run`.
251+
/// same hermetic `SOCKET_*` environment as `common::run` (not its JVM
252+
/// isolation; no test here spawns a JVM fixture this way).
252253
fn spawn_scrubbed(cwd: &Path, args: &[&str]) -> std::process::Child {
253254
let mut cmd = common::hermetic_command(&common::binary());
254255
cmd.args(args).current_dir(cwd);

‎crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ use std::path::Path;
4747
use wiremock::matchers::{method, path};
4848
use wiremock::{Mock, MockServer, ResponseTemplate};
4949

50-
use socket_patch_cli::args::{GLOBAL_ARG_ENV_VARS, LOCAL_ARG_ENV_VARS};
51-
5250
const ORG_SLUG: &str = "test-org";
5351
const UUID: &str = "11111111-1111-4111-8111-111111111111";
5452
const OLD_UUID: &str = "99999999-9999-4999-8999-999999999999";
@@ -78,13 +76,6 @@ fn write_npm_package(root: &Path, name: &str, version: &str, content: &[u8]) {
7876
fn run_scan(cwd: &Path, args: &[&str]) -> (i32, String, String) {
7977
let mut cmd = hermetic_command(&binary());
8078
cmd.arg("scan").args(args).current_dir(cwd);
81-
// The `mode_fold` tests below clear and restore the flag-bound vars
82-
// in-process; one restored by a parallel test between the builder's
83-
// scrub and the spawn would be inherited, so remove the whole
84-
// flag-bound surface unconditionally too.
85-
for var in GLOBAL_ARG_ENV_VARS.iter().chain(LOCAL_ARG_ENV_VARS.iter()) {
86-
cmd.env_remove(var);
87-
}
8879
// The unconditional python crawl consults VIRTUAL_ENV first, so an
8980
// activated host virtualenv would make "empty project" scans non-empty.
9081
cmd.env_remove("VIRTUAL_ENV");

‎crates/socket-patch-cli/tests/covgap_commands_vendor.rs‎

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -205,18 +205,6 @@ fn run_cli(cwd: &Path, args: &[&str], extra_env: &[(&str, &str)]) -> (i32, Strin
205205
}
206206
}
207207
cmd.args(&filtered).current_dir(cwd);
208-
// In-process tests in this binary `std::env::set_var` these via
209-
// `apply_env_toggles`; one set by a parallel test between the
210-
// builder's scrub and the spawn would be inherited, so remove them
211-
// unconditionally (see in_process_vendor.rs `run_cli`).
212-
for key in [
213-
"SOCKET_OFFLINE",
214-
"SOCKET_DEBUG",
215-
"SOCKET_API_URL",
216-
"SOCKET_PROXY_URL",
217-
] {
218-
cmd.env_remove(key);
219-
}
220208
for (k, v) in extra_env {
221209
cmd.env(k, v);
222210
}
@@ -763,8 +751,6 @@ fn npm_only_human_vendor_does_not_read_composer_lock() {
763751
.env("SOCKET_VENDOR_URL", &fixture.uri)
764752
.args(["vendor", "--cwd", fx.root().to_str().unwrap()])
765753
.current_dir(fx.root())
766-
.env_remove("SOCKET_OFFLINE")
767-
.env_remove("SOCKET_API_URL")
768754
.stdin(std::process::Stdio::null())
769755
.stdout(std::process::Stdio::piped())
770756
.stderr(std::process::Stdio::piped())

‎crates/socket-patch-cli/tests/spawn_env_hygiene.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ fn workflow_env_copies_carry_every_opt_out() {
342342
fn command_never_leaks_its_hostile_seeds() {
343343
let env = effective_env(&hermetic::command(Path::new("socket-patch")));
344344
for key in [
345+
"SOCKET_CWD",
345346
"SOCKET_GLOBAL",
346347
"SOCKET_GLOBAL_PREFIX",
347348
"SOCKET_DRY_RUN",
@@ -357,6 +358,34 @@ fn command_never_leaks_its_hostile_seeds() {
357358
}
358359
}
359360

361+
/// In-process tests set flag-bound vars in the test binary's own
362+
/// environment (`apply_env_toggles`, save/restore helpers). One set after
363+
/// the builder's sweep but before the spawn must still not reach the child.
364+
#[test]
365+
#[serial]
366+
fn command_drops_flag_bound_vars_set_after_it_is_built() {
367+
let cmd = hermetic::command(Path::new("socket-patch"));
368+
with_ambient(
369+
&[
370+
("SOCKET_OFFLINE", "true"),
371+
("SOCKET_DEBUG", "1"),
372+
("SOCKET_PROXY_URL", "http://ambient.invalid"),
373+
("SOCKET_SKIP_ROLLBACK", "true"),
374+
],
375+
|| {
376+
let env = effective_env(&cmd);
377+
for key in [
378+
"SOCKET_OFFLINE",
379+
"SOCKET_DEBUG",
380+
"SOCKET_PROXY_URL",
381+
"SOCKET_SKIP_ROLLBACK",
382+
] {
383+
assert_eq!(get(&env, key), None, "{key} set after build leaked");
384+
}
385+
},
386+
);
387+
}
388+
360389
#[test]
361390
#[serial]
362391
fn caller_env_set_after_command_survives_the_scrub() {

0 commit comments

Comments
 (0)