fix(sandboxing): reject WSL1 bubblewrap sandboxing (#17559)

## Summary

- detect WSL1 before Codex probes or invokes the Linux bubblewrap
sandbox
- fail early with a clear unsupported-operation message when a command
would require bubblewrap on WSL1
- document that WSL2 follows the normal Linux bubblewrap path while WSL1
is unsupported

## Why

Codex 0.115.0 made bubblewrap the default Linux sandbox. WSL1 cannot
create the user namespaces that bubblewrap needs, so shell commands
currently fail later with a raw bwrap namespace error. This makes the
unsupported environment explicit and keeps non-bubblewrap paths
unchanged.

The WSL detection reads /proc/version, lets an explicit WSL<version>
marker decide WSL1 vs WSL2+, and only treats a bare Microsoft marker as
WSL1 when no explicit WSL version is present.

addresses https://github.com/openai/codex/issues/16076

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-04-12 14:08:14 -07:00
committed by GitHub
parent a4d5112b37
commit cb870a169a
7 changed files with 169 additions and 2 deletions

View File

@@ -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

View File

@@ -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`)

View File

@@ -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<String> {
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::<u32>() {
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

View File

@@ -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");

View File

@@ -34,6 +34,10 @@ impl From<SandboxTransformError> 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(),

View File

@@ -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<OsString>) -> Vec<String> {
argv.into_iter()
.map(os_string_to_command_component)

View File

@@ -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() {