From e79549f0392c3c8dddd6e46e3454cb9de343acb7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 28 Apr 2025 16:37:05 -0700 Subject: [PATCH] feat: add `debug landlock` subcommand comparable to `debug seatbelt` (#715) This PR adds a `debug landlock` subcommand to the Codex CLI for testing how Codex would execute a command using the specified sandbox policy. Built and ran this code in the `rust:latest` Docker container. In the container, hitting the network with vanilla `curl` succeeds: ``` $ curl google.com 301 Moved

301 Moved

The document has moved here. ``` whereas this fails, as expected: ``` $ cargo run -- debug landlock -s network-restricted -- curl google.com curl: (6) getaddrinfo() thread failed to start ``` --- codex-rs/cli/src/landlock.rs | 51 ++++++++++++++++++++++++++++++++++++ codex-rs/cli/src/main.rs | 34 +++++++++++++++++++++++- codex-rs/core/src/lib.rs | 2 +- codex-rs/core/src/linux.rs | 14 ++++++++-- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 codex-rs/cli/src/landlock.rs diff --git a/codex-rs/cli/src/landlock.rs b/codex-rs/cli/src/landlock.rs new file mode 100644 index 00000000..be2ba1e3 --- /dev/null +++ b/codex-rs/cli/src/landlock.rs @@ -0,0 +1,51 @@ +//! `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::protocol::SandboxPolicy; +use std::os::unix::process::ExitStatusExt; +use std::path::PathBuf; +use std::process; +use std::process::Command; +use std::process::ExitStatus; + +/// Execute `command` in a Linux sandbox (Landlock + seccomp) the way Codex +/// would. +pub(crate) fn run_landlock( + command: Vec, + sandbox_policy: SandboxPolicy, + writable_roots: Vec, +) -> anyhow::Result<()> { + if command.is_empty() { + anyhow::bail!("command args are empty"); + } + + // Spawn a new thread and apply the sandbox policies there. + let handle = std::thread::spawn(move || -> anyhow::Result { + // Apply sandbox policies inside this thread so only the child inherits + // them, not the entire CLI process. + if sandbox_policy.is_network_restricted() { + codex_core::linux::install_network_seccomp_filter_on_current_thread()?; + } + + if sandbox_policy.is_file_write_restricted() { + codex_core::linux::install_filesystem_landlock_rules_on_current_thread(writable_roots)?; + } + + let status = Command::new(&command[0]).args(&command[1..]).status()?; + Ok(status) + }); + let status = handle + .join() + .map_err(|e| anyhow::anyhow!("Failed to join thread: {e:?}"))??; + + // Use ExitStatus to derive the exit code. + if let Some(code) = status.code() { + process::exit(code); + } else if let Some(signal) = status.signal() { + process::exit(128 + signal); + } else { + process::exit(1); + } +} diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7d8987c0..d8a58de8 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -1,3 +1,5 @@ +#[cfg(target_os = "linux")] +mod landlock; mod proto; mod seatbelt; @@ -58,11 +60,14 @@ struct DebugArgs { enum DebugCommand { /// Run a command under Seatbelt (macOS only). Seatbelt(SeatbeltCommand), + + /// Run a command under Landlock+seccomp (Linux only). + Landlock(LandlockCommand), } #[derive(Debug, Parser)] struct SeatbeltCommand { - /// Writable folder for sandbox in full-auto mode (can be specified multiple times). + /// Writable folder for sandbox (can be specified multiple times). #[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)] writable_roots: Vec, @@ -75,6 +80,21 @@ struct SeatbeltCommand { command: Vec, } +#[derive(Debug, Parser)] +struct LandlockCommand { + /// Writable folder for sandbox (can be specified multiple times). + #[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)] + writable_roots: Vec, + + /// Configure the process restrictions for the command. + #[arg(long = "sandbox", short = 's')] + sandbox_policy: SandboxModeCliArg, + + /// Full command args to run under landlock. + #[arg(trailing_var_arg = true)] + command: Vec, +} + #[derive(Debug, Parser)] struct ReplProto {} @@ -103,6 +123,18 @@ async fn main() -> anyhow::Result<()> { }) => { seatbelt::run_seatbelt(command, sandbox_policy.into(), writable_roots).await?; } + #[cfg(target_os = "linux")] + DebugCommand::Landlock(LandlockCommand { + command, + sandbox_policy, + writable_roots, + }) => { + landlock::run_landlock(command, sandbox_policy.into(), writable_roots)?; + } + #[cfg(not(target_os = "linux"))] + DebugCommand::Landlock(_) => { + anyhow::bail!("Landlock is only supported on Linux."); + } }, } diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index d517e688..e7d4e32a 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -14,7 +14,7 @@ pub mod exec; mod flags; mod is_safe_command; #[cfg(target_os = "linux")] -mod linux; +pub mod linux; mod models; pub mod protocol; mod safety; diff --git a/codex-rs/core/src/linux.rs b/codex-rs/core/src/linux.rs index 75d70e79..9f9d44b0 100644 --- a/codex-rs/core/src/linux.rs +++ b/codex-rs/core/src/linux.rs @@ -72,7 +72,15 @@ pub async fn exec_linux( } } -fn install_filesystem_landlock_rules_on_current_thread(writable_roots: Vec) -> Result<()> { +/// Installs Landlock file-system rules on the current thread allowing read +/// access to the entire file-system while restricting write access to +/// `/dev/null` and the provided list of `writable_roots`. +/// +/// # Errors +/// Returns [`CodexErr::Sandbox`] variants when the ruleset fails to apply. +pub fn install_filesystem_landlock_rules_on_current_thread( + writable_roots: Vec, +) -> Result<()> { let abi = ABI::V5; let access_rw = AccessFs::from_all(abi); let access_ro = AccessFs::from_read(abi); @@ -98,7 +106,9 @@ fn install_filesystem_landlock_rules_on_current_thread(writable_roots: Vec std::result::Result<(), SandboxErr> { +/// Installs a seccomp filter that blocks outbound network access except for +/// AF_UNIX domain sockets. +pub fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(), SandboxErr> { // Build rule map. let mut rules: BTreeMap> = BTreeMap::new();