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

This commit is contained in:
Michael Bolin
2025-05-23 11:12:54 -07:00
parent d1de7bb383
commit e1b4dbeddb
28 changed files with 866 additions and 727 deletions
+1 -4
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"
+91
View File
@@ -0,0 +1,91 @@
use std::path::PathBuf;
use codex_common::SandboxPermissionOption;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::exec::StdioPolicy;
use codex_core::exec::spawn_command_under_linux_sandbox;
use codex_core::exec::spawn_command_under_seatbelt;
use codex_core::exec_env::create_env;
use codex_core::protocol::SandboxPolicy;
use crate::LandlockCommand;
use crate::SeatbeltCommand;
use crate::exit_status::handle_exit_status;
pub async fn run_command_under_seatbelt(command: SeatbeltCommand) -> anyhow::Result<()> {
let SeatbeltCommand {
full_auto,
sandbox,
command,
} = command;
run_command_under_sandbox(full_auto, sandbox, command, None, SandboxType::Seatbelt).await
}
pub async fn run_command_under_landlock(command: LandlockCommand) -> anyhow::Result<()> {
let LandlockCommand {
full_auto,
sandbox,
command,
} = command;
run_command_under_sandbox(full_auto, sandbox, command, None, SandboxType::Landlock).await
}
enum SandboxType {
Seatbelt,
Landlock,
}
async fn run_command_under_sandbox(
full_auto: bool,
sandbox: SandboxPermissionOption,
command: Vec<String>,
codex_linux_sandbox_exe: Option<PathBuf>,
sandbox_type: SandboxType,
) -> anyhow::Result<()> {
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),
codex_linux_sandbox_exe,
..Default::default()
})?;
let stdio_policy = StdioPolicy::Inherit;
let env = create_env(&config.shell_environment_policy);
let mut child = match sandbox_type {
SandboxType::Seatbelt => {
spawn_command_under_seatbelt(command, &config.sandbox_policy, cwd, stdio_policy, env)
.await?
}
SandboxType::Landlock => {
#[expect(clippy::expect_used)]
let codex_linux_sandbox_exe = config
.codex_linux_sandbox_exe
.expect("codex-linux-sandbox executable not found");
spawn_command_under_linux_sandbox(
codex_linux_sandbox_exe,
command,
&config.sandbox_policy,
cwd,
stdio_policy,
env,
)
.await?
}
};
let status = child.wait().await?;
handle_exit_status(status);
}
pub fn create_sandbox_policy(full_auto: bool, sandbox: SandboxPermissionOption) -> SandboxPolicy {
if full_auto {
SandboxPolicy::new_full_auto_policy()
} else {
match sandbox.permissions.map(Into::into) {
Some(sandbox_policy) => sandbox_policy,
None => SandboxPolicy::new_read_only_policy(),
}
}
}
-37
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);
}
+1 -15
View File
@@ -1,12 +1,9 @@
pub mod debug_sandbox;
mod exit_status;
#[cfg(unix)]
pub mod landlock;
pub mod proto;
pub mod seatbelt;
use clap::Parser;
use codex_common::SandboxPermissionOption;
use codex_core::protocol::SandboxPolicy;
#[derive(Debug, Parser)]
pub struct SeatbeltCommand {
@@ -35,14 +32,3 @@ pub struct LandlockCommand {
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
}
pub fn create_sandbox_policy(full_auto: bool, sandbox: SandboxPermissionOption) -> SandboxPolicy {
if full_auto {
SandboxPolicy::new_full_auto_policy()
} else {
match sandbox.permissions.map(Into::into) {
Some(sandbox_policy) => sandbox_policy,
None => SandboxPolicy::new_read_only_policy(),
}
}
}
-28
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(())
}
+12 -40
View File
@@ -1,15 +1,10 @@
use std::path::PathBuf;
use clap::Parser;
use codex_cli::LandlockCommand;
use codex_cli::SeatbeltCommand;
use codex_cli::create_sandbox_policy;
use codex_cli::proto;
use codex_cli::seatbelt;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
use std::path::PathBuf;
use crate::proto::ProtoCli;
@@ -66,14 +61,14 @@ enum DebugCommand {
#[derive(Debug, Parser)]
struct ReplProto {}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
std::env::current_exe().ok()
} else {
None
};
fn main() -> anyhow::Result<()> {
codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move {
cli_main(codex_linux_sandbox_exe).await?;
Ok(())
})
}
async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
let cli = MultitoolCli::parse();
match cli.subcommand {
@@ -90,34 +85,11 @@ async fn main() -> anyhow::Result<()> {
proto::run_main(proto_cli).await?;
}
Some(Subcommand::Debug(debug_args)) => match debug_args.cmd {
DebugCommand::Seatbelt(SeatbeltCommand {
command,
sandbox,
full_auto,
}) => {
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
let config = Config::load_with_overrides(ConfigOverrides {
sandbox_policy: Some(sandbox_policy),
..Default::default()
})?;
seatbelt::run_seatbelt(command, &config).await?;
DebugCommand::Seatbelt(seatbelt_command) => {
codex_cli::debug_sandbox::run_command_under_seatbelt(seatbelt_command).await?;
}
#[cfg(unix)]
DebugCommand::Landlock(LandlockCommand {
command,
sandbox,
full_auto,
}) => {
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
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.");
DebugCommand::Landlock(landlock_command) => {
codex_cli::debug_sandbox::run_command_under_landlock(landlock_command).await?;
}
},
}
-21
View File
@@ -1,21 +0,0 @@
use codex_core::config::Config;
use codex_core::exec::StdioPolicy;
use codex_core::exec::spawn_command_under_seatbelt;
use codex_core::exec_env::create_env;
use crate::exit_status::handle_exit_status;
pub async fn run_seatbelt(command: Vec<String>, config: &Config) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
let env = create_env(&config.shell_environment_policy);
let mut child = spawn_command_under_seatbelt(
command,
&config.sandbox_policy,
cwd,
StdioPolicy::Inherit,
env,
)
.await?;
let status = child.wait().await?;
handle_exit_status(status);
}