When using codex-tui on a linux system I was unable to run `cargo clippy` inside of codex due to: ``` [pid 3548377] socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, <unfinished ...> [pid 3548370] close(8 <unfinished ...> [pid 3548377] <... socketpair resumed>0x7ffb97f4ed60) = -1 EPERM (Operation not permitted) ``` And ``` 3611300 <... recvfrom resumed>0x708b8b5cffe0, 8, 0, NULL, NULL) = -1 EPERM (Operation not permitted) ``` This PR: * Fixes a bug that disallowed AF_UNIX to allow it on `socket()` * Adds recvfrom() to the syscall allow list, this should be fine since we disable opening new sockets. But we should validate there is not a open socket inheritance issue. * Allow socketpair to be called for AF_UNIX * Adds tests for AF_UNIX components * All of which allows running `cargo clippy` within the sandbox on linux, and possibly other tooling using a fork server model + AF_UNIX comms.
146 lines
4.9 KiB
Rust
146 lines
4.9 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_sendmsg);
|
||
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_recvmsg);
|
||
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(())
|
||
}
|