diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 63d9cba53..e1eab3493 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -42,7 +42,11 @@ switches to a no-`--argv0` compatibility path for the inner re-exec. If `bwrap` is missing, it falls back to the vendored bubblewrap path compiled into the binary and Codex surfaces a startup warning through its normal notification path instead of printing directly from the sandbox helper. Codex also surfaces -a startup warning when bubblewrap cannot create user namespaces. +a startup warning when bubblewrap cannot create user namespaces. WSL2 uses the +normal Linux bubblewrap path. WSL1 is not supported for bubblewrap sandboxing +because it cannot create the required user namespaces, so Codex rejects +sandboxed shell commands that would enter the bubblewrap path before invoking +`bwrap`. ### Windows diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index 340b10910..3a004934b 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -16,7 +16,10 @@ the helper falls back to the vendored bubblewrap path compiled into this binary. Codex also surfaces a startup warning when `bwrap` is missing so users know it is falling back to the vendored helper. Codex surfaces the same startup warning -path when bubblewrap cannot create user namespaces. +path when bubblewrap cannot create user namespaces. WSL2 follows the normal +Linux bubblewrap path. WSL1 is not supported for bubblewrap sandboxing because +it cannot create the required user namespaces, so Codex rejects sandboxed shell +commands that would enter the bubblewrap path. **Current Behavior** - Legacy `SandboxPolicy` / `sandbox_mode` configs remain supported. @@ -31,6 +34,9 @@ path when bubblewrap cannot create user namespaces. printing directly from the sandbox helper. - If bubblewrap cannot create user namespaces, Codex surfaces a startup warning instead of waiting for a runtime sandbox failure. +- WSL2 uses the normal Linux bubblewrap path. +- WSL1 is not supported for bubblewrap sandboxing; Codex rejects sandboxed + shell commands that would require the bubblewrap path before invoking `bwrap`. - Legacy Landlock + mount protections remain available as an explicit legacy fallback path. - Set `features.use_legacy_landlock = true` (or CLI `-c use_legacy_landlock=true`) diff --git a/codex-rs/sandboxing/src/bwrap.rs b/codex-rs/sandboxing/src/bwrap.rs index 01d53ebbe..069807f98 100644 --- a/codex-rs/sandboxing/src/bwrap.rs +++ b/codex-rs/sandboxing/src/bwrap.rs @@ -14,6 +14,11 @@ const MISSING_BWRAP_WARNING: &str = concat!( ); const USER_NAMESPACE_WARNING: &str = "Codex's Linux sandbox uses bubblewrap and needs access to create user namespaces."; +pub(crate) const WSL1_BWRAP_WARNING: &str = concat!( + "Codex's Linux sandbox uses bubblewrap, which is not supported on WSL1 ", + "because WSL1 cannot create the required user namespaces. ", + "Use WSL2 for sandboxed shell commands." +); const USER_NAMESPACE_FAILURES: [&str; 4] = [ "loopback: Failed RTM_NEWADDR", "loopback: Failed RTM_NEWLINK", @@ -38,6 +43,10 @@ fn should_warn_about_system_bwrap(sandbox_policy: &SandboxPolicy) -> bool { } fn system_bwrap_warning_for_path(system_bwrap_path: Option<&Path>) -> Option { + if is_wsl1() { + return Some(WSL1_BWRAP_WARNING.to_string()); + } + let Some(system_bwrap_path) = system_bwrap_path else { return Some(MISSING_BWRAP_WARNING.to_string()); }; @@ -68,6 +77,29 @@ fn system_bwrap_has_user_namespace_access(system_bwrap_path: &Path) -> bool { output.status.success() || !is_user_namespace_failure(&output) } +pub(crate) fn is_wsl1() -> bool { + std::fs::read_to_string("/proc/version") + .is_ok_and(|proc_version| proc_version_indicates_wsl1(&proc_version)) +} + +fn proc_version_indicates_wsl1(proc_version: &str) -> bool { + let proc_version = proc_version.to_ascii_lowercase(); + let mut remaining = proc_version.as_str(); + while let Some(marker) = remaining.find("wsl") { + let version_start = marker + "wsl".len(); + let version_digits: String = remaining[version_start..] + .chars() + .take_while(char::is_ascii_digit) + .collect(); + if let Ok(version) = version_digits.parse::() { + return version == 1; + } + remaining = &remaining[version_start..]; + } + + proc_version.contains("microsoft") && !proc_version.contains("microsoft-standard") +} + fn is_user_namespace_failure(output: &Output) -> bool { let stderr = String::from_utf8_lossy(&output.stderr); USER_NAMESPACE_FAILURES diff --git a/codex-rs/sandboxing/src/bwrap_tests.rs b/codex-rs/sandboxing/src/bwrap_tests.rs index b0303e2b7..f36848e1e 100644 --- a/codex-rs/sandboxing/src/bwrap_tests.rs +++ b/codex-rs/sandboxing/src/bwrap_tests.rs @@ -44,6 +44,36 @@ exit 1 assert_eq!(system_bwrap_warning_for_path(Some(fake_bwrap_path)), None); } +#[test] +fn detects_wsl1_proc_version_formats() { + assert!(proc_version_indicates_wsl1( + "Linux version 4.4.0-22621-Microsoft" + )); + assert!(proc_version_indicates_wsl1( + "Linux version 5.15.0-microsoft-standard-WSL1" + )); + assert!(proc_version_indicates_wsl1( + "Linux version 5.15.0-wsl-microsoft-standard-WSL1" + )); +} + +#[test] +fn does_not_treat_wsl2_or_native_linux_as_wsl1() { + assert!(!proc_version_indicates_wsl1( + "Linux version 6.6.87.2-microsoft-standard-WSL2" + )); + assert!(!proc_version_indicates_wsl1( + "Linux version 6.6.87.2-wsl-microsoft-standard-WSL2" + )); + assert!(!proc_version_indicates_wsl1( + "Linux version 4.19.104-microsoft-standard" + )); + assert!(!proc_version_indicates_wsl1( + "Linux version 6.6.87.2-microsoft-standard-WSL3" + )); + assert!(!proc_version_indicates_wsl1("Linux version 6.8.0")); +} + #[test] fn finds_first_executable_bwrap_in_joined_search_path() { let temp_dir = tempdir().expect("temp dir"); diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index 244f65bb0..38aed6ce2 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -34,6 +34,10 @@ impl From for CodexErr { SandboxTransformError::MissingLinuxSandboxExecutable => { CodexErr::LandlockSandboxExecutableNotProvided } + #[cfg(target_os = "linux")] + SandboxTransformError::Wsl1UnsupportedForBubblewrap => { + CodexErr::UnsupportedOperation(crate::bwrap::WSL1_BWRAP_WARNING.to_string()) + } #[cfg(not(target_os = "macos"))] SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation( "seatbelt sandbox is only available on macOS".to_string(), diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 4ed413735..ebff33f10 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -1,3 +1,7 @@ +#[cfg(target_os = "linux")] +use crate::bwrap::WSL1_BWRAP_WARNING; +#[cfg(target_os = "linux")] +use crate::bwrap::is_wsl1; use crate::landlock::CODEX_LINUX_SANDBOX_ARG0; use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args_for_policies; @@ -109,6 +113,8 @@ pub struct SandboxTransformRequest<'a> { #[derive(Debug)] pub enum SandboxTransformError { MissingLinuxSandboxExecutable, + #[cfg(target_os = "linux")] + Wsl1UnsupportedForBubblewrap, #[cfg(not(target_os = "macos"))] SeatbeltUnavailable, } @@ -119,6 +125,8 @@ impl std::fmt::Display for SandboxTransformError { Self::MissingLinuxSandboxExecutable => { write!(f, "missing codex-linux-sandbox executable path") } + #[cfg(target_os = "linux")] + Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"), #[cfg(not(target_os = "macos"))] Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"), } @@ -219,6 +227,13 @@ impl SandboxManager { let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; let allow_proxy_network = allow_network_for_proxy(enforce_managed_network); + #[cfg(target_os = "linux")] + ensure_linux_bubblewrap_is_supported( + &effective_file_system_policy, + use_legacy_landlock, + allow_proxy_network, + is_wsl1(), + )?; let mut args = create_linux_sandbox_command_args_for_policies( os_argv_to_strings(argv), command.cwd.as_path(), @@ -256,6 +271,22 @@ impl SandboxManager { } } +#[cfg(target_os = "linux")] +fn ensure_linux_bubblewrap_is_supported( + file_system_sandbox_policy: &FileSystemSandboxPolicy, + use_legacy_landlock: bool, + allow_network_for_proxy: bool, + is_wsl1: bool, +) -> Result<(), SandboxTransformError> { + let requires_bubblewrap = !use_legacy_landlock + && (!file_system_sandbox_policy.has_full_disk_write_access() || allow_network_for_proxy); + if is_wsl1 && requires_bubblewrap { + return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap); + } + + Ok(()) +} + fn os_argv_to_strings(argv: Vec) -> Vec { argv.into_iter() .map(os_string_to_command_component) diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 39531f13b..f4e019ee2 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -275,6 +275,66 @@ fn transform_linux_seccomp_request( .expect("transform") } +#[cfg(target_os = "linux")] +#[test] +fn wsl1_rejects_linux_bubblewrap_path() { + let restricted_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }]); + + assert!(matches!( + super::ensure_linux_bubblewrap_is_supported( + &restricted_policy, + /*use_legacy_landlock*/ false, + /*allow_network_for_proxy*/ false, + /*is_wsl1*/ true, + ), + Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap) + )); + assert!(matches!( + super::ensure_linux_bubblewrap_is_supported( + &FileSystemSandboxPolicy::unrestricted(), + /*use_legacy_landlock*/ false, + /*allow_network_for_proxy*/ true, + /*is_wsl1*/ true, + ), + Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap) + )); +} + +#[cfg(target_os = "linux")] +#[test] +fn wsl1_allows_non_bubblewrap_linux_paths() { + assert!( + super::ensure_linux_bubblewrap_is_supported( + &FileSystemSandboxPolicy::unrestricted(), + /*use_legacy_landlock*/ false, + /*allow_network_for_proxy*/ false, + /*is_wsl1*/ true, + ) + .is_ok() + ); + + let restricted_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }]); + assert!( + super::ensure_linux_bubblewrap_is_supported( + &restricted_policy, + /*use_legacy_landlock*/ true, + /*allow_network_for_proxy*/ false, + /*is_wsl1*/ true, + ) + .is_ok() + ); +} + #[cfg(target_os = "linux")] #[test] fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() {