mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
This changes our default Landlock policy to allow `sendmsg(2)` and `recvmsg(2)` syscalls. We believe these were originally denied out of an abundance of caution, but given that `send(2)` nor `recv(2)` are allowed today [which provide comparable capability to the `*msg` equivalents], we do not believe allowing them grants any privileges beyond what we already allow. Rather than using the syscall as the security boundary, preventing access to the potentially hazardous file descriptor in the first place seems like the right layer of defense. In particular, this makes it possible for `shell-tool-mcp` to run on Linux when using a read-only sandbox for the Bash process, as demonstrated by `accept_elicitation_for_prompt_rule()` now succeeding in CI.
144 lines
4.8 KiB
Rust
144 lines
4.8 KiB
Rust
use std::collections::BTreeMap;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
|
||
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;
|
||
use landlock::AccessFs;
|
||
use landlock::CompatLevel;
|
||
use landlock::Compatible;
|
||
use landlock::Ruleset;
|
||
use landlock::RulesetAttr;
|
||
use landlock::RulesetCreatedAttr;
|
||
use seccompiler::BpfProgram;
|
||
use seccompiler::SeccompAction;
|
||
use seccompiler::SeccompCmpArgLen;
|
||
use seccompiler::SeccompCmpOp;
|
||
use seccompiler::SeccompCondition;
|
||
use seccompiler::SeccompFilter;
|
||
use seccompiler::SeccompRule;
|
||
use seccompiler::TargetArch;
|
||
use seccompiler::apply_filter;
|
||
|
||
/// Apply sandbox policies inside this thread so only the child inherits
|
||
/// them, not the entire CLI process.
|
||
pub(crate) fn apply_sandbox_policy_to_current_thread(
|
||
sandbox_policy: &SandboxPolicy,
|
||
cwd: &Path,
|
||
) -> Result<()> {
|
||
if !sandbox_policy.has_full_network_access() {
|
||
install_network_seccomp_filter_on_current_thread()?;
|
||
}
|
||
|
||
if !sandbox_policy.has_full_disk_write_access() {
|
||
let writable_roots = sandbox_policy
|
||
.get_writable_roots_with_cwd(cwd)
|
||
.into_iter()
|
||
.map(|writable_root| writable_root.root)
|
||
.collect();
|
||
install_filesystem_landlock_rules_on_current_thread(writable_roots)?;
|
||
}
|
||
|
||
// TODO(ragona): Add appropriate restrictions if
|
||
// `sandbox_policy.has_full_disk_read_access()` is `false`.
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 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.
|
||
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);
|
||
|
||
let mut ruleset = Ruleset::default()
|
||
.set_compatibility(CompatLevel::BestEffort)
|
||
.handle_access(access_rw)?
|
||
.create()?
|
||
.add_rules(landlock::path_beneath_rules(&["/"], access_ro))?
|
||
.add_rules(landlock::path_beneath_rules(&["/dev/null"], access_rw))?
|
||
.set_no_new_privs(true);
|
||
|
||
if !writable_roots.is_empty() {
|
||
ruleset = ruleset.add_rules(landlock::path_beneath_rules(&writable_roots, access_rw))?;
|
||
}
|
||
|
||
let status = ruleset.restrict_self()?;
|
||
|
||
if status.ruleset == landlock::RulesetStatus::NotEnforced {
|
||
return Err(CodexErr::Sandbox(SandboxErr::LandlockRestrict));
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Installs a seccomp filter that blocks outbound network access except for
|
||
/// AF_UNIX domain sockets.
|
||
fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(), SandboxErr> {
|
||
// Build rule map.
|
||
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
|
||
|
||
// Helper – insert unconditional deny rule for syscall number.
|
||
let mut deny_syscall = |nr: i64| {
|
||
rules.insert(nr, vec![]); // empty rule vec = unconditional match
|
||
};
|
||
|
||
deny_syscall(libc::SYS_connect);
|
||
deny_syscall(libc::SYS_accept);
|
||
deny_syscall(libc::SYS_accept4);
|
||
deny_syscall(libc::SYS_bind);
|
||
deny_syscall(libc::SYS_listen);
|
||
deny_syscall(libc::SYS_getpeername);
|
||
deny_syscall(libc::SYS_getsockname);
|
||
deny_syscall(libc::SYS_shutdown);
|
||
deny_syscall(libc::SYS_sendto);
|
||
deny_syscall(libc::SYS_sendmmsg);
|
||
// NOTE: allowing recvfrom allows some tools like: `cargo clippy` to run
|
||
// with their socketpair + child processes for sub-proc management
|
||
// deny_syscall(libc::SYS_recvfrom);
|
||
deny_syscall(libc::SYS_recvmmsg);
|
||
deny_syscall(libc::SYS_getsockopt);
|
||
deny_syscall(libc::SYS_setsockopt);
|
||
deny_syscall(libc::SYS_ptrace);
|
||
|
||
// For `socket` we allow AF_UNIX (arg0 == AF_UNIX) and deny everything else.
|
||
let unix_only_rule = SeccompRule::new(vec![SeccompCondition::new(
|
||
0, // first argument (domain)
|
||
SeccompCmpArgLen::Dword,
|
||
SeccompCmpOp::Ne,
|
||
libc::AF_UNIX as u64,
|
||
)?])?;
|
||
|
||
rules.insert(libc::SYS_socket, vec![unix_only_rule.clone()]);
|
||
rules.insert(libc::SYS_socketpair, vec![unix_only_rule]); // always deny (Unix can use socketpair but fine, keep open?)
|
||
|
||
let filter = SeccompFilter::new(
|
||
rules,
|
||
SeccompAction::Allow, // default – allow
|
||
SeccompAction::Errno(libc::EPERM as u32), // when rule matches – return EPERM
|
||
if cfg!(target_arch = "x86_64") {
|
||
TargetArch::x86_64
|
||
} else if cfg!(target_arch = "aarch64") {
|
||
TargetArch::aarch64
|
||
} else {
|
||
unimplemented!("unsupported architecture for seccomp filter");
|
||
},
|
||
)?;
|
||
|
||
let prog: BpfProgram = filter.try_into()?;
|
||
|
||
apply_filter(&prog)?;
|
||
|
||
Ok(())
|
||
}
|