fix: overhaul how we spawn commands under seccomp/landlock on Linux

This commit is contained in:
Michael Bolin
2025-05-22 15:44:32 -07:00
parent cb379d7797
commit b5ae657fad
18 changed files with 367 additions and 282 deletions

14
codex-rs/Cargo.lock generated
View File

@@ -491,6 +491,7 @@ dependencies = [
"codex-common",
"codex-core",
"codex-exec",
"codex-linux-sandbox",
"codex-mcp-server",
"codex-tui",
"serde_json",
@@ -562,6 +563,7 @@ dependencies = [
"clap",
"codex-common",
"codex-core",
"codex-linux-sandbox",
"mcp-types",
"owo-colors 4.2.0",
"serde_json",
@@ -591,6 +593,18 @@ dependencies = [
"tempfile",
]
[[package]]
name = "codex-linux-sandbox"
version = "0.0.0"
dependencies = [
"clap",
"codex-common",
"codex-core",
"landlock",
"libc",
"seccompiler",
]
[[package]]
name = "codex-mcp-client"
version = "0.0.0"

View File

@@ -8,6 +8,7 @@ members = [
"core",
"exec",
"execpolicy",
"linux-sandbox",
"mcp-client",
"mcp-server",
"mcp-types",
@@ -23,7 +24,7 @@ version = "0.0.0"
edition = "2024"
[workspace.lints]
rust = { }
rust = {}
[workspace.lints.clippy]
expect_used = "deny"

View File

@@ -7,10 +7,6 @@ edition = "2024"
name = "codex"
path = "src/main.rs"
[[bin]]
name = "codex-linux-sandbox"
path = "src/linux-sandbox/main.rs"
[lib]
name = "codex_cli"
path = "src/lib.rs"
@@ -24,6 +20,7 @@ clap = { version = "4", features = ["derive"] }
codex-core = { path = "../core" }
codex-common = { path = "../common", features = ["cli"] }
codex-exec = { path = "../exec" }
codex-linux-sandbox = { path = "../linux-sandbox" }
codex-mcp-server = { path = "../mcp-server" }
codex-tui = { path = "../tui" }
serde_json = "1"

View File

@@ -1,37 +0,0 @@
//! `debug landlock` implementation for the Codex CLI.
//!
//! On Linux the command is executed inside a Landlock + seccomp sandbox by
//! calling the low-level `exec_linux` helper from `codex_core::linux`.
use codex_core::config::Config;
use codex_core::exec::StdioPolicy;
use codex_core::exec::spawn_child_sync;
use codex_core::exec_linux::apply_sandbox_policy_to_current_thread;
use std::process::ExitStatus;
use crate::exit_status::handle_exit_status;
/// Execute `command` in a Linux sandbox (Landlock + seccomp) the way Codex
/// would.
pub fn run_landlock(command: Vec<String>, config: &Config) -> anyhow::Result<()> {
if command.is_empty() {
anyhow::bail!("command args are empty");
}
// Spawn a new thread and apply the sandbox policies there.
let env = codex_core::exec_env::create_env(&config.shell_environment_policy);
let sandbox_policy = config.sandbox_policy.clone();
let handle = std::thread::spawn(move || -> anyhow::Result<ExitStatus> {
let cwd = std::env::current_dir()?;
apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?;
let mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit, env)?;
let status = child.wait()?;
Ok(status)
});
let status = handle
.join()
.map_err(|e| anyhow::anyhow!("Failed to join thread: {e:?}"))??;
handle_exit_status(status);
}

View File

@@ -1,6 +1,4 @@
mod exit_status;
#[cfg(unix)]
pub mod landlock;
pub mod proto;
pub mod seatbelt;

View File

@@ -1,28 +0,0 @@
#[cfg(not(target_os = "linux"))]
fn main() -> anyhow::Result<()> {
eprintln!("codex-linux-sandbox is not supported on this platform.");
std::process::exit(1);
}
#[cfg(target_os = "linux")]
fn main() -> anyhow::Result<()> {
use clap::Parser;
use codex_cli::LandlockCommand;
use codex_cli::create_sandbox_policy;
use codex_cli::landlock;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
let LandlockCommand {
full_auto,
sandbox,
command,
} = LandlockCommand::parse();
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
let config = Config::load_with_overrides(ConfigOverrides {
sandbox_policy: Some(sandbox_policy),
..Default::default()
})?;
landlock::run_landlock(command, &config)?;
Ok(())
}

View File

@@ -1,3 +1,5 @@
use std::path::Path;
use clap::Parser;
use codex_cli::LandlockCommand;
use codex_cli::SeatbeltCommand;
@@ -6,6 +8,7 @@ use codex_cli::proto;
use codex_cli::seatbelt;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::exec_env::create_env;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
@@ -64,8 +67,27 @@ enum DebugCommand {
#[derive(Debug, Parser)]
struct ReplProto {}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
fn main() -> anyhow::Result<()> {
// Determine if we were invoked via the special alias.
let argv0 = std::env::args().next().unwrap_or_default();
let exe_name = Path::new(&argv0)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("");
if exe_name == "codex-linux-sandbox" {
codex_linux_sandbox::run_main()
}
// Regular `codex` invocation parse the normal CLI.
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(async {
cli_main().await?;
Ok(())
})
}
async fn cli_main() -> anyhow::Result<()> {
let cli = MultitoolCli::parse();
match cli.subcommand {
@@ -94,22 +116,32 @@ async fn main() -> anyhow::Result<()> {
})?;
seatbelt::run_seatbelt(command, &config).await?;
}
#[cfg(unix)]
DebugCommand::Landlock(LandlockCommand {
command,
sandbox,
full_auto,
}) => {
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
let cwd = std::env::current_dir()?;
let config = Config::load_with_overrides(ConfigOverrides {
sandbox_policy: Some(sandbox_policy),
..Default::default()
})?;
codex_cli::landlock::run_landlock(command, &config)?;
}
#[cfg(not(unix))]
DebugCommand::Landlock(_) => {
anyhow::bail!("Landlock is only supported on Linux.");
let full_args = codex_core::exec::create_linux_sandbox_command_args(
command,
&config.sandbox_policy,
&cwd,
);
let env = create_env(&config.shell_environment_policy);
codex_core::exec::spawn_command_under_linux_sandbox(
full_args,
&config.sandbox_policy,
cwd,
codex_core::exec::StdioPolicy::Inherit,
env,
)
.await?;
}
},
}

View File

@@ -21,7 +21,6 @@ use tokio::sync::Notify;
use crate::error::CodexErr;
use crate::error::Result;
use crate::error::SandboxErr;
use crate::exec_linux::exec_linux;
use crate::protocol::SandboxPolicy;
// Maximum we send for each stream, which is either:
@@ -101,7 +100,25 @@ pub async fn process_exec_tool_call(
.await?;
consume_truncated_output(child, ctrl_c, timeout_ms).await
}
SandboxType::LinuxSeccomp => exec_linux(params, ctrl_c, sandbox_policy),
SandboxType::LinuxSeccomp => {
let ExecParams {
command,
cwd,
timeout_ms,
env,
} = params;
let child = spawn_command_under_linux_sandbox(
command,
sandbox_policy,
cwd,
StdioPolicy::RedirectForShellTool,
env,
)
.await?;
consume_truncated_output(child, ctrl_c, timeout_ms).await
}
};
let duration = start.elapsed();
match raw_output_result {
@@ -152,7 +169,104 @@ pub async fn spawn_command_under_seatbelt(
env: HashMap<String, String>,
) -> std::io::Result<Child> {
let seatbelt_command = create_seatbelt_command(command, sandbox_policy, &cwd);
spawn_child_async(seatbelt_command, cwd, sandbox_policy, stdio_policy, env).await
let arg0 = None;
spawn_child_async(
seatbelt_command,
arg0,
cwd,
sandbox_policy,
stdio_policy,
env,
)
.await
}
/// Spawn a shell tool command under the Linux Landlock+seccomp sandbox helper
/// (codex-linux-sandbox).
///
/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux
/// helper accepts a list of `--sandbox-permission`/`-s` flags mirroring the
/// public CLI. We convert the internal [`SandboxPolicy`] representation into
/// the equivalent CLI options so that front-ends and the business-logic layer
/// remain decoupled from the platform-specific implementation.
pub async fn spawn_command_under_linux_sandbox(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
cwd: PathBuf,
stdio_policy: StdioPolicy,
env: HashMap<String, String>,
) -> std::io::Result<Child> {
let linux_cmd = create_linux_sandbox_command_args(command, sandbox_policy, &cwd);
let arg0 = Some("codex-linux-sandbox");
spawn_child_async(linux_cmd, arg0, cwd, sandbox_policy, stdio_policy, env).await
}
/// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`.
pub fn create_linux_sandbox_command_args(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
cwd: &Path,
) -> Vec<String> {
// TODO(mbolin): Require the client to pass codex_linux_sandbox_exe as a
// parameter to this function because code in `codex_core` should assume it
// is bundled in a binary that special-cases arg0 when it is
// "codex-linux-sandbox".
#[expect(clippy::expect_used)]
let codex_linux_sandbox_exe =
std::env::current_exe().expect("failed to get current executable");
#[expect(clippy::expect_used)]
let mut linux_cmd: Vec<String> = vec![
codex_linux_sandbox_exe
.to_str()
.expect("failed to convert path to str")
.to_string(),
];
// If the policy matches the built-in “full-auto” setting, use the concise flag.
if *sandbox_policy == SandboxPolicy::new_full_auto_policy() {
linux_cmd.push("--full-auto".to_string());
} else {
// Otherwise, translate individual permissions.
// Use high-level helper methods to infer flags when we cannot see the
// exact permission list (private field).
if sandbox_policy.has_full_disk_read_access() {
linux_cmd.extend(["-s", "disk-full-read-access"].map(String::from));
}
if sandbox_policy.has_full_disk_write_access() {
linux_cmd.extend(["-s", "disk-full-write-access"].map(String::from));
} else {
// Derive granular writable paths (includes cwd if `DiskWriteCwd` is
// present).
for root in sandbox_policy.get_writable_roots_with_cwd(cwd) {
// Check if this path corresponds exactly to cwd to map to
// `disk-write-cwd`, otherwise use the generic folder rule.
if root == cwd {
linux_cmd.extend(["-s", "disk-write-cwd"].map(String::from));
} else {
linux_cmd.extend([
"-s".to_string(),
format!("disk-write-folder={}", root.to_string_lossy()),
]);
}
}
}
if sandbox_policy.has_full_network_access() {
linux_cmd.extend(["-s", "network-full-access"].map(String::from));
}
}
// Separator so that command arguments starting with `-` are not parsed as
// options of the helper itself.
linux_cmd.push("--".to_string());
// Append the original tool command.
linux_cmd.extend(command);
linux_cmd
}
fn create_seatbelt_command(
@@ -243,8 +357,10 @@ async fn exec(
sandbox_policy: &SandboxPolicy,
ctrl_c: Arc<Notify>,
) -> Result<RawExecToolCallOutput> {
let arg0 = None;
let child = spawn_child_async(
command,
arg0,
cwd,
sandbox_policy,
StdioPolicy::RedirectForShellTool,
@@ -260,124 +376,62 @@ pub enum StdioPolicy {
Inherit,
}
macro_rules! configure_command {
(
$cmd_type: path,
$command: expr,
$cwd: expr,
$sandbox_policy: expr,
$stdio_policy: expr,
$env_map: expr
) => {{
// For now, we take `SandboxPolicy` as a parameter to spawn_child() because
// we need to determine whether to set the
// `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable.
// Ultimately, we should be stricter about the environment variables that
// are set for the command (as we are when spawning an MCP server), so
// instead of SandboxPolicy, we should take the exact env to use for the
// Command (i.e., `env_clear().envs(env)`).
if $command.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"command args are empty",
));
}
let mut cmd = <$cmd_type>::new(&$command[0]);
cmd.args(&$command[1..]);
cmd.current_dir($cwd);
// Previously, to update the env for `cmd`, we did the straightforward
// thing of calling `env_clear()` followed by `envs(&env_map)` so
// that the spawned process inherited *only* the variables explicitly
// provided by the caller. On Linux, the combination of `env_clear()`
// and Landlock/seccomp caused a permission error whereas this more
// "surgical" approach of setting variables individually appears to
// work fine. More time with `strace` and friends is merited to fully
// debug thus, though we will soon use a helper binary like we do for
// Seatbelt, which will simplify this logic.
// Iterate through the current process environment first so we can
// decide, for every variable that already exists, whether we need to
// override its value.
let mut remaining_overrides = $env_map.clone();
for (key, current_val) in std::env::vars() {
if let Some(desired_val) = remaining_overrides.remove(&key) {
// The caller provided a value for this variable. Override it
// only if the value differs from what is currently set.
if desired_val != current_val {
cmd.env(&key, desired_val);
}
}
// If the variable was not in `env_map`, we leave it unchanged.
}
// Any entries still left in `remaining_overrides` were not present in
// the parent environment. Add them now so that the child process sees
// the complete set requested by the caller.
for (key, val) in remaining_overrides {
cmd.env(key, val);
}
if !$sandbox_policy.has_full_network_access() {
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
}
match $stdio_policy {
StdioPolicy::RedirectForShellTool => {
// Do not create a file descriptor for stdin because otherwise some
// commands may hang forever waiting for input. For example, ripgrep has
// a heuristic where it may try to read from stdin as explained here:
// https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
}
StdioPolicy::Inherit => {
// Inherit stdin, stdout, and stderr from the parent process.
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
}
}
std::io::Result::<$cmd_type>::Ok(cmd)
}};
}
/// Spawns the appropriate child process for the ExecParams and SandboxPolicy,
/// ensuring the args and environment variables used to create the `Command`
/// (and `Child`) honor the configuration.
pub(crate) async fn spawn_child_async(
async fn spawn_child_async(
command: Vec<String>,
#[cfg_attr(not(unix), allow(unused_variables))] arg0: Option<&str>,
cwd: PathBuf,
sandbox_policy: &SandboxPolicy,
stdio_policy: StdioPolicy,
env: HashMap<String, String>,
) -> std::io::Result<Child> {
let mut cmd = configure_command!(Command, command, cwd, sandbox_policy, stdio_policy, env)?;
cmd.kill_on_drop(true).spawn()
}
// For now, we take `SandboxPolicy` as a parameter to spawn_child() because
// we need to determine whether to set the
// `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable.
// Ultimately, we should be stricter about the environment variables that
// are set for the command (as we are when spawning an MCP server), so
// instead of SandboxPolicy, we should take the exact env to use for the
// Command (i.e., `env_clear().envs(env)`).
if command.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"command args are empty",
));
}
/// Alternative version of `spawn_child_async()` that returns
/// `std::process::Child` instead of `tokio::process::Child`. This is useful for
/// spawning a child process in a thread that is not running a Tokio runtime.
pub fn spawn_child_sync(
command: Vec<String>,
cwd: PathBuf,
sandbox_policy: &SandboxPolicy,
stdio_policy: StdioPolicy,
env: HashMap<String, String>,
) -> std::io::Result<std::process::Child> {
let mut cmd = configure_command!(
std::process::Command,
command,
cwd,
sandbox_policy,
stdio_policy,
env
)?;
cmd.spawn()
let mut cmd = Command::new(&command[0]);
#[cfg(unix)]
cmd.arg0(arg0.unwrap_or_else(|| &command[0]));
cmd.args(&command[1..]);
cmd.current_dir(cwd);
cmd.env_clear();
cmd.envs(env);
if !sandbox_policy.has_full_network_access() {
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
}
match stdio_policy {
StdioPolicy::RedirectForShellTool => {
// Do not create a file descriptor for stdin because otherwise some
// commands may hang forever waiting for input. For example, ripgrep has
// a heuristic where it may try to read from stdin as explained here:
// https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
}
StdioPolicy::Inherit => {
// Inherit stdin, stdout, and stderr from the parent process.
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
}
}
cmd.kill_on_drop(true).spawn()
}
/// Consumes the output of a child process, truncating it so it is suitable for

