Skip to repository content881 lines · 34.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:49:35.452Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
wsl_sandbox_test_helper.rs
1//! Behavior test helper for the Windows WSL Bubblewrap sandbox — the Windows
2//! analog of `bwrap_test_helper`.
3//!
4//! Where the Linux helper is the sandboxed process itself (it re-execs under
5//! the launcher), here the sandboxed process is a *Linux* program inside WSL
6//! while this helper runs on Windows. So instead of a status channel and a
7//! launcher, the helper drives the real [`sandbox::Sandbox`] (`new` + `wrap`),
8//! spawns the command line it produces, and inspects exit codes and
9//! host-side filesystem effects to confirm every grant the sandbox makes and
10//! every restriction it imposes actually holds — including the Windows-specific
11//! one: that a sandboxed process cannot escape via WSL interop by exec'ing a
12//! Windows binary.
13//!
14//! It targets the **default** WSL distro (matching real Zed usage for native
15//! Windows paths); provision that distro before running (see
16//! `script/test-wsl-sandbox.ps1`). Like the Linux helper, it **skips** (rather
17//! than fails) the enforcement assertions when the environment can't actually
18//! enforce a sandbox, so a misconfigured WSL doesn't masquerade as a sandbox
19//! regression. Set `ZED_TEST_SANDBOX_REQUIRE_ENFORCED=1` to turn that skip into
20//! a failure once you've provisioned an environment that *should* enforce.
21//!
22//! Run it with `cargo xtask wsl-sandbox-tests` or `script/test-wsl-sandbox.ps1`.
23
24#![allow(
25 clippy::disallowed_methods,
26 reason = "a single-threaded test helper that intentionally blocks on child processes"
27)]
28
29#[cfg(not(target_os = "windows"))]
30fn main() {
31 eprintln!("wsl_sandbox_test_helper is only supported on Windows");
32 std::process::exit(1);
33}
34
35#[cfg(target_os = "windows")]
36fn main() {
37 imp::main();
38}
39
40#[cfg(target_os = "windows")]
41mod imp {
42 use std::collections::HashMap;
43 use std::ffi::OsStr;
44 use std::net::TcpListener;
45 use std::os::windows::process::CommandExt as _;
46 use std::path::{Path, PathBuf};
47 use std::process::{Command, Output};
48
49 use anyhow::{Context as _, Result, bail, ensure};
50 use sandbox::{
51 CommandAndArgs, HostFilesystemLocation, Sandbox, SandboxError, SandboxFsPolicy,
52 SandboxNetPolicy, SandboxPolicy,
53 };
54
55 /// Network access for a helper run, translated into a `SandboxNetPolicy` in
56 /// `drive_sandbox`. Only the all-or-nothing cases the helper exercises are
57 /// represented.
58 #[derive(Clone, Copy, Default)]
59 enum NetworkAccess {
60 #[default]
61 None,
62 All,
63 }
64
65 /// The per-run permission knobs the helper varies, translated into a
66 /// `SandboxPolicy` in `drive_sandbox`.
67 #[derive(Clone, Copy, Default)]
68 struct SandboxPermissions {
69 network: NetworkAccess,
70 allow_fs_write: bool,
71 }
72
73 /// Tag prefixed to every result line, matching `bwrap_test_helper` so both
74 /// helpers' output reads the same.
75 const RESULT_TAG: &str = "[sandbox_test]:";
76
77 /// `CREATE_NO_WINDOW`: keep `wsl.exe` (a console-subsystem binary) from
78 /// flashing a console window when spawned.
79 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
80
81 pub fn main() {
82 if let Err(error) = run() {
83 eprintln!("{RESULT_TAG} FAILED: {error:#}");
84 std::process::exit(1);
85 }
86 }
87
88 fn run() -> Result<()> {
89 let require_enforced = env_flag("ZED_TEST_SANDBOX_REQUIRE_ENFORCED");
90 let wsl = Wsl::detect();
91 println!("{RESULT_TAG} starting (require_enforced={require_enforced})");
92
93 // `wrap_invocation` performs the real environment probe (locate `bwrap`,
94 // reject setuid-root, smoke-test the exact namespaces) before it builds
95 // any command. So a default-permissions run of `true` doubles as our
96 // enforcement probe: `Ok` means the sandbox is enforceable here, an
97 // `Unavailable` error means it is not.
98 let probe = run_in_sandbox("true", &[], SandboxPermissions::default())?;
99 match &probe {
100 Outcome::Ran {
101 command_succeeded: true,
102 ..
103 } => {}
104 Outcome::Unavailable(message) => return not_enforced(require_enforced, message),
105 other => {
106 return not_enforced(
107 require_enforced,
108 &format!("the sandbox probe did not run cleanly: {other:?}"),
109 );
110 }
111 }
112
113 run_enforced(&wsl)
114 }
115
116 /// The environment can't enforce a sandbox. Skip the enforcement checks
117 /// unless the caller asserted (via `ZED_TEST_SANDBOX_REQUIRE_ENFORCED`) that
118 /// it should be able to, in which case this is a real failure.
119 fn not_enforced(require_enforced: bool, reason: &str) -> Result<()> {
120 if require_enforced {
121 bail!(
122 "ZED_TEST_SANDBOX_REQUIRE_ENFORCED is set, but the WSL sandbox could not be \
123 enforced: {reason}"
124 );
125 }
126 println!(
127 "{RESULT_TAG} SKIP: this environment cannot enforce a WSL bwrap sandbox: {reason}"
128 );
129 Ok(())
130 }
131
132 /// Enforced scenario: `bwrap` is present and a sandbox can be set up, so the
133 /// sandbox must actually be enforced. Assert every grant and every
134 /// restriction end-to-end against the real WSL distro.
135 fn run_enforced(wsl: &Wsl) -> Result<()> {
136 let mut checks = Checks::new();
137 let pid = std::process::id();
138
139 // The core filesystem checks use a scratch tree on the WSL distro's own
140 // rootfs (under `/var/tmp`, which the sandbox leaves read-only rather
141 // than overlaying like `/tmp`). This mirrors the Linux helper and is
142 // robust: it doesn't depend on how drvfs `/mnt/<drive>` submounts behave
143 // under bwrap's recursive root bind. The Windows-drive translation path
144 // (the realistic Zed-on-`C:` case) gets its own dedicated check below.
145 let root_base = format!("/var/tmp/zed-wsl-sandbox-test-{pid}");
146 let writable_wsl = format!("{root_base}/writable");
147 let forbidden_wsl = format!("{root_base}/forbidden");
148 let readable_wsl = format!("{root_base}/readable");
149 let mkdir = wsl.run_sh(&format!(
150 "mkdir -p {} {} {}",
151 shell_quote(&writable_wsl),
152 shell_quote(&forbidden_wsl),
153 shell_quote(&readable_wsl),
154 ))?;
155 ensure!(
156 mkdir.status.success(),
157 "failed to create the WSL scratch tree{}",
158 failure_details(&mkdir)
159 );
160 let _root_cleanup = WslCleanup {
161 exe: wsl.exe.clone(),
162 path: root_base,
163 };
164
165 let default = SandboxPermissions::default();
166 let fs_write_all = SandboxPermissions {
167 network: NetworkAccess::None,
168 allow_fs_write: true,
169 };
170 let network_allowed = SandboxPermissions {
171 network: NetworkAccess::All,
172 allow_fs_write: false,
173 };
174
175 // GRANT: writing into a writable bind succeeds and lands on the host.
176 let writable_file = format!("{writable_wsl}/from-sandbox.txt");
177 let write_writable = run_in_sandbox(
178 &format!("echo omega > {}", shell_quote(&writable_file)),
179 &[PathBuf::from(writable_wsl)],
180 default,
181 )?;
182 checks.expect_succeeded("GRANT: write into a writable dir succeeds", &write_writable);
183 checks.expect(
184 "GRANT: write into a writable dir lands on the host",
185 wsl.exists(&writable_file)?,
186 );
187
188 // RESTRICT: writing outside any writable bind is denied by the read-only
189 // root, and must not leak to the host.
190 let forbidden_file = format!("{forbidden_wsl}/escaped.txt");
191 let write_forbidden = run_in_sandbox(
192 &format!("echo omega > {}", shell_quote(&forbidden_file)),
193 &[],
194 default,
195 )?;
196 checks.expect_blocked(
197 "RESTRICT: write outside writable dirs is denied",
198 &write_forbidden,
199 );
200 checks.expect(
201 "RESTRICT: denied write did not leak to the host",
202 !wsl.exists(&forbidden_file)?,
203 );
204
205 // GRANT: the whole filesystem is readable (root is bound read-only), so
206 // a host file outside every writable dir can still be read.
207 let readable_file = format!("{readable_wsl}/host-data.txt");
208 let seed = wsl.run_sh(&format!(
209 "printf 'host data' > {}",
210 shell_quote(&readable_file)
211 ))?;
212 ensure!(
213 seed.status.success(),
214 "failed to seed the readable file{}",
215 failure_details(&seed)
216 );
217 let read_host = run_in_sandbox(
218 &format!("cat {}", shell_quote(&readable_file)),
219 &[],
220 default,
221 )?;
222 checks.expect_succeeded(
223 "GRANT: host files outside writable dirs are still readable",
224 &read_host,
225 );
226
227 // GRANT + RESTRICT: `/tmp` is a writable tmpfs, but ephemeral — it must
228 // not leak to the WSL distro's real `/tmp`.
229 let tmp_path = format!("/tmp/zed-sandbox-ephemeral-{pid}");
230 let write_tmp = run_in_sandbox(
231 &format!("echo omega > {}", shell_quote(&tmp_path)),
232 &[],
233 default,
234 )?;
235 checks.expect_succeeded("GRANT: writing to /tmp succeeds", &write_tmp);
236 checks.expect(
237 "RESTRICT: /tmp writes are ephemeral (do not leak to the WSL host /tmp)",
238 !wsl.exists(&tmp_path)?,
239 );
240
241 // RESTRICT + GRANT: outbound TCP is denied by the network namespace, but
242 // works when network access is explicitly granted. We discover a peer
243 // reachable from WSL first; that same reachability check proves the
244 // denial below is the sandbox's doing.
245 match discover_peer(wsl)? {
246 Some(peer) => {
247 let connect = connect_script(&peer);
248 let net_denied = run_in_sandbox(&connect, &[], default)?;
249 checks.expect_blocked(
250 "RESTRICT: outbound TCP is blocked when network is denied",
251 &net_denied,
252 );
253 let net_allowed = run_in_sandbox(&connect, &[], network_allowed)?;
254 checks.expect_succeeded(
255 "GRANT: outbound TCP works when network is allowed",
256 &net_allowed,
257 );
258 // RESTRICT: permissions are independent — granting filesystem
259 // writes must not also grant network access.
260 let net_with_fs_write = run_in_sandbox(&connect, &[], fs_write_all)?;
261 checks.expect_blocked(
262 "RESTRICT: allow_fs_write does not also grant network access",
263 &net_with_fs_write,
264 );
265 }
266 None => println!(
267 "{RESULT_TAG} SKIP: no TCP peer reachable from WSL; skipping network checks"
268 ),
269 }
270
271 // GRANT: local AF_UNIX IPC keeps working even while IP networking is
272 // denied. Needs python3 to create the socket; skip if it isn't present.
273 if wsl.has_program("python3")? {
274 let unix_ok = run_in_sandbox(
275 "python3 -c 'import socket; socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)'",
276 &[],
277 default,
278 )?;
279 checks.expect_succeeded(
280 "GRANT: AF_UNIX sockets still work while network is denied",
281 &unix_ok,
282 );
283 } else {
284 println!("{RESULT_TAG} SKIP: no python3 in WSL; skipping AF_UNIX check");
285 }
286
287 // GRANT (escape hatch): `allow_fs_write` lets the command write
288 // anywhere, and the write reaches the host.
289 let escape_file = format!("{forbidden_wsl}/escape-hatch.txt");
290 let write_escape = run_in_sandbox(
291 &format!("echo omega > {}", shell_quote(&escape_file)),
292 &[],
293 fs_write_all,
294 )?;
295 checks.expect_succeeded(
296 "GRANT: allow_fs_write lets the command write outside writable dirs",
297 &write_escape,
298 );
299 checks.expect(
300 "GRANT: allow_fs_write write lands on the host",
301 wsl.exists(&escape_file)?,
302 );
303
304 // GRANT + RESTRICT (Windows-specific): a writable directory given as a
305 // native `C:\` path is translated into WSL and bound read-write, and the
306 // write lands back on the Windows filesystem.
307 check_windows_drive_writable(wsl, &mut checks)?;
308
309 // RESTRICT (Windows-specific): WSL interop must be blocked, so a
310 // sandboxed process can't exec a Windows binary and escape bwrap.
311 check_interop_blocked(wsl, &mut checks)?;
312
313 // GRANT + RESTRICT: the caller's environment is forwarded into the
314 // command, but Windows-specific values like PATH are not (which would
315 // otherwise shadow WSL's PATH and break the shell).
316 check_env_forwarding(&mut checks)?;
317
318 // Degraded (bad request): a non-existent writable path is the model's
319 // mistake, not a broken sandbox environment, so it must be reported
320 // *without* the "sandboxing is unavailable" marker (which would wrongly
321 // prompt the user to disable sandboxing globally).
322 let missing = std::env::temp_dir().join(format!("zed-wsl-missing-{pid}"));
323 let bad_request = drive_sandbox(
324 "true",
325 &[],
326 std::slice::from_ref(&missing),
327 default,
328 &HashMap::new(),
329 )?;
330 checks.expect(
331 "a non-existent writable path is a bad request, not an unavailable-environment error",
332 matches!(bad_request, Outcome::BadRequest(_)),
333 );
334 if !matches!(bad_request, Outcome::BadRequest(_)) {
335 println!("{RESULT_TAG} (got {bad_request:?})");
336 }
337
338 checks.finish()
339 }
340
341 /// Windows-specific GRANT: a writable directory passed as a native `C:\`
342 /// path is translated into WSL with `wslpath`, bound read-write, and a write
343 /// inside the sandbox lands back on the Windows filesystem. Exercises the
344 /// native-drive path translation end-to-end (the realistic case of Zed on
345 /// Windows sandboxing a command in a project under `C:\`).
346 fn check_windows_drive_writable(wsl: &Wsl, checks: &mut Checks) -> Result<()> {
347 let base =
348 std::env::temp_dir().join(format!("zed-wsl-sandbox-drive-{}", std::process::id()));
349 let writable = base.join("writable");
350 std::fs::create_dir_all(&writable)
351 .with_context(|| format!("failed to create scratch dir `{}`", writable.display()))?;
352 let _cleanup = Cleanup(base);
353
354 let mapped = wsl.wsl_paths(&[&writable]).context(
355 "failed to translate the scratch dir into a WSL path (is the C: drive automounted?)",
356 )?;
357 let Some(writable_wsl) = mapped.into_iter().next() else {
358 bail!("wslpath returned no result for the scratch dir");
359 };
360
361 let write = run_in_sandbox(
362 &format!(
363 "echo omega > {}",
364 shell_quote(&format!("{writable_wsl}/from-sandbox.txt"))
365 ),
366 std::slice::from_ref(&writable),
367 SandboxPermissions::default(),
368 )?;
369 checks.expect_succeeded(
370 "GRANT: write into a writable C:\\ dir succeeds (native path translated into WSL)",
371 &write,
372 );
373 checks.expect(
374 "GRANT: write into a writable C:\\ dir lands on the Windows host",
375 writable.join("from-sandbox.txt").exists(),
376 );
377 Ok(())
378 }
379
380 /// Assert that a sandboxed process cannot reach the Windows host through WSL
381 /// interop. Without the sandbox's interop block, a command could exec a
382 /// Windows binary (e.g. `cmd.exe`), which the WSL binfmt handler runs on the
383 /// host — fully outside bwrap.
384 fn check_interop_blocked(wsl: &Wsl, checks: &mut Checks) -> Result<()> {
385 // `$WSL_INTEROP` must be unset inside the sandbox (the variable `/init`
386 // uses to find the interop socket).
387 let interop_env = run_in_sandbox(
388 "[ -z \"$WSL_INTEROP\" ]",
389 &[],
390 SandboxPermissions::default(),
391 )?;
392 checks.expect_succeeded(
393 "RESTRICT: $WSL_INTEROP is unset inside the sandbox",
394 &interop_env,
395 );
396
397 // Resolve cmd.exe's path inside WSL; skip the exec check if we can't
398 // (e.g. a non-standard automount root).
399 let cmd = match wsl.wsl_paths(&[Path::new(r"C:\Windows\System32\cmd.exe")]) {
400 Ok(mut paths) => paths.pop(),
401 Err(_) => None,
402 };
403 let Some(cmd) = cmd else {
404 println!(
405 "{RESULT_TAG} SKIP: could not resolve cmd.exe inside WSL; skipping interop exec check"
406 );
407 return Ok(());
408 };
409
410 // Control: unsandboxed, interop should let WSL exec a Windows binary. If
411 // even this fails, interop isn't available here, so the sandboxed denial
412 // below would prove nothing — skip.
413 let control = wsl.run(&cmd, ["/C", "exit"])?;
414 if !control.status.success() {
415 println!(
416 "{RESULT_TAG} SKIP: WSL interop is unavailable in this environment (the unsandboxed \
417 control run failed); skipping interop exec check"
418 );
419 return Ok(());
420 }
421
422 // Sandboxed: exec'ing the same Windows binary must fail, because interop
423 // is blocked.
424 let escape = run_in_sandbox(
425 &format!("{} /C exit", shell_quote(&cmd)),
426 &[],
427 SandboxPermissions::default(),
428 )?;
429 checks.expect_blocked(
430 "RESTRICT: cannot exec a Windows binary via WSL interop (sandbox escape blocked)",
431 &escape,
432 );
433 Ok(())
434 }
435
436 /// Assert the caller's environment is forwarded into the sandbox, while
437 /// Windows-specific values like PATH are dropped rather than overriding
438 /// WSL's own.
439 fn check_env_forwarding(checks: &mut Checks) -> Result<()> {
440 let mut env = HashMap::new();
441 env.insert("ZED_TEST_FORWARDED".to_string(), "yes".to_string());
442 // If PATH were forwarded it would replace WSL's PATH with this bogus
443 // value; it must not be.
444 env.insert(
445 "PATH".to_string(),
446 "/omega-sentinel-should-not-win".to_string(),
447 );
448 let outcome = drive_sandbox(
449 "/bin/sh",
450 &[
451 "-c",
452 "[ \"$ZED_TEST_FORWARDED\" = yes ] && [ \"$PATH\" != /omega-sentinel-should-not-win ]",
453 ],
454 &[],
455 SandboxPermissions::default(),
456 &env,
457 )?;
458 checks.expect_succeeded(
459 "GRANT: caller env is forwarded into the sandbox; RESTRICT: PATH is not overridden",
460 &outcome,
461 );
462 Ok(())
463 }
464
465 /// The outcome of asking the sandbox to run a command.
466 #[derive(Debug)]
467 enum Outcome {
468 /// `wrap_invocation` succeeded and the wrapped `wsl.exe` command ran;
469 /// `command_succeeded` is its exit success.
470 Ran {
471 command_succeeded: bool,
472 stdout: String,
473 stderr: String,
474 },
475 /// `wrap_invocation` reported the sandbox *environment* as unavailable
476 /// (carried the shared unavailable-prefix marker).
477 Unavailable(String),
478 /// `wrap_invocation` reported a bad request (a mappable-path / distro
479 /// problem); no unavailable-prefix marker.
480 BadRequest(String),
481 }
482
483 /// Run `/bin/sh -c <script>` under the sandbox with the given writable paths
484 /// and permissions.
485 fn run_in_sandbox(
486 script: &str,
487 writable_paths: &[PathBuf],
488 permissions: SandboxPermissions,
489 ) -> Result<Outcome> {
490 drive_sandbox(
491 "/bin/sh",
492 &["-c", script],
493 writable_paths,
494 permissions,
495 &HashMap::new(),
496 )
497 }
498
499 /// Drive the real sandbox the way Zed's terminal integration does: wrap the
500 /// invocation, then spawn the resulting `wsl.exe` command and collect its
501 /// result. `wrap_invocation` errors are classified into [`Outcome`] by the
502 /// shared unavailable-prefix marker rather than bubbling up, so callers can
503 /// assert on the classification.
504 fn drive_sandbox(
505 program: &str,
506 args: &[&str],
507 writable_paths: &[PathBuf],
508 permissions: SandboxPermissions,
509 env: &HashMap<String, String>,
510 ) -> Result<Outcome> {
511 let policy = SandboxPolicy {
512 fs: if permissions.allow_fs_write {
513 SandboxFsPolicy::Unrestricted {
514 protected_paths: Vec::new(),
515 }
516 } else {
517 SandboxFsPolicy::Restricted {
518 // On Windows the location only records the requested path;
519 // the real capture happens WSL-side in the helper.
520 writable_paths: writable_paths
521 .iter()
522 .filter_map(|path| HostFilesystemLocation::new(path).ok())
523 .collect(),
524 protected_paths: Vec::new(),
525 }
526 },
527 network: match permissions.network {
528 NetworkAccess::None => SandboxNetPolicy::Blocked,
529 NetworkAccess::All => SandboxNetPolicy::Unrestricted,
530 },
531 };
532 let command = CommandAndArgs {
533 program: program.to_string(),
534 args: args.iter().map(|arg| arg.to_string()).collect(),
535 env: env.clone(),
536 cwd: None,
537 };
538 let prepared =
539 Sandbox::new(policy).and_then(|mut sandbox| smol::block_on(sandbox.wrap(&command)));
540
541 let prepared = match prepared {
542 Ok(prepared) => prepared,
543 Err(error) => {
544 let message = error.to_string();
545 return Ok(match error {
546 SandboxError::WslUnavailable(_) => Outcome::Unavailable(message),
547 _ => Outcome::BadRequest(message),
548 });
549 }
550 };
551
552 let output = Command::new(&prepared.program)
553 .args(&prepared.args)
554 .creation_flags(CREATE_NO_WINDOW)
555 .output()
556 .context("failed to spawn the wrapped wsl.exe sandbox command")?;
557 Ok(Outcome::Ran {
558 command_succeeded: output.status.success(),
559 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
560 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
561 })
562 }
563
564 /// Find a TCP peer reachable from inside WSL so the network checks have a
565 /// real endpoint. Honors `ZED_TEST_ECHO_ADDR` (a `host:port`) if set;
566 /// otherwise binds a local listener on the Windows side and finds an address
567 /// WSL can use to reach the Windows host. Returns `None` (so the caller
568 /// skips, rather than fails, the network checks) when nothing is reachable.
569 fn discover_peer(wsl: &Wsl) -> Result<Option<String>> {
570 if let Some(address) = std::env::var("ZED_TEST_ECHO_ADDR")
571 .ok()
572 .filter(|address| !address.is_empty())
573 {
574 ensure_host_port(&address)?;
575 return Ok(Some(address));
576 }
577
578 // A listener on the Windows side that accepts and immediately drops
579 // connections, so an in-WSL connect succeeds. It lives for the rest of
580 // the process (the thread owns it and never closes it).
581 let listener =
582 TcpListener::bind(("0.0.0.0", 0)).context("failed to bind a TCP listener")?;
583 let port = listener
584 .local_addr()
585 .context("local listener has no address")?
586 .port();
587 std::thread::spawn(move || {
588 for stream in listener.incoming() {
589 match stream {
590 Ok(stream) => drop(stream),
591 Err(_) => break,
592 }
593 }
594 });
595
596 // Addresses WSL might use to reach the Windows host: the default
597 // gateway (WSL2 NAT mode) and loopback (mirrored networking mode).
598 let mut candidates = Vec::new();
599 if let Ok(output) =
600 wsl.run_sh("ip route show default 2>/dev/null | awk '/default/{print $3; exit}'")
601 {
602 let gateway = String::from_utf8_lossy(&output.stdout).trim().to_string();
603 if !gateway.is_empty() {
604 candidates.push(gateway);
605 }
606 }
607 candidates.push("127.0.0.1".to_string());
608
609 for host in candidates {
610 let address = format!("{host}:{port}");
611 if wsl.run_sh(&connect_script(&address))?.status.success() {
612 return Ok(Some(address));
613 }
614 }
615 Ok(None)
616 }
617
618 /// A shell snippet that attempts an outbound TCP connection to `host:port`,
619 /// exiting 0 on success. Uses bash's `/dev/tcp` (the most widely available
620 /// option in default WSL distros) under `timeout` so a blocked connect can't
621 /// hang. If bash or timeout is missing the snippet simply fails, which the
622 /// caller treats as "unreachable" and skips.
623 fn connect_script(address: &str) -> String {
624 format!(
625 "timeout 5 bash -c 'exec 3<>/dev/tcp/{}'",
626 address.replace(':', "/")
627 )
628 }
629
630 fn ensure_host_port(address: &str) -> Result<()> {
631 let (host, port) = address
632 .rsplit_once(':')
633 .with_context(|| format!("ZED_TEST_ECHO_ADDR must be host:port, got {address:?}"))?;
634 ensure!(
635 !host.is_empty() && port.parse::<u16>().is_ok(),
636 "ZED_TEST_ECHO_ADDR must be host:port, got {address:?}"
637 );
638 Ok(())
639 }
640
641 /// Thin wrapper over `wsl.exe` for the helper's own (unsandboxed) WSL calls:
642 /// path translation, the interop control run, and peer discovery. Targets
643 /// the default distro, matching `wrap_invocation`.
644 struct Wsl {
645 exe: PathBuf,
646 }
647
648 impl Wsl {
649 fn detect() -> Self {
650 let exe = std::env::var_os("SystemRoot")
651 .map(PathBuf::from)
652 .unwrap_or_else(|| PathBuf::from(r"C:\Windows"))
653 .join("System32")
654 .join("wsl.exe");
655 Self { exe }
656 }
657
658 /// Run a Linux `program` (with `args`) unsandboxed via `wsl.exe --exec`.
659 fn run<I, S>(&self, program: &str, args: I) -> Result<Output>
660 where
661 I: IntoIterator<Item = S>,
662 S: AsRef<OsStr>,
663 {
664 Command::new(&self.exe)
665 .arg("--exec")
666 .arg(program)
667 .args(args)
668 .creation_flags(CREATE_NO_WINDOW)
669 .output()
670 .with_context(|| format!("failed to run `wsl.exe --exec {program}`"))
671 }
672
673 /// Run `/bin/sh -c <script>` unsandboxed.
674 fn run_sh(&self, script: &str) -> Result<Output> {
675 self.run("/bin/sh", ["-c", script])
676 }
677
678 /// Whether `name` resolves to a program inside WSL.
679 fn has_program(&self, name: &str) -> Result<bool> {
680 Ok(self.run_sh(&format!("command -v {name}"))?.status.success())
681 }
682
683 /// Whether `linux_path` exists in the (unsandboxed) WSL distro — used to
684 /// confirm that sandboxed writes did or didn't reach the host.
685 fn exists(&self, linux_path: &str) -> Result<bool> {
686 Ok(self
687 .run_sh(&format!("[ -e {} ]", shell_quote(linux_path)))?
688 .status
689 .success())
690 }
691
692 /// Translate Windows paths to their WSL paths with `wslpath -u`, in one
693 /// round-trip. Paths are passed as argv (not interpolated) so quoting is
694 /// never a concern.
695 fn wsl_paths(&self, paths: &[&Path]) -> Result<Vec<String>> {
696 let mut args: Vec<std::ffi::OsString> = vec![
697 "-c".into(),
698 "for path; do wslpath -u \"$path\" || exit 9; done".into(),
699 "zed-wslpath".into(),
700 ];
701 args.extend(paths.iter().map(|path| path.as_os_str().to_os_string()));
702
703 let output = self.run("/bin/sh", args)?;
704 ensure!(
705 output.status.success(),
706 "wslpath translation failed{}",
707 failure_details(&output)
708 );
709 let stdout = String::from_utf8_lossy(&output.stdout);
710 let resolved: Vec<String> = stdout
711 .lines()
712 .map(|line| line.trim_end_matches('\r').to_string())
713 .collect();
714 ensure!(
715 resolved.len() == paths.len(),
716 "expected {} wslpath results, got {}: {stdout:?}",
717 paths.len(),
718 resolved.len()
719 );
720 Ok(resolved)
721 }
722 }
723
724 /// Tracks assertion results so the helper can report a summary and a single
725 /// exit code, mirroring `bwrap_test_helper`.
726 struct Checks {
727 passed: usize,
728 failed: usize,
729 }
730
731 impl Checks {
732 fn new() -> Self {
733 Self {
734 passed: 0,
735 failed: 0,
736 }
737 }
738
739 fn expect(&mut self, description: &str, condition: bool) {
740 if condition {
741 self.passed += 1;
742 println!("{RESULT_TAG} PASS: {description}");
743 } else {
744 self.failed += 1;
745 println!("{RESULT_TAG} FAIL: {description}");
746 }
747 }
748
749 /// Expect the wrapped command to have run and succeeded.
750 fn expect_succeeded(&mut self, description: &str, outcome: &Outcome) {
751 let ok = matches!(
752 outcome,
753 Outcome::Ran {
754 command_succeeded: true,
755 ..
756 }
757 );
758 self.report(description, ok, outcome);
759 }
760
761 /// Expect the wrapped command to have run but been blocked (non-zero
762 /// exit) — the sandbox imposed a restriction.
763 fn expect_blocked(&mut self, description: &str, outcome: &Outcome) {
764 let ok = matches!(
765 outcome,
766 Outcome::Ran {
767 command_succeeded: false,
768 ..
769 }
770 );
771 self.report(description, ok, outcome);
772 }
773
774 fn report(&mut self, description: &str, ok: bool, outcome: &Outcome) {
775 if ok {
776 self.passed += 1;
777 println!("{RESULT_TAG} PASS: {description}");
778 } else {
779 self.failed += 1;
780 println!("{RESULT_TAG} FAIL: {description}");
781 match outcome {
782 Outcome::Ran {
783 command_succeeded,
784 stdout,
785 stderr,
786 } => println!(
787 "{RESULT_TAG} (command {}, stdout: {:?}, stderr: {:?})",
788 if *command_succeeded {
789 "succeeded"
790 } else {
791 "failed"
792 },
793 truncate(stdout),
794 truncate(stderr),
795 ),
796 Outcome::Unavailable(message) => {
797 println!("{RESULT_TAG} (sandbox reported unavailable: {message})");
798 }
799 Outcome::BadRequest(message) => {
800 println!("{RESULT_TAG} (sandbox reported bad request: {message})");
801 }
802 }
803 }
804 }
805
806 fn finish(self) -> Result<()> {
807 println!(
808 "{RESULT_TAG} summary: {} passed, {} failed",
809 self.passed, self.failed
810 );
811 if self.failed > 0 {
812 bail!("{} sandbox assertion(s) failed", self.failed);
813 }
814 Ok(())
815 }
816 }
817
818 fn failure_details(output: &Output) -> String {
819 let stderr = String::from_utf8_lossy(&output.stderr);
820 let stderr = stderr.trim();
821 let status = match output.status.code() {
822 Some(code) => format!("exit code {code}"),
823 None => "terminated by signal".to_string(),
824 };
825 if stderr.is_empty() {
826 format!(" ({status})")
827 } else {
828 format!(" ({status}; stderr: {stderr})")
829 }
830 }
831
832 /// Single-quote a value for safe interpolation into an `sh -c` script.
833 fn shell_quote(value: &str) -> String {
834 format!("'{}'", value.replace('\'', "'\\''"))
835 }
836
837 fn truncate(value: &str) -> String {
838 const LIMIT: usize = 200;
839 let trimmed = value.trim();
840 match trimmed.char_indices().nth(LIMIT) {
841 // `index` is a char boundary, so this slice can't split a code point.
842 Some((index, _)) => format!("{}…", &trimmed[..index]),
843 None => trimmed.to_string(),
844 }
845 }
846
847 fn env_flag(name: &str) -> bool {
848 std::env::var(name)
849 .map(|value| value == "1")
850 .unwrap_or(false)
851 }
852
853 /// Removes the Windows-side scratch tree on drop, so it doesn't pile up
854 /// across runs.
855 struct Cleanup(PathBuf);
856
857 impl Drop for Cleanup {
858 fn drop(&mut self) {
859 let _ = std::fs::remove_dir_all(&self.0);
860 }
861 }
862
863 /// Removes a WSL-side scratch tree on drop (best effort).
864 struct WslCleanup {
865 exe: PathBuf,
866 path: String,
867 }
868
869 impl Drop for WslCleanup {
870 fn drop(&mut self) {
871 let _ = Command::new(&self.exe)
872 .arg("--exec")
873 .arg("rm")
874 .arg("-rf")
875 .arg(&self.path)
876 .creation_flags(CREATE_NO_WINDOW)
877 .status();
878 }
879 }
880}
881