Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

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

3 changes: 3 additions & 0 deletions codex-rs/exec-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ tracing = { workspace = true }
url = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

[target.'cfg(target_os = "linux")'.dependencies]
codex-linux-sandbox = { workspace = true }

[target.'cfg(unix)'.dependencies]
codex-uds = { workspace = true }
libc = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ mod rpc;
mod rpc_server_requests;
mod rpc_timing;
mod runtime_options;
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "linux", target_os = "windows"))]
#[expect(
dead_code,
reason = "The integrity runner is added later in this stack."
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec-server/src/sandbox_integrity/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ negatives. Backend glob/symlink nuances and writable hard-link aliases are not
fully modeled. The separate glob scan can differ from enforcement's snapshot.
Metrics describe observed checker outcomes, not accuracy or missed violations.

Windows scanner discovery currently uses `which`, while enforcement uses
MXC scanner discovery currently uses `which`, while enforcement uses
`Command`. Their search-order and environment differences can select different
executables or cause only one scan to use the internal walker. This is an accepted
telemetry limitation for this phase; changing production scanner selection is deferred.
Expand Down
105 changes: 105 additions & 0 deletions codex-rs/exec-server/src/sandbox_integrity/backend_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
#[cfg(target_os = "linux")]
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::sandbox::SandboxOverride;
use codex_sandboxing::FileContentsChecker;
Expand Down Expand Up @@ -65,3 +67,106 @@ pub(super) fn sandbox_request(
arg0: None,
}
}
#[cfg(target_os = "linux")]
#[test]
fn dependency_inventory_uses_launcher_location_and_command_path() -> io::Result<()> {
use std::os::unix::fs::PermissionsExt;

let temp = tempfile::tempdir()?;
let root = AbsolutePathBuf::from_absolute_path(temp.path())?.canonicalize()?;
let cwd = root.join("command");
let tools = root.join("tools");
let bundled = tools.join("codex-resources/bwrap");
fs::create_dir(&cwd)?;
fs::create_dir(&tools)?;
fs::create_dir(tools.join("codex-resources"))?;
for path in [
tools.join("bwrap"),
tools.join("rg"),
cwd.join("rg"),
tools.join("codex"),
bundled.clone(),
] {
fs::write(&path, "fixture")?;
fs::set_permissions(&path, fs::Permissions::from_mode(/*mode*/ 0o755))?;
}
let launcher = cwd.join("sandbox-helper");
std::os::unix::fs::symlink(tools.join("codex"), &launcher)?;
let policy = FileSystemSandboxPolicy::restricted(vec![deny_glob("*.key")]);
let mut request = sandbox_request(&cwd, &policy);
request.command[0] = launcher.to_string_lossy().into_owned();
for (path, scanner) in [
("../tools", tools.join("rg")),
(":../tools", tools.join("rg")),
] {
request.env.insert("PATH".to_owned(), path.to_owned());
let dependencies = backend::critical_dependencies(&request, &policy, &cwd)?
.into_iter()
.filter(|(name, _)| {
matches!(
*name,
"bundled_bwrap_candidate" | "system_bwrap_candidate" | "glob_scanner"
)
})
.map(|(name, path)| path.and_then(fs::canonicalize).map(|path| (name, path)))
.collect::<io::Result<Vec<_>>>()?;
assert_eq!(
dependencies,
vec![
("bundled_bwrap_candidate", bundled.to_path_buf()),
(
"system_bwrap_candidate",
tools.join("bwrap").into_path_buf()
),
("glob_scanner", scanner.into_path_buf()),
]
);
}
Ok(())
}

