feat: add debug landlock subcommand comparable to debug seatbelt

This commit is contained in:
Michael Bolin
2025-04-28 15:03:27 -07:00
parent 40460faf2a
commit 7b98df230c
4 changed files with 97 additions and 4 deletions

View File

@@ -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<String>,
sandbox_policy: SandboxPolicy,
writable_roots: Vec<PathBuf>,
) -> 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<ExitStatus> {
// 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);
}
}

View File

@@ -1,3 +1,5 @@
#[cfg(target_os = "linux")]
mod landlock;
mod proto;
mod seatbelt;
@@ -63,11 +65,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<PathBuf>,
@@ -80,6 +85,21 @@ struct SeatbeltCommand {
command: Vec<String>,
}
#[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<PathBuf>,
/// 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<String>,
}
#[derive(Debug, Parser)]
struct ReplProto {}
@@ -111,6 +131,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.");
}
},
}

View File

@@ -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;

View File

@@ -72,7 +72,15 @@ pub async fn exec_linux(
}
}
fn install_filesystem_landlock_rules_on_current_thread(writable_roots: Vec<PathBuf>) -> 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<PathBuf>,
) -> 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<PathB
Ok(())
}
fn install_network_seccomp_filter_on_current_thread() -> 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<i64, Vec<SeccompRule>> = BTreeMap::new();