mirror of
https://github.com/openai/codex.git
synced 2026-04-27 09:51:03 +03:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user