From f71543813fc0fbf652a6efd86f65c8d723eef7db Mon Sep 17 00:00:00 2001 From: Ian MacLeod Date: Wed, 9 Sep 2026 19:20:32 +0000 Subject: [PATCH] Block WSL interop escapes from restricted filesystem sandboxes (#44286) ## Why With network access enabled, WSL interop can launch Windows processes outside the Linux filesystem sandbox, including using `wsl.exe` to reenter the distribution as root. ## What changed - Mask WSL interop sockets in bubblewrap when filesystem write access is restricted, and hide inherited procfs when running without a fresh proc mount. - Deny `AF_VSOCK` sockets and `io_uring` under restricted filesystem policies even when ordinary network access is allowed, closing the VM socket route to host services. - Reject legacy Landlock configurations that combine restricted filesystem access with unrestricted networking when the WSL interop directory exists. ## Testing Add unit coverage for mount masking and legacy policy rejection, plus WSL integration tests for Windows process launch, bind aliases, direct `/init` invocation, root reentry, and `--no-proc` isolation. WSL tests skip when host prerequisites are unavailable. GitOrigin-RevId: 1e8b97ac43675807ee541ca614534d9b1febf410 --- codex-rs/linux-sandbox/src/bwrap.rs | 17 + codex-rs/linux-sandbox/src/landlock.rs | 33 +- codex-rs/linux-sandbox/src/linux_run_main.rs | 19 + .../linux-sandbox/src/linux_run_main_tests.rs | 73 ++++ .../linux-sandbox/tests/suite/landlock.rs | 359 ++++++++++++++++++ 5 files changed, 496 insertions(+), 5 deletions(-) diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 11e8e612af..d63997d617 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -55,6 +55,7 @@ const LINUX_PLATFORM_DEFAULT_READ_ROOTS: &[&str] = &[ ]; const MAX_UNREADABLE_GLOB_MATCHES: usize = 8192; +pub(crate) const WSL_INTEROP_DIR: &str = "/run/WSL"; /// Options that control how bubblewrap is invoked. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -66,6 +67,8 @@ pub(crate) struct BwrapOptions { pub mount_proc: bool, /// How networking should be configured inside the bubblewrap sandbox. pub network_mode: BwrapNetworkMode, + /// Hide the WSL Windows interop socket from commands with restricted filesystem access. + pub mask_wsl_interop: bool, /// Optional maximum depth for expanding unreadable glob patterns with ripgrep. /// /// Keep this uncapped by default so existing nested deny-read matches are @@ -78,6 +81,7 @@ impl Default for BwrapOptions { Self { mount_proc: true, network_mode: BwrapNetworkMode::FullAccess, + mask_wsl_interop: false, glob_scan_max_depth: None, } } @@ -329,6 +333,19 @@ fn create_bwrap_flags( args.push("--new-session".to_string()); args.push("--die-with-parent".to_string()); args.extend(filesystem_args); + if options.mask_wsl_interop { + // WSL's Windows-process bridge uses Unix sockets under /run/WSL. + // Mask them after all filesystem binds so network access cannot bypass + // the Linux filesystem policy by launching an unrestricted Windows process. + args.push("--tmpfs".to_string()); + args.push(WSL_INTEROP_DIR.to_string()); + if !options.mount_proc { + // Without a fresh procfs, the root bind retains the host procfs. + // Hide it so /proc//root cannot alias the interop sockets. + args.push("--tmpfs".to_string()); + args.push("/proc".to_string()); + } + } // Request a user namespace explicitly rather than relying on bubblewrap's // auto-enable behavior, which is skipped when the caller runs as uid 0. args.push("--unshare-user".to_string()); diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index 65ba3ebf17..0d07441107 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -36,7 +36,7 @@ use seccompiler::apply_filter; /// /// This function is responsible for: /// - enabling `PR_SET_NO_NEW_PRIVS` when restrictions apply, and -/// - installing the network seccomp filter when network access is disabled. +/// - installing seccomp restrictions for network isolation and VM sockets. /// /// Filesystem restrictions are intentionally handled by bubblewrap. pub(crate) fn apply_permission_profile_to_current_thread( @@ -52,7 +52,15 @@ pub(crate) fn apply_permission_profile_to_current_thread( network_sandbox_policy, allow_network_for_proxy, proxy_routed_network, - ); + ) + .or_else(|| { + // VM sockets can reach host services outside the filesystem sandbox. + // In WSL2 they also allow Windows process launch through an alias of + // the interop socket, even when /run/WSL is masked. Keep ordinary + // network access while denying that host bridge. + (!file_system_sandbox_policy.has_full_disk_write_access()) + .then_some(NetworkSeccompMode::VmSocketRestricted) + }); // `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents // setuid privilege elevation. Many `bwrap` deployments rely on setuid, so @@ -91,6 +99,7 @@ pub(crate) fn apply_permission_profile_to_current_thread( enum NetworkSeccompMode { Restricted, ProxyRouted, + VmSocketRestricted, } fn should_install_network_seccomp( @@ -176,9 +185,13 @@ fn install_network_seccomp_filter_on_current_thread( // Build rule map. let mut rules: BTreeMap> = BTreeMap::new(); - deny_syscall(&mut rules, libc::SYS_ptrace); - deny_syscall(&mut rules, libc::SYS_process_vm_readv); - deny_syscall(&mut rules, libc::SYS_process_vm_writev); + if mode != NetworkSeccompMode::VmSocketRestricted { + deny_syscall(&mut rules, libc::SYS_ptrace); + deny_syscall(&mut rules, libc::SYS_process_vm_readv); + deny_syscall(&mut rules, libc::SYS_process_vm_writev); + } + // io_uring can create AF_VSOCK sockets without a socket() syscall, so + // keep it unavailable in every mode with socket-family restrictions. deny_syscall(&mut rules, libc::SYS_io_uring_setup); deny_syscall(&mut rules, libc::SYS_io_uring_enter); deny_syscall(&mut rules, libc::SYS_io_uring_register); @@ -245,6 +258,16 @@ fn install_network_seccomp_filter_on_current_thread( rules.insert(libc::SYS_socket, vec![deny_non_ip_socket]); rules.insert(libc::SYS_socketpair, vec![deny_non_unix_socketpair]); } + NetworkSeccompMode::VmSocketRestricted => { + let deny_vsock = SeccompRule::new(vec![SeccompCondition::new( + 0, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::AF_VSOCK as u64, + )?])?; + rules.insert(libc::SYS_socket, vec![deny_vsock.clone()]); + rules.insert(libc::SYS_socketpair, vec![deny_vsock]); + } } let filter = SeccompFilter::new( diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 43dfbb99fb..bcdaa23ba0 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -22,6 +22,7 @@ use std::time::Duration; use crate::bwrap::BwrapNetworkMode; use crate::bwrap::BwrapOptions; +use crate::bwrap::WSL_INTEROP_DIR; use crate::bwrap::create_bwrap_command_args; use crate::landlock::apply_permission_profile_to_current_thread; use crate::launcher::exec_bwrap; @@ -186,7 +187,9 @@ pub fn run_main() -> ! { use_legacy_landlock, &file_system_sandbox_policy, network_sandbox_policy, + allow_network_for_proxy, &sandbox_policy_cwd, + Path::new(WSL_INTEROP_DIR), ); // Inner stage: apply seccomp/no_new_privs after bubblewrap has already @@ -378,7 +381,9 @@ fn ensure_legacy_landlock_mode_supports_policy( use_legacy_landlock: bool, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, + allow_network_for_proxy: bool, sandbox_policy_cwd: &Path, + wsl_interop_dir: &Path, ) { if use_legacy_landlock && file_system_sandbox_policy @@ -388,6 +393,18 @@ fn ensure_legacy_landlock_mode_supports_policy( "permission profiles requiring direct runtime enforcement are incompatible with --use-legacy-landlock" ); } + if use_legacy_landlock + && network_sandbox_policy.is_enabled() + && !allow_network_for_proxy + && !file_system_sandbox_policy.has_full_disk_write_access() + // Interop can use another binfmt handler or a newly created socket. + // An active endpoint probe would race with sandboxed command startup. + && wsl_interop_dir.is_dir() + { + panic!( + "legacy Landlock cannot isolate WSL Windows interop with network access enabled; use bubblewrap or restrict network access" + ); + } } fn run_bwrap_with_proc_fallback( @@ -414,6 +431,8 @@ fn run_bwrap_with_proc_fallback( let options = BwrapOptions { mount_proc, network_mode, + mask_wsl_interop: !file_system_sandbox_policy.has_full_disk_write_access() + && Path::new(WSL_INTEROP_DIR).is_dir(), ..Default::default() }; let mut bwrap_args = build_bwrap_argv( diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index e6fe9b3134..77171a0289 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -203,6 +203,48 @@ fn inserts_unshare_net_when_proxy_only_network_mode_requested() { assert!(argv.contains(&"--unshare-net".to_string())); } +#[test] +fn masks_wsl_interop_with_full_network_and_restricted_filesystem() { + let argv = build_bwrap_argv( + vec!["/bin/true".to_string()], + &read_only_file_system_policy(), + Path::new("/"), + Path::new("/"), + BwrapOptions { + network_mode: BwrapNetworkMode::FullAccess, + mask_wsl_interop: true, + ..Default::default() + }, + ) + .expect("build bwrap argv") + .args; + + assert!(argv.windows(2).any(|args| args == ["--tmpfs", "/run/WSL"])); + assert!(!argv.iter().any(|arg| arg == "--unshare-net")); +} + +#[test] +fn masks_inherited_procfs_when_wsl_interop_is_masked_without_fresh_proc() { + let argv = build_bwrap_argv( + vec!["/bin/true".to_string()], + &read_only_file_system_policy(), + Path::new("/"), + Path::new("/"), + BwrapOptions { + mount_proc: false, + network_mode: BwrapNetworkMode::FullAccess, + mask_wsl_interop: true, + ..Default::default() + }, + ) + .expect("build bwrap argv") + .args; + + assert!(argv.windows(2).any(|args| args == ["--tmpfs", "/run/WSL"])); + assert!(argv.windows(2).any(|args| args == ["--tmpfs", "/proc"])); + assert!(!argv.windows(2).any(|args| args == ["--proc", "/proc"])); +} + #[test] fn proxy_only_mode_takes_precedence_over_full_network_policy() { let mode = bwrap_network_mode( @@ -673,13 +715,44 @@ fn legacy_landlock_rejects_split_only_filesystem_policies() { /*use_legacy_landlock*/ true, &policy, NetworkSandboxPolicy::Restricted, + /*allow_network_for_proxy*/ false, temp_dir.path(), + &temp_dir.path().join("WSL"), ); }); assert!(result.is_err()); } +#[test] +fn legacy_landlock_rejects_full_network_when_wsl_interop_is_available() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let wsl_interop_dir = temp_dir.path().join("WSL"); + std::fs::create_dir(&wsl_interop_dir).expect("create interop directory"); + let policy = read_only_file_system_policy(); + + let result = std::panic::catch_unwind(|| { + ensure_legacy_landlock_mode_supports_policy( + /*use_legacy_landlock*/ true, + &policy, + NetworkSandboxPolicy::Enabled, + /*allow_network_for_proxy*/ false, + temp_dir.path(), + &wsl_interop_dir, + ); + }); + assert!(result.is_err()); + + ensure_legacy_landlock_mode_supports_policy( + /*use_legacy_landlock*/ true, + &policy, + NetworkSandboxPolicy::Enabled, + /*allow_network_for_proxy*/ true, + temp_dir.path(), + &wsl_interop_dir, + ); +} + #[test] fn valid_inner_stage_modes_do_not_panic() { ensure_inner_stage_mode_is_valid( diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index a82022754d..56c0180124 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -21,6 +21,8 @@ use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::path::PathBuf; +use std::process::Output; +use std::time::Duration; use tempfile::NamedTempFile; // At least on GitHub CI, the arm64 tests appear to need longer timeouts. @@ -251,6 +253,363 @@ fn expect_denied( } } +async fn wsl_windows_executable(name: &str) -> Option { + if !std::path::Path::new("/run/WSL").is_dir() { + return None; + } + + let windows_path = format!(r"C:\Windows\System32\{name}"); + let output = tokio::time::timeout( + Duration::from_millis(NETWORK_TIMEOUT_MS), + tokio::process::Command::new("wslpath") + .args(["-u", &windows_path]) + .kill_on_drop(true) + .output(), + ) + .await + .ok()? + .ok()?; + if !output.status.success() { + return None; + } + let path = String::from_utf8(output.stdout).ok()?.trim().to_string(); + (!path.is_empty()).then_some(path) +} + +async fn wsl_baseline_output(executable: &str, args: &[&str]) -> Option { + tokio::time::timeout( + Duration::from_millis(NETWORK_TIMEOUT_MS), + tokio::process::Command::new(executable) + .args(args) + .kill_on_drop(true) + .output(), + ) + .await + .ok()? + .ok() + .filter(|output| output.status.success()) +} + +#[tokio::test] +async fn wsl_interop_cannot_run_windows_program_with_network_access() { + let Some(powershell) = wsl_windows_executable(r"WindowsPowerShell\v1.0\powershell.exe").await + else { + return; + }; + let args = [ + "-NoProfile", + "-NonInteractive", + "-Command", + "Write-Output interop-escaped", + ]; + if wsl_baseline_output(&powershell, &args).await.is_none() { + eprintln!("skipping WSL interop test: Windows interop is unavailable on the host"); + return; + } + if should_skip_bwrap_tests().await { + eprintln!("skipping WSL interop test: bwrap sandbox prerequisites are unavailable"); + return; + } + + let output = expect_denied( + run_cmd_result_with_writable_roots( + &[&powershell, args[0], args[1], args[2], args[3]], + &[], + NETWORK_TIMEOUT_MS, + /*use_legacy_landlock*/ false, + /*network_access*/ true, + ) + .await, + "WSL interop must not start a Windows program inside a restricted filesystem sandbox", + ); + assert!(!output.stdout.text.contains("interop-escaped")); +} + +#[tokio::test] +async fn wsl_interop_bind_alias_cannot_run_windows_program() { + let Some(powershell) = wsl_windows_executable(r"WindowsPowerShell\v1.0\powershell.exe").await + else { + return; + }; + if wsl_baseline_output( + &powershell, + &["-NoProfile", "-NonInteractive", "-Command", "exit 0"], + ) + .await + .is_none() + || should_skip_bwrap_tests().await + || !std::path::Path::new("/usr/bin/unshare").exists() + || !std::path::Path::new("/usr/bin/mount").exists() + { + return; + } + let Ok(interop_socket) = std::env::var("WSL_INTEROP") else { + eprintln!("skipping WSL bind alias test: WSL_INTEROP is unavailable"); + return; + }; + if !std::path::Path::new(&interop_socket).exists() { + eprintln!("skipping WSL bind alias test: interop socket is unavailable"); + return; + } + + let mount_dir = tempfile::tempdir().expect("create mount target"); + let alias = mount_dir.path().join("WSL"); + std::fs::create_dir(&alias).expect("create interop bind target"); + let namespace_probe = tokio::time::timeout( + Duration::from_millis(NETWORK_TIMEOUT_MS), + tokio::process::Command::new("unshare") + .args([ + "--user", + "--map-root-user", + "--mount", + "--propagation", + "private", + "--", + "/bin/sh", + "-c", + "mount --bind /run/WSL \"$1\" && umount \"$1\"", + "sh", + ]) + .arg(&alias) + .kill_on_drop(true) + .output(), + ) + .await; + let Ok(Ok(probe)) = namespace_probe else { + eprintln!("skipping WSL bind alias test: user/mount namespace probe could not run"); + return; + }; + if !probe.status.success() { + eprintln!( + "skipping WSL bind alias test: user/mount namespace unavailable: {}", + String::from_utf8_lossy(&probe.stderr) + ); + return; + } + + let profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::read_only(), + NetworkSandboxPolicy::Enabled, + ); + let profile = serde_json::to_string(&profile).expect("serialize permission profile"); + let cwd = std::env::current_dir().expect("current directory"); + let script = r#" + mount --bind /run/WSL "$1" + trap 'umount "$1"' EXIT + export WSL_INTEROP="$1/${WSL_INTEROP##*/}" + "$2" -NoProfile -NonInteractive -Command 'Write-Output alias-baseline-ok' + bash -c 'exec -a "$1" /init "$1" -NoProfile -NonInteractive -Command "Write-Output direct-init-baseline-ok"' bash "$2" + "$3" --sandbox-policy-cwd "$4" --permission-profile "$5" -- \ + "$2" -NoProfile -NonInteractive -Command 'Write-Output interop-escaped' + status=$? + printf 'sandbox-exit=%s\n' "$status" + if test "$status" -eq 0; then exit 1; fi + "$3" --sandbox-policy-cwd "$4" --permission-profile "$5" -- \ + bash -c 'exec -a "$1" /init "$1" -NoProfile -NonInteractive -Command "Write-Output direct-init-escaped"' bash "$2" + status=$? + printf 'direct-init-exit=%s\n' "$status" + test "$status" -ne 0 + "#; + let output = tokio::time::timeout( + Duration::from_millis(NETWORK_TIMEOUT_MS * 3), + tokio::process::Command::new("unshare") + .args([ + "--user", + "--map-root-user", + "--mount", + "--propagation", + "private", + "--", + ]) + .args(["/bin/sh", "-c", script, "sh"]) + .arg(&alias) + .arg(&powershell) + .arg(codex_linux_sandbox_exe()) + .arg(&cwd) + .arg(profile) + .kill_on_drop(true) + .output(), + ) + .await + .expect("WSL bind alias test should finish") + .expect("unshare should start"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success(), + "WSL bind alias must be isolated: stdout={stdout} stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(stdout.contains("alias-baseline-ok"), "stdout={stdout}"); + assert!( + stdout.contains("direct-init-baseline-ok"), + "stdout={stdout}" + ); + assert!(stdout.contains("sandbox-exit="), "stdout={stdout}"); + assert!(stdout.contains("direct-init-exit="), "stdout={stdout}"); + assert!(!stdout.contains("interop-escaped"), "stdout={stdout}"); + assert!(!stdout.contains("direct-init-escaped"), "stdout={stdout}"); +} + +#[tokio::test] +async fn wsl_no_proc_masks_inherited_procfs_and_windows_interop() { + let Some(powershell) = wsl_windows_executable(r"WindowsPowerShell\v1.0\powershell.exe").await + else { + return; + }; + let args = [ + "-NoProfile", + "-NonInteractive", + "-Command", + "Write-Output interop-escaped", + ]; + if wsl_baseline_output(&powershell, &args).await.is_none() || should_skip_bwrap_tests().await { + return; + } + + let profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::read_only(), + NetworkSandboxPolicy::Enabled, + ); + let profile = serde_json::to_string(&profile).expect("serialize permission profile"); + let cwd = std::env::current_dir().expect("current directory"); + let proc_check = tokio::time::timeout( + Duration::from_millis(NETWORK_TIMEOUT_MS), + tokio::process::Command::new(codex_linux_sandbox_exe()) + .arg("--sandbox-policy-cwd") + .arg(&cwd) + .args([ + "--permission-profile", + &profile, + "--no-proc", + "--", + "/bin/sh", + "-c", + "test ! -e /proc/1", + ]) + .kill_on_drop(true) + .output(), + ) + .await + .expect("procfs check should finish") + .expect("sandbox helper should start"); + assert!( + proc_check.status.success(), + "inherited procfs was not masked: {}", + String::from_utf8_lossy(&proc_check.stderr) + ); + + let output = tokio::time::timeout( + Duration::from_millis(NETWORK_TIMEOUT_MS), + tokio::process::Command::new(codex_linux_sandbox_exe()) + .arg("--sandbox-policy-cwd") + .arg(&cwd) + .args([ + "--permission-profile", + &profile, + "--no-proc", + "--", + &powershell, + ]) + .args(args) + .kill_on_drop(true) + .output(), + ) + .await + .expect("sandbox command should finish") + .expect("sandbox helper should start"); + assert!(!output.status.success()); + assert!(!String::from_utf8_lossy(&output.stdout).contains("interop-escaped")); +} + +#[tokio::test] +async fn wsl_interop_cannot_reenter_distro_as_root_with_network_access() { + let Some(wsl) = wsl_windows_executable("wsl.exe").await else { + return; + }; + let Ok(distro) = std::env::var("WSL_DISTRO_NAME") else { + eprintln!("skipping WSL root test: distro name is unavailable"); + return; + }; + let args = ["-d", &distro, "-u", "root", "--", "/usr/bin/id", "-u"]; + let Some(baseline) = wsl_baseline_output(&wsl, &args).await else { + eprintln!("skipping WSL root test: Windows interop is unavailable on the host"); + return; + }; + assert_eq!(baseline.stdout, b"0\n"); + if should_skip_bwrap_tests().await { + eprintln!("skipping WSL root test: bwrap sandbox prerequisites are unavailable"); + return; + } + + let output = expect_denied( + run_cmd_result_with_writable_roots( + &[ + &wsl, args[0], args[1], args[2], args[3], args[4], args[5], args[6], + ], + &[], + NETWORK_TIMEOUT_MS, + /*use_legacy_landlock*/ false, + /*network_access*/ true, + ) + .await, + "WSL interop must not start a new root session inside a restricted filesystem sandbox", + ); + assert_ne!(output.stdout.text.trim(), "0"); +} + +#[tokio::test] +async fn legacy_landlock_rejects_wsl_interop_with_network_access() { + let Some(powershell) = wsl_windows_executable(r"WindowsPowerShell\v1.0\powershell.exe").await + else { + return; + }; + if wsl_baseline_output( + &powershell, + &["-NoProfile", "-NonInteractive", "-Command", "exit 0"], + ) + .await + .is_none() + { + eprintln!("skipping legacy WSL interop test: Windows interop is unavailable on the host"); + return; + } + + let safe_profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::read_only(), + NetworkSandboxPolicy::Restricted, + ); + let linux_output = run_cmd_result_with_permission_profile( + &["/bin/true"], + safe_profile, + NETWORK_TIMEOUT_MS, + /*use_legacy_landlock*/ true, + ) + .await + .expect("legacy Landlock should run a Linux command"); + assert_eq!(linux_output.exit_code, 0); + + let unsafe_profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::read_only(), + NetworkSandboxPolicy::Enabled, + ); + let output = expect_denied( + run_cmd_result_with_permission_profile( + &["/bin/true"], + unsafe_profile, + NETWORK_TIMEOUT_MS, + /*use_legacy_landlock*/ true, + ) + .await, + "legacy Landlock must reject restricted filesystem access with full network access on WSL", + ); + assert!( + output + .stderr + .text + .contains("legacy Landlock cannot isolate WSL Windows interop") + ); +} + #[tokio::test] async fn test_root_read() { run_cmd(&["ls", "-l", "/bin"], &[], SHORT_TIMEOUT_MS).await;