#[cfg(target_os = "linux")]
#[test]
fn policy_preparation_uses_command_tmpdir_and_home() -> io::Result<()> {
let temp = tempfile::tempdir()?;
let root = AbsolutePathBuf::from_absolute_path(temp.path())?.canonicalize()?;
let cwd = root.join("command");
let home = root.join("home");
fs::create_dir(&cwd)?;
fs::create_dir(&home)?;
let denied = home.join("secret.key");
fs::write(&denied, "fixture")?;
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
},
FileSystemAccessMode::Write,
),
deny_glob("~/secret.*"),
]);
let mut request = sandbox_request(&cwd, &policy);
request
.env
.insert("HOME".to_owned(), home.to_string_lossy().into_owned());
for (tmpdir, expected) in [
("", None),
("scratch", Some(cwd.join("scratch"))),
("~/scratch", Some(home.join("scratch"))),
] {
request.env.insert("TMPDIR".to_owned(), tmpdir.to_owned());
let mut entries = expected
.into_iter()
.map(|path| FileSystemSandboxEntry::new(path.into(), FileSystemAccessMode::Write))
.collect::<Vec<_>>();
entries.push(FileSystemSandboxEntry::new(
denied.clone().into(),
FileSystemAccessMode::Deny,
));
assert_eq!(
backend::prepare_policy(&request, policy.clone(), &cwd)?,
FileSystemSandboxPolicy::restricted(entries),
);
}
Ok(())
}
134 changes: 134 additions & 0 deletions codex-rs/exec-server/src/sandbox_integrity/bubblewrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! Discover bubblewrap dependencies separately from resolving configured policy rules.
use super::dependencies::Dependency;
use super::dependencies::codex_and_launcher_dependencies;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxType;
use codex_shell_command::shell_detect::get_user_home_path;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use std::ffi::OsString;
use std::io;
use std::path::PathBuf;

pub(super) const SANDBOX_TYPE: SandboxType = SandboxType::LinuxSeccomp;
pub(super) const NAME: &str = "bubblewrap";

pub(super) fn critical_dependencies(
request: &SandboxExecRequest,
policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> io::Result<Vec<Dependency>> {
let command_cwd = request.cwd.to_abs_path()?;
let path = request
.env
.get("PATH")
.map(|path| {
std::env::join_paths(
std::env::split_paths(path).map(|entry| command_cwd.as_path().join(entry)),
)
})
.transpose()
.map_err(io::Error::other)?;
let mut dependencies = codex_and_launcher_dependencies(request);
if let Some(exe) = request.command.first()
&& let Some(bwrap) = codex_linux_sandbox::find_bundled_bwrap_for_exe(
&command_cwd.as_path().join(exe),
command_cwd.as_path(),
|key| request.env.get(key).map(OsString::from),
)
{
dependencies.push(("bundled_bwrap_candidate", Ok(bwrap.into_path_buf())));
}
if let Some(path) = &path
&& let Some(bwrap) = codex_sandboxing::find_executable_in_search_paths(
"bwrap",
std::env::split_paths(path),
command_cwd.as_path(),
policy,
cwd.as_path(),
)
{
// The launcher probes this executable even if it later uses its bundled copy.
dependencies.push(("system_bwrap_candidate", Ok(bwrap)));
}
if !policy
.get_unreadable_globs_with_cwd(cwd.as_path())
.is_empty()
&& let Some(path) = &path
&& let Some(scanner) = codex_sandboxing::find_executable_in_search_paths(
codex_linux_sandbox::GLOB_SCAN_PROGRAM,
std::env::split_paths(path),
command_cwd.as_path(),
policy,
cwd.as_path(),
)
{
dependencies.push(("glob_scanner", Ok(scanner)));
}
Ok(dependencies)
}

pub(super) fn prepare_policy(
request: &SandboxExecRequest,
mut policy: FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> io::Result<FileSystemSandboxPolicy> {
let command_cwd = request.cwd.to_abs_path()?;
let needs_home = policy.entries.iter().any(|entry| match &entry.path {
FileSystemPath::GlobPattern { pattern } => pattern.starts_with('~'),
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
} => request
.env
.get("TMPDIR")
.is_some_and(|path| path.starts_with('~')),
FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => false,
});
let prepare = || {
// The helper resolves :tmpdir from its filtered environment, not the executor's.
policy.entries.retain_mut(|entry| {
if matches!(
entry.path,
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir
}
) {
let Some(tmpdir) = request.env.get("TMPDIR").filter(|path| !path.is_empty()) else {
return false;
};
entry.path =
AbsolutePathBuf::resolve_path_against_base(tmpdir, &command_cwd).into();
}
true
});
let patterns = policy.get_unreadable_globs_with_cwd(cwd.as_path());
if !patterns.is_empty() {
let paths = codex_linux_sandbox::expand_unreadable_globs_in_environment(
&patterns,
cwd.as_path(),
&policy,
policy.glob_scan_max_depth,
&request.env,
command_cwd.as_path(),
)
.map_err(io::Error::other)?;
policy = policy.with_expanded_deny_globs(paths);
}
Ok(policy)
};
if !needs_home {
return prepare();
}
let home = request
.env
.get("HOME")
.filter(|home| !home.is_empty())
.map(PathBuf::from)
.or_else(get_user_home_path)
.ok_or_else(|| io::Error::other("could not resolve the command's home directory"))?;
AbsolutePathBufGuard::with_home_directory(&home, prepare)
}
3 changes: 2 additions & 1 deletion codex-rs/exec-server/src/sandbox_integrity/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#[path = "mxc.rs"]
#[cfg_attr(target_os = "linux", path = "bubblewrap.rs")]
#[cfg_attr(target_os = "windows", path = "mxc.rs")]
mod backend;
mod dependencies;

