Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3855146
Fix old system bwrap fallback by avoiding --argv0
viyatb-oai Mar 24, 2026
e048d63
fix: preserve codex-linux-sandbox argv0 dispatch fallback
viyatb-oai Mar 25, 2026
dcc22b4
test: trim redundant system bwrap warning coverage
viyatb-oai Mar 25, 2026
2502f96
fix(linux-sandbox): preserve inner command in old bwrap fallback
viyatb-oai Mar 25, 2026
89bff52
fix(linux-sandbox): rewrite helper argv0 at bwrap command boundary
viyatb-oai Mar 25, 2026
a17aa0e
fix(linux-sandbox): restore legacy policy mismatch rejection
viyatb-oai Mar 25, 2026
c04464b
fix(core): run apply_patch through codex binary
viyatb-oai Mar 25, 2026
cfa5fd7
Merge branch 'main' into codex/viyatb/fix-argv0-bubblewrap-fallback
viyatb-oai Mar 25, 2026
614c559
fix(core): preserve codex binary for macOS apply_patch
viyatb-oai Mar 25, 2026
b09e9ad
fix(core): cfg-gate macOS apply_patch codex path
viyatb-oai Mar 25, 2026
a40b50e
Merge remote-tracking branch 'origin/main' into codex/viyatb/fix-argv…
viyatb-oai Mar 25, 2026
29950a7
fix(core): drop macOS apply_patch cfg split
viyatb-oai Mar 25, 2026
70df627
style(arg0): rename path entry guard parameter
viyatb-oai Mar 25, 2026
3e14bf6
fix(core): resolve current exe for apply_patch
viyatb-oai Mar 25, 2026
e869060
refactor(core): share linux sandbox arg0 constant
viyatb-oai Mar 25, 2026
4c19b46
fix(core): use shared linux sandbox arg0 constant
viyatb-oai Mar 25, 2026
b184bef
Merge branch 'main' into codex/viyatb/fix-argv0-bubblewrap-fallback
viyatb-oai Mar 25, 2026
e03f62b
fix test
bolinfest Mar 25, 2026
151a09e
more platform-specific behavior
bolinfest Mar 26, 2026
1605672
Fix apply_patch self-exec paths in tests and sandbox manager
bolinfest Mar 26, 2026
72a0d34
Handle apply_patch self-exec from Bazel test env
bolinfest Mar 26, 2026
d0354b2
Fix apply_patch clippy closure
bolinfest Mar 26, 2026
0cfa954
fix(core): add explicit codex self exe for apply_patch
viyatb-oai Mar 26, 2026
f194692
test(core): skip arg0 alias setup for helper re-execs
viyatb-oai Mar 26, 2026
62336e0
test(core): narrow ctor unsafe scope
viyatb-oai Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/arg0/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ workspace = true
anyhow = { workspace = true }
codex-apply-patch = { workspace = true }
codex-linux-sandbox = { workspace = true }
codex-sandboxing = { workspace = true }
codex-shell-escalation = { workspace = true }
codex-utils-home-dir = { workspace = true }
dotenvy = { workspace = true }
Expand Down
61 changes: 48 additions & 13 deletions codex-rs/arg0/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ use std::path::Path;
use std::path::PathBuf;

use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
use codex_utils_home_dir::find_codex_home;
#[cfg(unix)]
use std::os::unix::fs::symlink;
use tempfile::TempDir;

const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox";
const APPLY_PATCH_ARG0: &str = "apply_patch";
const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
#[cfg(unix)]
Expand Down Expand Up @@ -79,7 +79,7 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
}
}

