mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Add a bubblewrap backend for sandbox integrity checks (#51840)
## What changed Add Linux dependency discovery and policy preparation to `exec-server`'s sandbox integrity module. Inventory bundled and system `bwrap` candidates and the deny-glob scanner using the launcher's location and the command's `PATH` and working directory. Resolve `:tmpdir` and tilde-prefixed deny globs using the command's `TMPDIR` and `HOME`, with account home-directory fallback. Reuse Linux sandbox helpers to expand deny globs in the command's explicit environment. ## Testing Add Linux tests covering symlinked launchers, relative and empty `PATH` entries, and empty, relative, and tilde-prefixed `TMPDIR` values alongside home-relative deny globs. GitOrigin-RevId: fe404fa4be77ece677b22d52d77a7234dbf0cffe
This commit is contained in:
Generated
+1
@@ -3214,6 +3214,7 @@ dependencies = [
|
||||
"codex-file-system",
|
||||
"codex-file-watcher",
|
||||
"codex-http-client",
|
||||
"codex-linux-sandbox",
|
||||
"codex-mxc-sandbox",
|
||||
"codex-network-proxy",
|
||||
"codex-otel",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::ffi::CStr;
|
||||
use std::ffi::CString;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::os::fd::AsRawFd;
|
||||
@@ -27,9 +28,38 @@ pub(crate) struct BundledBwrapLauncher {
|
||||
|
||||
pub(crate) fn launcher() -> Option<BundledBwrapLauncher> {
|
||||
let current_exe = std::env::current_exe().ok()?;
|
||||
find_for_install_context(InstallContext::current())
|
||||
.or_else(|| find_legacy_for_exe(¤t_exe))
|
||||
.map(|program| BundledBwrapLauncher { program })
|
||||
find_program(
|
||||
InstallContext::current(),
|
||||
¤t_exe,
|
||||
Path::new("."),
|
||||
|key| std::env::var_os(key),
|
||||
)
|
||||
.map(|program| BundledBwrapLauncher { program })
|
||||
}
|
||||
|
||||
/// Discover the bundled fallback for an explicit launcher and its environment.
|
||||
pub fn find_bundled_bwrap_for_exe(
|
||||
exe: &Path,
|
||||
command_cwd: &Path,
|
||||
env: impl Fn(&str) -> Option<OsString>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
let exe = std::fs::canonicalize(exe).ok()?;
|
||||
let context = InstallContext::from_exe(
|
||||
/*is_macos*/ false,
|
||||
Some(&exe),
|
||||
/*method_override*/ None,
|
||||
);
|
||||
find_program(&context, &exe, command_cwd, env)
|
||||
}
|
||||
|
||||
fn find_program(
|
||||
context: &InstallContext,
|
||||
exe: &Path,
|
||||
cwd: &Path,
|
||||
env: impl Fn(&str) -> Option<OsString>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
find_for_install_context(context)
|
||||
.or_else(|| find_legacy_for_exe(exe, bazel_bwrap::candidate(cwd, env)))
|
||||
}
|
||||
|
||||
impl BundledBwrapLauncher {
|
||||
@@ -77,8 +107,8 @@ fn find_for_install_context(context: &InstallContext) -> Option<AbsolutePathBuf>
|
||||
.filter(|path| is_executable_file(path))
|
||||
}
|
||||
|
||||
fn find_legacy_for_exe(exe: &Path) -> Option<AbsolutePathBuf> {
|
||||
legacy_candidates_for_exe(exe)
|
||||
fn find_legacy_for_exe(exe: &Path, bazel_candidate: Option<PathBuf>) -> Option<AbsolutePathBuf> {
|
||||
legacy_candidates_for_exe(exe, bazel_candidate)
|
||||
.into_iter()
|
||||
.find(|candidate| is_executable_file(candidate))
|
||||
.map(|path| {
|
||||
@@ -91,7 +121,7 @@ fn find_legacy_for_exe(exe: &Path) -> Option<AbsolutePathBuf> {
|
||||
})
|
||||
}
|
||||
|
||||
fn legacy_candidates_for_exe(exe: &Path) -> Vec<PathBuf> {
|
||||
fn legacy_candidates_for_exe(exe: &Path, bazel_candidate: Option<PathBuf>) -> Vec<PathBuf> {
|
||||
let Some(exe_dir) = exe.parent() else {
|
||||
return Vec::new();
|
||||
};
|
||||
@@ -102,7 +132,7 @@ fn legacy_candidates_for_exe(exe: &Path) -> Vec<PathBuf> {
|
||||
candidates.push(package_target_dir.join("codex-resources").join("bwrap"));
|
||||
}
|
||||
candidates.push(exe_dir.join("bwrap"));
|
||||
if let Some(path) = bazel_bwrap::candidate() {
|
||||
if let Some(path) = bazel_candidate {
|
||||
candidates.push(path);
|
||||
}
|
||||
candidates
|
||||
@@ -236,7 +266,7 @@ mod tests {
|
||||
write_executable(&expected_bwrap);
|
||||
|
||||
assert_eq!(
|
||||
find_legacy_for_exe(&exe),
|
||||
find_legacy_for_exe(&exe, /*bazel_candidate*/ None),
|
||||
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
|
||||
);
|
||||
}
|
||||
@@ -251,7 +281,7 @@ mod tests {
|
||||
write_executable(&expected_bwrap);
|
||||
|
||||
assert_eq!(
|
||||
find_legacy_for_exe(&exe),
|
||||
find_legacy_for_exe(&exe, /*bazel_candidate*/ None),
|
||||
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
|
||||
);
|
||||
}
|
||||
@@ -265,7 +295,7 @@ mod tests {
|
||||
write_executable(&expected_bwrap);
|
||||
|
||||
assert_eq!(
|
||||
find_legacy_for_exe(&exe),
|
||||
find_legacy_for_exe(&exe, /*bazel_candidate*/ None),
|
||||
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//! - bubblewrap used to construct the filesystem view before exec.
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
@@ -37,6 +38,7 @@ use codex_protocol::protocol::FileSystemPath;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::WritableRoot;
|
||||
use codex_sandboxing::find_executable_in_search_paths;
|
||||
use codex_sandboxing::find_pre_sandbox_executable_in_path;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use globset::GlobBuilder;
|
||||
@@ -59,6 +61,9 @@ const LINUX_PLATFORM_DEFAULT_READ_ROOTS: &[&str] = &[
|
||||
"/run/current-system/sw",
|
||||
];
|
||||
|
||||
/// External scanner used by deny-glob expansion and its dependency inventory.
|
||||
pub const GLOB_SCAN_PROGRAM: &str = "rg";
|
||||
|
||||
const MAX_UNREADABLE_GLOB_MATCHES: usize = 8192;
|
||||
pub(crate) const WSL_INTEROP_DIR: &str = "/run/WSL";
|
||||
pub(crate) const WSLG_DISTRO_ROOT: &str = "/mnt/wslg/distro";
|
||||
@@ -499,10 +504,17 @@ fn create_filesystem_args(
|
||||
expand_unreadable_globs_with_ripgrep(
|
||||
&unreadable_globs,
|
||||
cwd,
|
||||
file_system_sandbox_policy,
|
||||
|| {
|
||||
find_pre_sandbox_executable_in_path(
|
||||
GLOB_SCAN_PROGRAM,
|
||||
file_system_sandbox_policy,
|
||||
cwd,
|
||||
)
|
||||
},
|
||||
options
|
||||
.glob_scan_max_depth
|
||||
.or(file_system_sandbox_policy.glob_scan_max_depth),
|
||||
|_| {},
|
||||
)?
|
||||
.into_iter()
|
||||
.map(AbsolutePathBuf::into_path_buf),
|
||||
@@ -888,17 +900,47 @@ fn append_daemon_socket_masks(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expand_unreadable_globs_with_ripgrep(
|
||||
/// Expand deny globs with the launcher's scanner in an explicit command environment.
|
||||
/// The supplied environment replaces inheritance, including when it is empty.
|
||||
pub fn expand_unreadable_globs_in_environment(
|
||||
patterns: &[String],
|
||||
cwd: &Path,
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
max_depth: Option<usize>,
|
||||
env: &HashMap<String, String>,
|
||||
command_cwd: &Path,
|
||||
) -> Result<Vec<AbsolutePathBuf>> {
|
||||
expand_unreadable_globs_with_ripgrep(
|
||||
patterns,
|
||||
cwd,
|
||||
|| {
|
||||
find_executable_in_search_paths(
|
||||
GLOB_SCAN_PROGRAM,
|
||||
std::env::split_paths(env.get("PATH")?),
|
||||
command_cwd,
|
||||
file_system_policy,
|
||||
cwd,
|
||||
)
|
||||
},
|
||||
max_depth,
|
||||
|command| {
|
||||
command.env_clear().envs(env).current_dir(command_cwd);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn expand_unreadable_globs_with_ripgrep(
|
||||
patterns: &[String],
|
||||
cwd: &Path,
|
||||
resolve_scanner: impl FnOnce() -> Option<PathBuf>,
|
||||
max_depth: Option<usize>,
|
||||
configure_command: impl Fn(&mut Command),
|
||||
) -> Result<Vec<AbsolutePathBuf>> {
|
||||
if patterns.is_empty() || max_depth == Some(0) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rg_path = find_pre_sandbox_executable_in_path("rg", file_system_policy, cwd);
|
||||
let rg_path = resolve_scanner();
|
||||
|
||||
// Group each pattern by the static path prefix before its first glob
|
||||
// metacharacter. That keeps scans narrow, avoids searching from `/`, and
|
||||
@@ -924,7 +966,13 @@ fn expand_unreadable_globs_with_ripgrep(
|
||||
let mut expanded_paths = BTreeSet::new();
|
||||
for (search_root, globs) in patterns_by_search_root {
|
||||
let paths = match &rg_path {
|
||||
Some(rg_path) => ripgrep_files(rg_path, search_root.as_path(), &globs, max_depth)?,
|
||||
Some(rg_path) => ripgrep_files(
|
||||
rg_path,
|
||||
search_root.as_path(),
|
||||
&globs,
|
||||
max_depth,
|
||||
&configure_command,
|
||||
)?,
|
||||
None => glob_files(search_root.as_path(), &globs, max_depth)?,
|
||||
};
|
||||
for path in paths {
|
||||
@@ -1013,11 +1061,13 @@ fn ripgrep_files(
|
||||
search_root: &Path,
|
||||
globs: &[String],
|
||||
max_depth: Option<usize>,
|
||||
configure_command: &impl Fn(&mut Command),
|
||||
) -> Result<Vec<AbsolutePathBuf>> {
|
||||
let mut command = Command::new(rg_path);
|
||||
configure_command(&mut command);
|
||||
// Use `rg --files` rather than shell expansion so dotfiles and ignored files
|
||||
// are still considered. A status 1 with no stderr is ripgrep's "no matches"
|
||||
// case, not a sandbox construction error.
|
||||
let mut command = Command::new(rg_path);
|
||||
command
|
||||
.arg("--no-config")
|
||||
.arg("--files")
|
||||
@@ -3028,7 +3078,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn ripgrep_available() -> bool {
|
||||
Command::new("rg")
|
||||
Command::new(GLOB_SCAN_PROGRAM)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_ok_and(|output| output.status.success())
|
||||
|
||||
@@ -28,6 +28,13 @@ mod proxy_routing;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod wslg;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bundled_bwrap::find_bundled_bwrap_for_exe;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bwrap::GLOB_SCAN_PROGRAM;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bwrap::expand_unreadable_globs_in_environment;
|
||||
|
||||
/// Exit status returned when bundled bubblewrap fails digest verification.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const BUNDLED_BWRAP_DIGEST_VERIFICATION_FAILURE_EXIT_CODE: i32 = 8;
|
||||
|
||||
@@ -206,7 +206,8 @@ pub fn find_pre_sandbox_executable_in_path(
|
||||
)
|
||||
}
|
||||
|
||||
fn find_executable_in_search_paths(
|
||||
/// Apply the pre-sandbox executable selection rules to an explicit command context.
|
||||
pub fn find_executable_in_search_paths(
|
||||
program: &str,
|
||||
search_paths: impl IntoIterator<Item = PathBuf>,
|
||||
cwd: &Path,
|
||||
|
||||
@@ -15,6 +15,8 @@ mod windows;
|
||||
#[cfg(windows)]
|
||||
mod windows_mxc;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bwrap::find_executable_in_search_paths;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bwrap::find_pre_sandbox_executable_in_path;
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -60,9 +60,22 @@ pub fn detect_shell_type(shell_path: impl AsRef<std::path::Path>) -> Option<Shel
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_user_shell_path() -> Option<PathBuf> {
|
||||
get_user_path(|passwd| passwd.pw_shell)
|
||||
.map(|path| PathBuf::from(path.to_string_lossy().into_owned()))
|
||||
}
|
||||
|
||||
/// Read the current account's home directory without consulting the environment.
|
||||
#[cfg(unix)]
|
||||
pub fn get_user_home_path() -> Option<PathBuf> {
|
||||
get_user_path(|passwd| passwd.pw_dir).filter(|path| !path.as_os_str().is_empty())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_user_path(field: fn(&libc::passwd) -> *mut libc::c_char) -> Option<PathBuf> {
|
||||
let uid = unsafe { libc::getuid() };
|
||||
use std::ffi::CStr;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::ptr;
|
||||
|
||||
let mut passwd = MaybeUninit::<libc::passwd>::uninit();
|
||||
@@ -96,14 +109,14 @@ fn get_user_shell_path() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
let passwd = unsafe { passwd.assume_init_ref() };
|
||||
if passwd.pw_shell.is_null() {
|
||||
let path = field(passwd);
|
||||
if path.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let shell_path = unsafe { CStr::from_ptr(passwd.pw_shell) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
return Some(PathBuf::from(shell_path));
|
||||
// Both callers select a field stored in the still-live passwd buffer.
|
||||
let path = unsafe { CStr::from_ptr(path) };
|
||||
return Some(PathBuf::from(std::ffi::OsStr::from_bytes(path.to_bytes())));
|
||||
}
|
||||
|
||||
if status != libc::ERANGE {
|
||||
|
||||
Reference in New Issue
Block a user