Expand Down
36 changes: 21 additions & 15 deletions codex-rs/linux-sandbox/src/bazel_bwrap.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,57 @@
use std::ffi::OsString;
#[cfg(debug_assertions)]
use std::fs::File;
#[cfg(debug_assertions)]
use std::io::BufRead;
use std::path::Path;
use std::path::PathBuf;

#[cfg(debug_assertions)]
const BAZEL_BWRAP_ENV_VAR: &str = "CARGO_BIN_EXE_bwrap";

#[cfg(debug_assertions)]
pub(crate) fn candidate() -> Option<PathBuf> {
if option_env!("BAZEL_PACKAGE").is_none() || !runfiles_env_present() {
pub(crate) fn candidate(cwd: &Path, env: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
if option_env!("BAZEL_PACKAGE").is_none() || !runfiles_env_present(&env) {
return None;
}

let raw = PathBuf::from(std::env::var_os(BAZEL_BWRAP_ENV_VAR)?);
let raw = PathBuf::from(env(BAZEL_BWRAP_ENV_VAR)?);
if raw.is_absolute() {
return Some(raw);
}
resolve_runfile(raw.to_str()?)
resolve_runfile(raw.to_str()?, cwd, &env)
}

#[cfg(not(debug_assertions))]
pub(crate) fn candidate() -> Option<PathBuf> {
pub(crate) fn candidate(_cwd: &Path, _env: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
None
}

#[cfg(debug_assertions)]
fn runfiles_env_present() -> bool {
std::env::var_os("RUNFILES_DIR").is_some()
|| std::env::var_os("TEST_SRCDIR").is_some()
|| std::env::var_os("RUNFILES_MANIFEST_FILE").is_some()
fn runfiles_env_present(env: &impl Fn(&str) -> Option<OsString>) -> bool {
env("RUNFILES_DIR").is_some()
|| env("TEST_SRCDIR").is_some()
|| env("RUNFILES_MANIFEST_FILE").is_some()
}

#[cfg(debug_assertions)]
fn resolve_runfile(logical_path: &str) -> Option<PathBuf> {
fn resolve_runfile(
logical_path: &str,
cwd: &Path,
env: &impl Fn(&str) -> Option<OsString>,
) -> Option<PathBuf> {
let mut logical_paths = vec![logical_path.to_string()];
if let Ok(workspace) = std::env::var("TEST_WORKSPACE")
if let Some(workspace) = env("TEST_WORKSPACE").and_then(|value| value.into_string().ok())
&& !workspace.is_empty()
{
logical_paths.push(format!("{workspace}/{logical_path}"));
}

for root_env in ["RUNFILES_DIR", "TEST_SRCDIR"] {
let Some(root) = std::env::var_os(root_env) else {
let Some(root) = env(root_env) else {
continue;
};
let root = PathBuf::from(root);
let root = cwd.join(root);
for logical_path in &logical_paths {
let candidate = root.join(logical_path);
if candidate.exists() {
Expand All @@ -54,14 +60,14 @@ fn resolve_runfile(logical_path: &str) -> Option<PathBuf> {
}
}

let manifest = PathBuf::from(std::env::var_os("RUNFILES_MANIFEST_FILE")?);
let manifest = cwd.join(env("RUNFILES_MANIFEST_FILE")?);
let file = File::open(manifest).ok()?;
for line in std::io::BufReader::new(file).lines().map_while(Result::ok) {
let Some((key, value)) = line.split_once(' ') else {
continue;
};
if logical_paths.iter().any(|logical_path| logical_path == key) {
return Some(PathBuf::from(value));
return Some(cwd.join(value));
}
}
None
Expand Down
Loading
Loading