View File

@@ -1,79 +0,0 @@
use std::io;
use std::path::Path;
use std::sync::Arc;
use crate::error::CodexErr;
use crate::error::Result;
use crate::exec::ExecParams;
use crate::exec::RawExecToolCallOutput;
use crate::exec::StdioPolicy;
use crate::exec::consume_truncated_output;
use crate::exec::spawn_child_async;
use crate::protocol::SandboxPolicy;
use tokio::sync::Notify;
pub fn exec_linux(
params: ExecParams,
ctrl_c: Arc<Notify>,
sandbox_policy: &SandboxPolicy,
) -> Result<RawExecToolCallOutput> {
// Allow READ on /
// Allow WRITE on /dev/null
let ctrl_c_copy = ctrl_c.clone();
let sandbox_policy = sandbox_policy.clone();
// Isolate thread to run the sandbox from
let tool_call_output = std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(async {
let ExecParams {
command,
cwd,
timeout_ms,
env,
} = params;
apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?;
let child = spawn_child_async(
command,
cwd,
&sandbox_policy,
StdioPolicy::RedirectForShellTool,
env,
)
.await?;
consume_truncated_output(child, ctrl_c_copy, timeout_ms).await
})
})
.join();
match tool_call_output {
Ok(Ok(output)) => Ok(output),
Ok(Err(e)) => Err(e),
Err(e) => Err(CodexErr::Io(io::Error::other(format!(
"thread join failed: {e:?}"
)))),
}
}
#[cfg(target_os = "linux")]
pub fn apply_sandbox_policy_to_current_thread(
sandbox_policy: &SandboxPolicy,
cwd: &Path,
) -> Result<()> {
crate::landlock::apply_sandbox_policy_to_current_thread(sandbox_policy, cwd)
}
#[cfg(not(target_os = "linux"))]
pub fn apply_sandbox_policy_to_current_thread(
_sandbox_policy: &SandboxPolicy,
_cwd: &Path,
) -> Result<()> {
Err(CodexErr::Io(io::Error::new(
io::ErrorKind::InvalidInput,
"linux sandbox is not supported on this platform",
)))
}