if exe_name == LINUX_SANDBOX_ARG0 {
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
// Safety: [`run_main`] never returns.
codex_linux_sandbox::run_main();
} else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
Expand Down Expand Up @@ -133,8 +133,9 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
///
/// 1. Load `.env` values from `~/.codex/.env` before creating any threads.
/// 2. Construct a Tokio multi-thread runtime.
/// 3. Derive the path to the current executable (so children can re-invoke the
/// sandbox) when running on Linux.
/// 3. Derive the `codex-linux-sandbox` helper path (falling back to the
/// current executable if needed) so children can re-invoke the sandbox when
/// running on Linux.
/// 4. Execute the provided async `main_fn` inside that runtime, forwarding any
/// error. Note that `main_fn` receives [`Arg0DispatchPaths`], which
/// contains the helper executable paths needed to construct
Expand All @@ -150,7 +151,7 @@ where
// Retain the TempDir so it exists for the lifetime of the invocation of
// this executable. Admittedly, we could invoke `keep()` on it, but it
// would be nice to avoid leaving temporary directories behind, if possible.
let path_entry = arg0_dispatch();
let path_entry_guard = arg0_dispatch();

// Regular invocation – create a Tokio runtime and execute the provided
// async entry-point.
Expand All @@ -159,15 +160,11 @@ where
let current_exe = std::env::current_exe().ok();
let paths = Arg0DispatchPaths {
codex_linux_sandbox_exe: if cfg!(target_os = "linux") {
current_exe.or_else(|| {
Comment thread
viyatb-oai marked this conversation as resolved.
path_entry
.as_ref()
.and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone())
})
linux_sandbox_exe_path(path_entry_guard.as_ref(), current_exe)
} else {
None
},
main_execve_wrapper_exe: path_entry
main_execve_wrapper_exe: path_entry_guard
.as_ref()
.and_then(|path_entry| path_entry.paths().main_execve_wrapper_exe.clone()),
};
Expand All @@ -176,6 +173,18 @@ where
})
}

fn linux_sandbox_exe_path(
path_entry_guard: Option<&Arg0PathEntryGuard>,
current_exe: Option<PathBuf>,
) -> Option<PathBuf> {
// Prefer the `codex-linux-sandbox` alias when available so callers can
// re-exec through a path whose basename still triggers arg0 dispatch on
// bubblewrap builds that do not support `--argv0`.
path_entry_guard
.and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone())
.or(current_exe)
}