View File

@@ -18,11 +18,8 @@ mod conversation_history;
pub mod error;
pub mod exec;
pub mod exec_env;
pub mod exec_linux;
mod flags;
mod is_safe_command;
#[cfg(target_os = "linux")]
pub mod landlock;
mod mcp_connection_manager;
mod mcp_tool_call;
mod message_history;

View File

@@ -20,6 +20,7 @@ chrono = "0.4.40"
clap = { version = "4", features = ["derive"] }
codex-core = { path = "../core" }
codex-common = { path = "../common", features = ["cli", "elapsed"] }
codex-linux-sandbox = { path = "../linux-sandbox" }
mcp-types = { path = "../mcp-types" }
owo-colors = "4.2.0"
serde_json = "1"

View File

@@ -1,11 +1,38 @@
//! Entry-point for the `codex-exec` binary.
//!
//! When this CLI is invoked normally, it parses the standard `codex-exec` CLI
//! options and launches the non-interactive Codex agent. However, if it is
//! invoked with arg0 as `codex-linux-sandbox`, we instead treat the invocation
//! as a request to run the logic for the standalone `codex-linux-sandbox`
//! executable (i.e., parse any -s args and then run a *sandboxed* command under
//! Landlock + seccomp.
//!
//! This allows us to ship a completely separate set of functionality as part
//! of the `codex-exec` binary.
use clap::Parser;
use codex_exec::Cli;
use codex_exec::run_main;
use std::path::Path;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
run_main(cli).await?;
// No #[tokio::main]! If arg0 is `codex-linux-sandbox`, we delegate to
// `codex_linux_sandbox::run_main()` and do not want to start the Tokio runtime.
fn main() -> anyhow::Result<()> {
// Determine if we were invoked via the special alias.
let argv0 = std::env::args().next().unwrap_or_default();
let exe_name = Path::new(&argv0)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("");
Ok(())
if exe_name == "codex-linux-sandbox" {
codex_linux_sandbox::run_main()
}
// Regular `codex-exec` invocation parse the normal CLI.
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(async {
let cli = Cli::parse();
run_main(cli).await?;
Ok(())
})
}

View File

@@ -0,0 +1,25 @@
[package]
name = "codex-linux-sandbox"
version = { workspace = true }
edition = "2024"
[[bin]]
name = "codex-linux-sandbox"
path = "src/main.rs"
[lib]
name = "codex_linux_sandbox"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
clap = { version = "4", features = ["derive"] }
codex-core = { path = "../core" }
codex-common = { path = "../common", features = ["cli"] }
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2.172"
landlock = "0.4.1"
seccompiler = "0.5.0"

View File

@@ -0,0 +1,8 @@
# codex-linux-sandbox
This crate is responsible for producing:
- a `codex-linux-sandbox` standalone executable for Linux that is bundled with the Node.js version of the Codex CLI
- a lib crate that exposes the business logic of the executable as `run_main()` so that
- the `codex-exec` CLI can check if its arg0 is `codex-linux-sandbox` and, if so, execute as if it were `codex-linux-sandbox`
- this should also be true of the `codex` multitool CLI

View File

@@ -2,10 +2,10 @@ use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use crate::error::CodexErr;
use crate::error::Result;
use crate::error::SandboxErr;
use crate::protocol::SandboxPolicy;
use codex_core::error::CodexErr;
use codex_core::error::Result;
use codex_core::error::SandboxErr;
use codex_core::protocol::SandboxPolicy;
use landlock::ABI;
use landlock::Access;