fn build_runtime() -> anyhow::Result<tokio::runtime::Runtime> {
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_all();
Expand Down Expand Up @@ -276,7 +285,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGu
APPLY_PATCH_ARG0,
MISSPELLED_APPLY_PATCH_ARG0,
#[cfg(target_os = "linux")]
LINUX_SANDBOX_ARG0,
CODEX_LINUX_SANDBOX_ARG0,
#[cfg(unix)]
EXECVE_WRAPPER_ARG0,
] {
Expand Down Expand Up @@ -329,7 +338,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGu
codex_linux_sandbox_exe: {
#[cfg(target_os = "linux")]
{
Some(path.join(LINUX_SANDBOX_ARG0))
Some(path.join(CODEX_LINUX_SANDBOX_ARG0))
}
#[cfg(not(target_os = "linux"))]
{
Expand Down Expand Up @@ -397,11 +406,16 @@ fn try_lock_dir(dir: &Path) -> std::io::Result<Option<File>> {

#[cfg(test)]
mod tests {
use super::Arg0DispatchPaths;
use super::Arg0PathEntryGuard;
use super::LOCK_FILENAME;
use super::janitor_cleanup;
use super::linux_sandbox_exe_path;
use std::fs;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;

fn create_lock(dir: &Path) -> std::io::Result<File> {
let lock_path = dir.join(LOCK_FILENAME);
Expand All @@ -413,6 +427,27 @@ mod tests {
.open(lock_path)
}

#[test]
fn linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let lock_file = create_lock(temp_dir.path())?;
let alias_path = temp_dir.path().join("codex-linux-sandbox");
let path_entry = Arg0PathEntryGuard::new(
temp_dir,
lock_file,
Arg0DispatchPaths {
codex_linux_sandbox_exe: Some(alias_path.clone()),
main_execve_wrapper_exe: None,
},
);

assert_eq!(
linux_sandbox_exe_path(Some(&path_entry), Some(PathBuf::from("/usr/bin/codex"))),
Some(alias_path),
);
Ok(())
}

#[test]
fn janitor_skips_dirs_without_lock_file() -> std::io::Result<()> {
let root = tempfile::tempdir()?;
Expand Down
9 changes: 5 additions & 4 deletions codex-rs/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ only when the split filesystem policy round-trips through the legacy
cases like `/repo = write`, `/repo/a = none`, `/repo/a/b = write`, where the
more specific writable child must reopen under a denied parent.

The Linux sandbox helper prefers `/usr/bin/bwrap` whenever it is available and
supports the required argv-rewrite flags, and falls back to the vendored
bubblewrap path compiled into the binary otherwise. When `/usr/bin/bwrap` is
missing or too old to support the required flags, Codex also surfaces a startup
The Linux sandbox helper prefers `/usr/bin/bwrap` whenever it is available. If
`/usr/bin/bwrap` is present but too old to support `--argv0`, the helper keeps
using system bubblewrap and switches to a no-`--argv0` compatibility path for
the inner re-exec. If `/usr/bin/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.

Expand Down
25 changes: 5 additions & 20 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5615,7 +5615,7 @@ fn system_bwrap_warning_reports_missing_system_bwrap() {

#[cfg(target_os = "linux")]
#[test]
fn system_bwrap_warning_reports_too_old_system_bwrap() {
fn system_bwrap_warning_skips_too_old_system_bwrap() {
let fake_bwrap = write_fake_bwrap(
r#"#!/bin/sh
if [ "$1" = "--help" ]; then
Expand All @@ -5626,27 +5626,12 @@ exit 1
"#,
);
let fake_bwrap_path: &Path = fake_bwrap.as_ref();
let warning = system_bwrap_warning_for_path(fake_bwrap_path)
.expect("old system bwrap should emit a warning");

assert!(warning.contains("too old to support `--argv0`"));
}

#[cfg(target_os = "linux")]
#[test]
fn system_bwrap_warning_skips_supported_system_bwrap() {
let fake_bwrap = write_fake_bwrap(
r#"#!/bin/sh
if [ "$1" = "--help" ]; then
echo ' --argv0 PROGRAM'
exit 0
fi
exit 1
"#,
assert_eq!(
system_bwrap_warning_for_path(fake_bwrap_path),
None,
"Do not warn even if bwrap does not support `--argv0`",
);
let fake_bwrap_path: &Path = fake_bwrap.as_ref();

assert_eq!(system_bwrap_warning_for_path(fake_bwrap_path), None);
}

#[cfg(not(target_os = "linux"))]
Expand Down
23 changes: 1 addition & 22 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::process::Command;

use crate::config::permissions::compile_permission_profile;
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
Expand Down Expand Up @@ -172,27 +170,8 @@ fn system_bwrap_warning_for_path(system_bwrap_path: &Path) -> Option<String> {
system_bwrap_path.display()
));
}
if system_bwrap_supports_argv0(system_bwrap_path) {
return None;
}

Some(format!(
"Codex found system bubblewrap at {}, but it is too old to support `--argv0`. Please upgrade bubblewrap with your package manager. Codex will use the vendored bubblewrap in the meantime.",
system_bwrap_path.display()
))
}

#[cfg(target_os = "linux")]
fn system_bwrap_supports_argv0(system_bwrap_path: &Path) -> bool {
// bubblewrap added `--argv0` in v0.9.0:
// https://github.com/containers/bubblewrap/releases/tag/v0.9.0
let output = match Command::new(system_bwrap_path).arg("--help").output() {
Ok(output) => output,
Err(_) => return false,
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
stdout.contains("--argv0") || stderr.contains("--argv0")
None
}

fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option<PathBuf> {
Expand Down
20 changes: 17 additions & 3 deletions codex-rs/core/src/landlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::spawn::spawn_child_async;
use codex_network_proxy::NetworkProxy;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
use codex_sandboxing::landlock::allow_network_for_proxy;
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
use std::collections::HashMap;
Expand Down Expand Up @@ -48,11 +49,24 @@ where
use_legacy_landlock,
allow_network_for_proxy(/*enforce_managed_network*/ false),
);
let arg0 = Some("codex-linux-sandbox");
let codex_linux_sandbox_exe = codex_linux_sandbox_exe.as_ref();
// Preserve the helper alias when we already have it; otherwise force argv0
// so arg0 dispatch still reaches the Linux sandbox path.
let arg0 = if codex_linux_sandbox_exe
Comment thread
viyatb-oai marked this conversation as resolved.
.file_name()
.and_then(|name| name.to_str())
== Some(CODEX_LINUX_SANDBOX_ARG0)
{
// Old bubblewrap builds without `--argv0` need a real helper path whose
// basename still dispatches to the Linux sandbox entrypoint.
codex_linux_sandbox_exe.to_string_lossy().into_owned()
} else {
CODEX_LINUX_SANDBOX_ARG0.to_string()
};
spawn_child_async(SpawnChildRequest {
program: codex_linux_sandbox_exe.as_ref().to_path_buf(),
program: codex_linux_sandbox_exe.to_path_buf(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outside the scope of this PR, but I would like to change program to be AsRef<OsStr> instead of String so we don't have to use to_string_lossy() quite so much.

args,
arg0,
arg0: Some(&arg0),
cwd: command_cwd,
network_sandbox_policy,
network,
Expand Down
2 changes: 0 additions & 2 deletions codex-rs/core/src/tools/handlers/apply_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ impl ToolHandler for ApplyPatchHandler {
permissions_preapproved: effective_additional_permissions
.permissions_preapproved,
timeout_ms: None,
codex_exe: turn.codex_linux_sandbox_exe.clone(),
};

let mut orchestrator = ToolOrchestrator::new();
Expand Down Expand Up @@ -308,7 +307,6 @@ pub(crate) async fn intercept_apply_patch(
permissions_preapproved: effective_additional_permissions
.permissions_preapproved,
timeout_ms,
codex_exe: turn.codex_linux_sandbox_exe.clone(),
};

let mut orchestrator = ToolOrchestrator::new();
Expand Down
31 changes: 15 additions & 16 deletions codex-rs/core/src/tools/runtimes/apply_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ pub struct ApplyPatchRequest {
pub additional_permissions: Option<PermissionProfile>,
pub permissions_preapproved: bool,
pub timeout_ms: Option<u64>,
pub codex_exe: Option<PathBuf>,
}

#[derive(Default)]
Expand All @@ -69,20 +68,16 @@ impl ApplyPatchRuntime {
fn build_sandbox_command(
req: &ApplyPatchRequest,
_codex_home: &std::path::Path,
configured_codex_exe: Option<&PathBuf>,
) -> Result<SandboxCommand, ToolError> {
let exe = if let Some(path) = &req.codex_exe {
#[cfg(target_os = "windows")]
let exe = codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe");
#[cfg(not(target_os = "windows"))]
let exe = if let Some(path) = configured_codex_exe {
path.clone()
} else {
#[cfg(target_os = "windows")]
{
codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe")
}
#[cfg(not(target_os = "windows"))]
{
std::env::current_exe().map_err(|e| {
ToolError::Rejected(format!("failed to determine codex exe: {e}"))
})?
}
std::env::current_exe()
.map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))?
};
Ok(SandboxCommand {
program: exe.to_string_lossy().to_string(),
Expand Down Expand Up @@ -134,13 +129,13 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
let approval_keys = self.approval_keys(req);
let changes = req.changes.clone();
Box::pin(async move {
if req.permissions_preapproved && retry_reason.is_none() {
return ReviewDecision::Approved;
}
if routes_approval_to_guardian(turn) {
Comment thread
viyatb-oai marked this conversation as resolved.
let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id);
return review_approval_request(session, turn, action, retry_reason).await;
}
if req.permissions_preapproved && retry_reason.is_none() {
return ReviewDecision::Approved;
}
if let Some(reason) = retry_reason {
let rx_approve = session
.request_patch_approval(
Expand Down Expand Up @@ -200,7 +195,11 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
) -> Result<ExecToolCallOutput, ToolError> {
let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?;
let command = Self::build_sandbox_command(
req,
&ctx.turn.config.codex_home,
ctx.turn.codex_linux_sandbox_exe.as_ref(),
)?;
let options = ExecOptions {
expiration: req.timeout_ms.into(),
capture_policy: ExecCapturePolicy::ShellTool,
Expand Down
Loading
Loading