View File

@@ -0,0 +1,12 @@
#[cfg(target_os = "linux")]
mod landlock;
#[cfg(target_os = "linux")]
mod linux_run_main;
#[cfg(target_os = "linux")]
pub use linux_run_main::run_main;
#[cfg(not(target_os = "linux"))]
pub fn run_main() -> ! {
panic!("codex-linux-sandbox is only supported on Linux");
}

View File

@@ -0,0 +1,57 @@
use clap::Parser;
use codex_common::SandboxPermissionOption;
use std::ffi::CString;
use crate::landlock::apply_sandbox_policy_to_current_thread;
#[derive(Debug, Parser)]
pub struct LandlockCommand {
#[clap(flatten)]
pub sandbox: SandboxPermissionOption,
/// Full command args to run under landlock.
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
}
pub fn run_main() -> ! {
let LandlockCommand { sandbox, command } = LandlockCommand::parse();
let sandbox_policy = match sandbox.permissions.map(Into::into) {
Some(sandbox_policy) => sandbox_policy,
None => codex_core::protocol::SandboxPolicy::new_read_only_policy(),
};
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(e) => {
panic!("failed to getcwd(): {e:?}");
}
};
if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd) {
panic!("error running landlock: {e:?}");
}
if command.is_empty() {
panic!("No command specified to execute.");
}
let c_command =
CString::new(command[0].as_str()).expect("Failed to convert command to CString");
let c_args: Vec<CString> = command
.iter()
.map(|arg| CString::new(arg.as_str()).expect("Failed to convert arg to CString"))
.collect();
let mut c_args_ptrs: Vec<*const libc::c_char> = c_args.iter().map(|arg| arg.as_ptr()).collect();
c_args_ptrs.push(std::ptr::null());
unsafe {
libc::execv(c_command.as_ptr(), c_args_ptrs.as_ptr());
}
// If execv returns, there was an error.
let err = std::io::Error::last_os_error();
panic!("Failed to execv: {err}");
}

View File

@@ -0,0 +1,6 @@
/// Note that the cwd, env, and command args are preserved in the ultimate call
/// to `execv`, so the caller is responsible for ensuring those values are
/// correct.
fn main() -> ! {
codex_linux_sandbox::run_main()
}