refactor: make bubblewrap the default Linux sandbox (#13996)
## Summary - make bubblewrap the default Linux sandbox and keep `use_legacy_landlock` as the only override - remove `use_linux_sandbox_bwrap` from feature, config, schema, and docs surfaces - update Linux sandbox selection, CLI/config plumbing, and related tests/docs to match the new default - fold in the follow-up CI fixes for request-permissions responses and Linux read-only sandbox error text
This commit is contained in:
parent
b5f927b973
commit
04892b4ceb
29 changed files with 184 additions and 222 deletions
|
|
@ -1723,7 +1723,7 @@ impl CodexMessageProcessor {
|
|||
let outgoing = self.outgoing.clone();
|
||||
let request_for_task = request.clone();
|
||||
let started_network_proxy_for_task = started_network_proxy;
|
||||
let use_linux_sandbox_bwrap = self.config.features.enabled(Feature::UseLinuxSandboxBwrap);
|
||||
let use_legacy_landlock = self.config.features.use_legacy_landlock();
|
||||
let size = match size.map(crate::command_exec::terminal_size_from_protocol) {
|
||||
Some(Ok(size)) => Some(size),
|
||||
Some(Err(error)) => {
|
||||
|
|
@ -1740,7 +1740,7 @@ impl CodexMessageProcessor {
|
|||
effective_network_sandbox_policy,
|
||||
sandbox_cwd.as_path(),
|
||||
&codex_linux_sandbox_exe,
|
||||
use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock,
|
||||
) {
|
||||
Ok(exec_request) => {
|
||||
if let Err(error) = self
|
||||
|
|
|
|||
|
|
@ -710,6 +710,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
|
|||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let marker = format!(
|
||||
"codex-command-exec-marker-{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_nanos()
|
||||
);
|
||||
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
|
|
@ -726,7 +732,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
|
|||
"command/exec",
|
||||
101,
|
||||
Some(serde_json::json!({
|
||||
"command": ["sh", "-lc", "printf 'ready\\n%s\\n' $$; sleep 30"],
|
||||
"command": [
|
||||
"python3",
|
||||
"-c",
|
||||
"import time; print('ready', flush=True); time.sleep(30)",
|
||||
marker,
|
||||
],
|
||||
"processId": "shared-process",
|
||||
"streamStdoutStderr": true,
|
||||
})),
|
||||
|
|
@ -737,12 +748,8 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
|
|||
assert_eq!(delta.process_id, "shared-process");
|
||||
assert_eq!(delta.stream, CommandExecOutputStream::Stdout);
|
||||
let delta_text = String::from_utf8(STANDARD.decode(&delta.delta_base64)?)?;
|
||||
let pid = delta_text
|
||||
.lines()
|
||||
.last()
|
||||
.context("delta should include shell pid")?
|
||||
.parse::<u32>()
|
||||
.context("parse shell pid")?;
|
||||
assert!(delta_text.contains("ready"));
|
||||
wait_for_process_marker(&marker, true).await?;
|
||||
|
||||
send_request(
|
||||
&mut ws2,
|
||||
|
|
@ -766,12 +773,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
|
|||
terminate_error.error.message,
|
||||
"no active command/exec for process id \"shared-process\""
|
||||
);
|
||||
assert!(process_is_alive(pid)?);
|
||||
wait_for_process_marker(&marker, true).await?;
|
||||
|
||||
assert_no_message(&mut ws2, Duration::from_millis(250)).await?;
|
||||
ws1.close(None).await?;
|
||||
|
||||
wait_for_process_exit(pid).await?;
|
||||
wait_for_process_marker(&marker, false).await?;
|
||||
|
||||
process
|
||||
.kill()
|
||||
|
|
@ -855,24 +862,25 @@ async fn read_initialize_response(
|
|||
}
|
||||
}
|
||||
|
||||
async fn wait_for_process_exit(pid: u32) -> Result<()> {
|
||||
async fn wait_for_process_marker(marker: &str, should_exist: bool) -> Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
if !process_is_alive(pid)? {
|
||||
if process_with_marker_exists(marker)? == should_exist {
|
||||
return Ok(());
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
anyhow::bail!("process {pid} was still alive after websocket disconnect");
|
||||
let expectation = if should_exist { "appear" } else { "exit" };
|
||||
anyhow::bail!("process marker {marker:?} did not {expectation} before timeout");
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn process_is_alive(pid: u32) -> Result<bool> {
|
||||
let status = std::process::Command::new("kill")
|
||||
.arg("-0")
|
||||
.arg(pid.to_string())
|
||||
.status()
|
||||
.context("spawn kill -0")?;
|
||||
Ok(status.success())
|
||||
fn process_with_marker_exists(marker: &str) -> Result<bool> {
|
||||
let output = std::process::Command::new("ps")
|
||||
.args(["-axo", "command"])
|
||||
.output()
|
||||
.context("spawn ps -axo command")?;
|
||||
let stdout = String::from_utf8(output.stdout).context("decode ps output")?;
|
||||
Ok(stdout.lines().any(|line| line.contains(marker)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,19 +250,18 @@ async fn run_command_under_sandbox(
|
|||
.await?
|
||||
}
|
||||
SandboxType::Landlock => {
|
||||
use codex_core::features::Feature;
|
||||
#[expect(clippy::expect_used)]
|
||||
let codex_linux_sandbox_exe = config
|
||||
.codex_linux_sandbox_exe
|
||||
.expect("codex-linux-sandbox executable not found");
|
||||
let use_bwrap_sandbox = config.features.enabled(Feature::UseLinuxSandboxBwrap);
|
||||
let use_legacy_landlock = config.features.use_legacy_landlock();
|
||||
spawn_command_under_linux_sandbox(
|
||||
codex_linux_sandbox_exe,
|
||||
command,
|
||||
cwd,
|
||||
config.permissions.sandbox_policy.get(),
|
||||
sandbox_policy_cwd.as_path(),
|
||||
use_bwrap_sandbox,
|
||||
use_legacy_landlock,
|
||||
stdio_policy,
|
||||
network.as_ref(),
|
||||
env,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub struct LandlockCommand {
|
|||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
/// Full command args to run under landlock.
|
||||
/// Full command args to run under the Linux sandbox.
|
||||
#[arg(trailing_var_arg = true)]
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ enum SandboxCommand {
|
|||
#[clap(visible_alias = "seatbelt")]
|
||||
Macos(SeatbeltCommand),
|
||||
|
||||
/// Run a command under Landlock+seccomp (Linux only).
|
||||
/// Run a command under the Linux sandbox (bubblewrap by default).
|
||||
#[clap(visible_alias = "landlock")]
|
||||
Linux(LandlockCommand),
|
||||
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@
|
|||
"unified_exec": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"use_linux_sandbox_bwrap": {
|
||||
"use_legacy_landlock": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"voice_transcription": {
|
||||
|
|
@ -1985,7 +1985,7 @@
|
|||
"unified_exec": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"use_linux_sandbox_bwrap": {
|
||||
"use_legacy_landlock": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"voice_transcription": {
|
||||
|
|
|
|||
|
|
@ -1290,9 +1290,7 @@ impl Session {
|
|||
cwd.clone(),
|
||||
session_configuration.sandbox_policy.get(),
|
||||
session_configuration.windows_sandbox_level,
|
||||
per_turn_config
|
||||
.features
|
||||
.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
per_turn_config.features.use_legacy_landlock(),
|
||||
));
|
||||
let (current_date, timezone) = local_time_context();
|
||||
TurnContext {
|
||||
|
|
@ -1802,7 +1800,7 @@ impl Session {
|
|||
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd: session_configuration.cwd.clone(),
|
||||
use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: config.features.use_legacy_landlock(),
|
||||
};
|
||||
let mut required_mcp_servers: Vec<String> = mcp_servers
|
||||
.iter()
|
||||
|
|
@ -2275,9 +2273,7 @@ impl Session {
|
|||
sandbox_policy: per_turn_config.permissions.sandbox_policy.get().clone(),
|
||||
codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd: per_turn_config.cwd.clone(),
|
||||
use_linux_sandbox_bwrap: per_turn_config
|
||||
.features
|
||||
.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: per_turn_config.features.use_legacy_landlock(),
|
||||
};
|
||||
if let Err(e) = self
|
||||
.services
|
||||
|
|
@ -3938,7 +3934,7 @@ impl Session {
|
|||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd: turn_context.cwd.clone(),
|
||||
use_linux_sandbox_bwrap: turn_context.features.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: turn_context.features.use_legacy_landlock(),
|
||||
};
|
||||
{
|
||||
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
||||
|
|
@ -5215,9 +5211,7 @@ async fn spawn_review_thread(
|
|||
parent_turn_context.cwd.clone(),
|
||||
parent_turn_context.sandbox_policy.get(),
|
||||
parent_turn_context.windows_sandbox_level,
|
||||
parent_turn_context
|
||||
.features
|
||||
.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
parent_turn_context.features.use_legacy_landlock(),
|
||||
));
|
||||
|
||||
let review_turn_context = TurnContext {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ use crate::config::types::AppsConfigToml;
|
|||
use crate::default_client::create_client;
|
||||
use crate::default_client::is_first_party_chat_originator;
|
||||
use crate::default_client::originator;
|
||||
use crate::features::Feature;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
|
|
@ -203,7 +202,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
|||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
|
||||
use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: config.features.use_legacy_landlock(),
|
||||
};
|
||||
|
||||
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ pub async fn process_exec_tool_call(
|
|||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
use_linux_sandbox_bwrap: bool,
|
||||
use_legacy_landlock: bool,
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
) -> Result<ExecToolCallOutput> {
|
||||
let exec_req = build_exec_request(
|
||||
|
|
@ -195,7 +195,7 @@ pub async fn process_exec_tool_call(
|
|||
network_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
codex_linux_sandbox_exe,
|
||||
use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock,
|
||||
)?;
|
||||
|
||||
// Route through the sandboxing module for a single, unified execution path.
|
||||
|
|
@ -211,7 +211,7 @@ pub fn build_exec_request(
|
|||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
use_linux_sandbox_bwrap: bool,
|
||||
use_legacy_landlock: bool,
|
||||
) -> Result<ExecRequest> {
|
||||
let windows_sandbox_level = params.windows_sandbox_level;
|
||||
let enforce_managed_network = params.network.is_some();
|
||||
|
|
@ -269,7 +269,7 @@ pub fn build_exec_request(
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(),
|
||||
use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
})
|
||||
.map_err(CodexErr::from)?;
|
||||
|
|
|
|||
|
|
@ -108,8 +108,9 @@ pub enum Feature {
|
|||
WebSearchCached,
|
||||
/// Legacy search-tool feature flag kept for backward compatibility.
|
||||
SearchTool,
|
||||
/// Use the bubblewrap-based Linux sandbox pipeline.
|
||||
UseLinuxSandboxBwrap,
|
||||
/// Use the legacy Landlock Linux sandbox fallback instead of the default
|
||||
/// bubblewrap pipeline.
|
||||
UseLegacyLandlock,
|
||||
/// Allow the model to request approval and propose exec rules.
|
||||
RequestRule,
|
||||
/// Enable Windows sandbox (restricted token) on Windows.
|
||||
|
|
@ -284,6 +285,10 @@ impl Features {
|
|||
self.enabled(Feature::Apps) && auth.is_some_and(CodexAuth::is_chatgpt_auth)
|
||||
}
|
||||
|
||||
pub fn use_legacy_landlock(&self) -> bool {
|
||||
self.enabled(Feature::UseLegacyLandlock)
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, f: Feature) -> &mut Self {
|
||||
self.enabled.insert(f);
|
||||
self
|
||||
|
|
@ -636,16 +641,9 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::UseLinuxSandboxBwrap,
|
||||
key: "use_linux_sandbox_bwrap",
|
||||
#[cfg(target_os = "linux")]
|
||||
stage: Stage::Experimental {
|
||||
name: "Bubblewrap sandbox",
|
||||
menu_description: "Try the new linux sandbox based on bubblewrap.",
|
||||
announcement: "NEW: Linux bubblewrap sandbox offers stronger filesystem and network controls than Landlock alone, including keeping .git and .codex read-only inside writable workspaces. Enable it in /experimental and restart Codex to try it.",
|
||||
},
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
stage: Stage::UnderDevelopment,
|
||||
id: Feature::UseLegacyLandlock,
|
||||
key: "use_legacy_landlock",
|
||||
stage: Stage::Stable,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
|
|
@ -932,24 +930,10 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn use_linux_sandbox_bwrap_is_experimental_on_linux() {
|
||||
assert!(matches!(
|
||||
Feature::UseLinuxSandboxBwrap.stage(),
|
||||
Stage::Experimental { .. }
|
||||
));
|
||||
assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[test]
|
||||
fn use_linux_sandbox_bwrap_is_under_development_off_linux() {
|
||||
assert_eq!(
|
||||
Feature::UseLinuxSandboxBwrap.stage(),
|
||||
Stage::UnderDevelopment
|
||||
);
|
||||
assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false);
|
||||
fn use_legacy_landlock_is_stable_and_disabled_by_default() {
|
||||
assert_eq!(Feature::UseLegacyLandlock.stage(), Stage::Stable);
|
||||
assert_eq!(Feature::UseLegacyLandlock.default_enabled(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use std::path::PathBuf;
|
|||
use tokio::process::Child;
|
||||
|
||||
/// Spawn a shell tool command under the Linux sandbox helper
|
||||
/// (codex-linux-sandbox), which currently uses bubblewrap for filesystem
|
||||
/// (codex-linux-sandbox), which defaults to bubblewrap for filesystem
|
||||
/// isolation plus seccomp for network restrictions.
|
||||
///
|
||||
/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux
|
||||
|
|
@ -25,7 +25,7 @@ pub async fn spawn_command_under_linux_sandbox<P>(
|
|||
command_cwd: PathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_bwrap_sandbox: bool,
|
||||
use_legacy_landlock: bool,
|
||||
stdio_policy: StdioPolicy,
|
||||
network: Option<&NetworkProxy>,
|
||||
env: HashMap<String, String>,
|
||||
|
|
@ -42,7 +42,7 @@ where
|
|||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
use_bwrap_sandbox,
|
||||
use_legacy_landlock,
|
||||
allow_network_for_proxy(false),
|
||||
);
|
||||
let arg0 = Some("codex-linux-sandbox");
|
||||
|
|
@ -69,7 +69,7 @@ pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool {
|
|||
/// Converts the sandbox policies into the CLI invocation for
|
||||
/// `codex-linux-sandbox`.
|
||||
///
|
||||
/// The helper performs the actual sandboxing (bubblewrap + seccomp) after
|
||||
/// The helper performs the actual sandboxing (bubblewrap by default + seccomp) after
|
||||
/// parsing these arguments. Policy JSON flags are emitted before helper feature
|
||||
/// flags so the argv order matches the helper's CLI shape. See
|
||||
/// `docs/linux_sandbox.md` for the Linux semantics.
|
||||
|
|
@ -80,7 +80,7 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies(
|
|||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_bwrap_sandbox: bool,
|
||||
use_legacy_landlock: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> Vec<String> {
|
||||
let sandbox_policy_json = serde_json::to_string(sandbox_policy)
|
||||
|
|
@ -104,8 +104,8 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies(
|
|||
"--network-sandbox-policy".to_string(),
|
||||
network_policy_json,
|
||||
];
|
||||
if use_bwrap_sandbox {
|
||||
linux_cmd.push("--use-bwrap-sandbox".to_string());
|
||||
if use_legacy_landlock {
|
||||
linux_cmd.push("--use-legacy-landlock".to_string());
|
||||
}
|
||||
if allow_network_for_proxy {
|
||||
linux_cmd.push("--allow-network-for-proxy".to_string());
|
||||
|
|
@ -121,7 +121,7 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies(
|
|||
pub(crate) fn create_linux_sandbox_command_args(
|
||||
command: Vec<String>,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_bwrap_sandbox: bool,
|
||||
use_legacy_landlock: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> Vec<String> {
|
||||
let sandbox_policy_cwd = sandbox_policy_cwd
|
||||
|
|
@ -130,8 +130,8 @@ pub(crate) fn create_linux_sandbox_command_args(
|
|||
.to_string();
|
||||
|
||||
let mut linux_cmd: Vec<String> = vec!["--sandbox-policy-cwd".to_string(), sandbox_policy_cwd];
|
||||
if use_bwrap_sandbox {
|
||||
linux_cmd.push("--use-bwrap-sandbox".to_string());
|
||||
if use_legacy_landlock {
|
||||
linux_cmd.push("--use-legacy-landlock".to_string());
|
||||
}
|
||||
if allow_network_for_proxy {
|
||||
linux_cmd.push("--allow-network-for-proxy".to_string());
|
||||
|
|
@ -153,20 +153,20 @@ mod tests {
|
|||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn bwrap_flags_are_feature_gated() {
|
||||
fn legacy_landlock_flag_is_included_when_requested() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let cwd = Path::new("/tmp");
|
||||
|
||||
let with_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, true, false);
|
||||
let default_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, false, false);
|
||||
assert_eq!(
|
||||
with_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
|
||||
true
|
||||
default_bwrap.contains(&"--use-legacy-landlock".to_string()),
|
||||
false
|
||||
);
|
||||
|
||||
let without_bwrap = create_linux_sandbox_command_args(command, cwd, false, false);
|
||||
let legacy_landlock = create_linux_sandbox_command_args(command, cwd, true, false);
|
||||
assert_eq!(
|
||||
without_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
|
||||
false
|
||||
legacy_landlock.contains(&"--use-legacy-landlock".to_string()),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent
|
|||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
|
||||
use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: config.features.use_legacy_landlock(),
|
||||
};
|
||||
|
||||
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
|
||||
|
|
|
|||
|
|
@ -591,7 +591,7 @@ pub struct SandboxState {
|
|||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub sandbox_cwd: PathBuf,
|
||||
#[serde(default)]
|
||||
pub use_linux_sandbox_bwrap: bool,
|
||||
pub use_legacy_landlock: bool,
|
||||
}
|
||||
|
||||
/// A thin wrapper around a set of running [`RmcpClient`] instances.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use codex_protocol::config_types::WindowsSandboxLevel;
|
|||
pub(crate) fn sandbox_tag(
|
||||
policy: &SandboxPolicy,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
use_linux_sandbox_bwrap: bool,
|
||||
use_legacy_landlock: bool,
|
||||
) -> &'static str {
|
||||
if matches!(policy, SandboxPolicy::DangerFullAccess) {
|
||||
return "none";
|
||||
|
|
@ -18,7 +18,7 @@ pub(crate) fn sandbox_tag(
|
|||
{
|
||||
return "windows_elevated";
|
||||
}
|
||||
if cfg!(target_os = "linux") && use_linux_sandbox_bwrap {
|
||||
if cfg!(target_os = "linux") && !use_legacy_landlock {
|
||||
return "linux_bubblewrap";
|
||||
}
|
||||
|
||||
|
|
@ -38,33 +38,33 @@ mod tests {
|
|||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn danger_full_access_is_untagged_even_when_bubblewrap_is_enabled() {
|
||||
fn danger_full_access_is_untagged_even_when_bubblewrap_is_default() {
|
||||
let actual = sandbox_tag(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
assert_eq!(actual, "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_sandbox_keeps_external_tag_when_bubblewrap_is_enabled() {
|
||||
fn external_sandbox_keeps_external_tag_when_bubblewrap_is_default() {
|
||||
let actual = sandbox_tag(
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Enabled,
|
||||
},
|
||||
WindowsSandboxLevel::Disabled,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
assert_eq!(actual, "external");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bubblewrap_feature_sets_distinct_linux_tag() {
|
||||
fn bubblewrap_default_sets_distinct_linux_tag() {
|
||||
let actual = sandbox_tag(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
WindowsSandboxLevel::Disabled,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
let expected = if cfg!(target_os = "linux") {
|
||||
"linux_bubblewrap"
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ pub(crate) struct SandboxTransformRequest<'a> {
|
|||
#[cfg(target_os = "macos")]
|
||||
pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
|
||||
pub codex_linux_sandbox_exe: Option<&'a PathBuf>,
|
||||
pub use_linux_sandbox_bwrap: bool,
|
||||
pub use_legacy_landlock: bool,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
}
|
||||
|
||||
|
|
@ -571,7 +571,7 @@ impl SandboxManager {
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
codex_linux_sandbox_exe,
|
||||
use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
} = request;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
|
@ -653,7 +653,7 @@ impl SandboxManager {
|
|||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
sandbox_policy_cwd,
|
||||
use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
|
|
@ -886,7 +886,7 @@ mod tests {
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.expect("transform");
|
||||
|
|
@ -1219,7 +1219,7 @@ mod tests {
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.expect("transform");
|
||||
|
|
@ -1291,7 +1291,7 @@ mod tests {
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.expect("transform");
|
||||
|
|
|
|||
|
|
@ -884,9 +884,7 @@ impl JsReplManager {
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(),
|
||||
use_linux_sandbox_bwrap: turn
|
||||
.features
|
||||
.enabled(crate::features::Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: turn.features.use_legacy_landlock(),
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
})
|
||||
.map_err(|err| format!("failed to configure sandbox for js_repl: {err}"))?;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ caching).
|
|||
use crate::error::CodexErr;
|
||||
use crate::error::SandboxErr;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::features::Feature;
|
||||
use crate::guardian::GUARDIAN_REJECTION_MESSAGE;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::network_policy_decision::network_approval_context_from_payload;
|
||||
|
|
@ -186,7 +185,7 @@ impl ToolOrchestrator {
|
|||
|
||||
// Platform-specific flag gating is handled by SandboxManager::select_initial
|
||||
// via crate::safety::get_platform_sandbox(..).
|
||||
let use_linux_sandbox_bwrap = turn_ctx.features.enabled(Feature::UseLinuxSandboxBwrap);
|
||||
let use_legacy_landlock = turn_ctx.features.use_legacy_landlock();
|
||||
let initial_attempt = SandboxAttempt {
|
||||
sandbox: initial_sandbox,
|
||||
policy: &turn_ctx.sandbox_policy,
|
||||
|
|
@ -196,7 +195,7 @@ impl ToolOrchestrator {
|
|||
manager: &self.sandbox,
|
||||
sandbox_cwd: &turn_ctx.cwd,
|
||||
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
|
||||
use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level: turn_ctx.windows_sandbox_level,
|
||||
};
|
||||
|
||||
|
|
@ -318,7 +317,7 @@ impl ToolOrchestrator {
|
|||
manager: &self.sandbox,
|
||||
sandbox_cwd: &turn_ctx.cwd,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level: turn_ctx.windows_sandbox_level,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use std::time::Duration;
|
|||
use std::time::Instant;
|
||||
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::features::Feature;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::memories::usage::emit_metric_for_tool_read;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
|
|
@ -174,10 +173,7 @@ impl ToolRegistry {
|
|||
sandbox_tag(
|
||||
&invocation.turn.sandbox_policy,
|
||||
invocation.turn.windows_sandbox_level,
|
||||
invocation
|
||||
.turn
|
||||
.features
|
||||
.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
invocation.turn.features.use_legacy_landlock(),
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -505,7 +501,7 @@ async fn dispatch_after_tool_use_hook(
|
|||
sandbox: sandbox_tag(
|
||||
&turn.sandbox_policy,
|
||||
turn.windows_sandbox_level,
|
||||
turn.features.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
turn.features.use_legacy_landlock(),
|
||||
)
|
||||
.to_string(),
|
||||
sandbox_policy: sandbox_policy_tag(&turn.sandbox_policy).to_string(),
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ pub(super) async fn try_run_zsh_fork(
|
|||
.macos_seatbelt_profile_extensions
|
||||
.clone(),
|
||||
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
|
||||
use_linux_sandbox_bwrap: ctx.turn.features.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
|
||||
};
|
||||
let main_execve_wrapper_exe = ctx
|
||||
.session
|
||||
|
|
@ -258,7 +258,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
|||
.macos_seatbelt_profile_extensions
|
||||
.clone(),
|
||||
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
|
||||
use_linux_sandbox_bwrap: ctx.turn.features.enabled(Feature::UseLinuxSandboxBwrap),
|
||||
use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
|
||||
};
|
||||
let main_execve_wrapper_exe = ctx
|
||||
.session
|
||||
|
|
@ -855,7 +855,7 @@ struct CoreShellCommandExecutor {
|
|||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
macos_seatbelt_profile_extensions: Option<MacOsSeatbeltProfileExtensions>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
use_linux_sandbox_bwrap: bool,
|
||||
use_legacy_landlock: bool,
|
||||
}
|
||||
|
||||
struct PrepareSandboxedExecParams<'a> {
|
||||
|
|
@ -1052,7 +1052,7 @@ impl CoreShellCommandExecutor {
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(),
|
||||
use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
})?;
|
||||
if let Some(network) = exec_request.network.as_ref() {
|
||||
|
|
|
|||
|
|
@ -611,7 +611,7 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions
|
|||
..Default::default()
|
||||
}),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
use_legacy_landlock: false,
|
||||
};
|
||||
|
||||
let prepared = executor
|
||||
|
|
@ -660,7 +660,7 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
|
|||
sandbox_policy_cwd: cwd.to_path_buf(),
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
use_legacy_landlock: false,
|
||||
};
|
||||
|
||||
let permissions = Permissions {
|
||||
|
|
@ -737,7 +737,7 @@ async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_mac
|
|||
..Default::default()
|
||||
}),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
use_legacy_landlock: false,
|
||||
};
|
||||
|
||||
let prepared = executor
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ pub(crate) struct SandboxAttempt<'a> {
|
|||
pub(crate) manager: &'a SandboxManager,
|
||||
pub(crate) sandbox_cwd: &'a Path,
|
||||
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
|
||||
pub use_linux_sandbox_bwrap: bool,
|
||||
pub use_legacy_landlock: bool,
|
||||
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||
}
|
||||
|
||||
|
|
@ -353,7 +353,7 @@ impl<'a> SandboxAttempt<'a> {
|
|||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe,
|
||||
use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap,
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,16 +132,11 @@ impl TurnMetadataState {
|
|||
cwd: PathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
use_linux_sandbox_bwrap: bool,
|
||||
use_legacy_landlock: bool,
|
||||
) -> Self {
|
||||
let repo_root = get_git_repo_root(&cwd).map(|root| root.to_string_lossy().into_owned());
|
||||
let sandbox = Some(
|
||||
sandbox_tag(
|
||||
sandbox_policy,
|
||||
windows_sandbox_level,
|
||||
use_linux_sandbox_bwrap,
|
||||
)
|
||||
.to_string(),
|
||||
sandbox_tag(sandbox_policy, windows_sandbox_level, use_legacy_landlock).to_string(),
|
||||
);
|
||||
let base_metadata = build_turn_metadata_bag(Some(turn_id), sandbox, None, None);
|
||||
let base_header = base_metadata
|
||||
|
|
@ -300,19 +295,19 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_respects_linux_bubblewrap_toggle() {
|
||||
fn turn_metadata_state_respects_legacy_landlock_flag() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().to_path_buf();
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
|
||||
let without_bubblewrap = TurnMetadataState::new(
|
||||
let default_bubblewrap = TurnMetadataState::new(
|
||||
"turn-a".to_string(),
|
||||
cwd.clone(),
|
||||
&sandbox_policy,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
false,
|
||||
);
|
||||
let with_bubblewrap = TurnMetadataState::new(
|
||||
let legacy_landlock = TurnMetadataState::new(
|
||||
"turn-b".to_string(),
|
||||
cwd,
|
||||
&sandbox_policy,
|
||||
|
|
@ -320,30 +315,33 @@ mod tests {
|
|||
true,
|
||||
);
|
||||
|
||||
let without_bubblewrap_header = without_bubblewrap
|
||||
let default_bubblewrap_header = default_bubblewrap
|
||||
.current_header_value()
|
||||
.expect("without_bubblewrap_header");
|
||||
let with_bubblewrap_header = with_bubblewrap
|
||||
.expect("default_bubblewrap_header");
|
||||
let legacy_landlock_header = legacy_landlock
|
||||
.current_header_value()
|
||||
.expect("with_bubblewrap_header");
|
||||
.expect("legacy_landlock_header");
|
||||
|
||||
let without_bubblewrap_json: Value =
|
||||
serde_json::from_str(&without_bubblewrap_header).expect("without_bubblewrap_json");
|
||||
let with_bubblewrap_json: Value =
|
||||
serde_json::from_str(&with_bubblewrap_header).expect("with_bubblewrap_json");
|
||||
let default_bubblewrap_json: Value =
|
||||
serde_json::from_str(&default_bubblewrap_header).expect("default_bubblewrap_json");
|
||||
let legacy_landlock_json: Value =
|
||||
serde_json::from_str(&legacy_landlock_header).expect("legacy_landlock_json");
|
||||
|
||||
let without_bubblewrap_sandbox = without_bubblewrap_json
|
||||
let default_bubblewrap_sandbox = default_bubblewrap_json
|
||||
.get("sandbox")
|
||||
.and_then(Value::as_str);
|
||||
let with_bubblewrap_sandbox = with_bubblewrap_json.get("sandbox").and_then(Value::as_str);
|
||||
let legacy_landlock_sandbox = legacy_landlock_json.get("sandbox").and_then(Value::as_str);
|
||||
|
||||
let expected_with_bubblewrap =
|
||||
sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled, true);
|
||||
assert_eq!(with_bubblewrap_sandbox, Some(expected_with_bubblewrap));
|
||||
let expected_default_bubblewrap =
|
||||
sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled, false);
|
||||
assert_eq!(
|
||||
default_bubblewrap_sandbox,
|
||||
Some(expected_default_bubblewrap)
|
||||
);
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
assert_eq!(with_bubblewrap_sandbox, Some("linux_bubblewrap"));
|
||||
assert_ne!(with_bubblewrap_sandbox, without_bubblewrap_sandbox);
|
||||
assert_eq!(default_bubblewrap_sandbox, Some("linux_bubblewrap"));
|
||||
assert_ne!(default_bubblewrap_sandbox, legacy_landlock_sandbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1321,7 +1321,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
|
|||
expectation: Expectation::FileNotCreated {
|
||||
target: TargetPath::Workspace("ro_never.txt"),
|
||||
message_contains: if cfg!(target_os = "linux") {
|
||||
&["Permission denied"]
|
||||
&["Permission denied|Read-only file system"]
|
||||
} else {
|
||||
&[
|
||||
"Permission denied|Operation not permitted|operation not permitted|\
|
||||
|
|
@ -1468,7 +1468,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
|
|||
expectation: Expectation::FileNotCreated {
|
||||
target: TargetPath::OutsideWorkspace("ww_never.txt"),
|
||||
message_contains: if cfg!(target_os = "linux") {
|
||||
&["Permission denied"]
|
||||
&["Permission denied|Read-only file system"]
|
||||
} else {
|
||||
&[
|
||||
"Permission denied|Operation not permitted|operation not permitted|\
|
||||
|
|
@ -2290,20 +2290,17 @@ allow_local_binding = true
|
|||
test.config.permissions.network.is_some(),
|
||||
"expected managed network proxy config to be present"
|
||||
);
|
||||
let runtime_proxy = test
|
||||
.session_configured
|
||||
test.session_configured
|
||||
.network_proxy
|
||||
.as_ref()
|
||||
.expect("expected runtime managed network proxy addresses");
|
||||
let proxy_addr = runtime_proxy.http_addr.as_str();
|
||||
|
||||
let call_id_first = "allow-network-first";
|
||||
// Use the same urllib-based pattern as the other network integration tests,
|
||||
// but point it at the runtime proxy directly so the blocked host reliably
|
||||
// produces a network approval request without relying on curl.
|
||||
let fetch_command = format!(
|
||||
"python3 -c \"import urllib.request; proxy = urllib.request.ProxyHandler({{'http': 'http://{proxy_addr}'}}); opener = urllib.request.build_opener(proxy); print('OK:' + opener.open('http://codex-network-test.invalid', timeout=30).read().decode(errors='replace'))\""
|
||||
);
|
||||
// Use urllib without overriding proxy settings so managed-network sessions
|
||||
// continue to exercise the env-based proxy routing path under bubblewrap.
|
||||
let fetch_command =
|
||||
"python3 -c \"import urllib.request; opener = urllib.request.build_opener(urllib.request.ProxyHandler()); print('OK:' + opener.open('http://codex-network-test.invalid', timeout=30).read().decode(errors='replace'))\""
|
||||
.to_string();
|
||||
let first_event = shell_event(
|
||||
call_id_first,
|
||||
&fetch_command,
|
||||
|
|
|
|||
|
|
@ -12,29 +12,28 @@ into this binary.
|
|||
|
||||
**Current Behavior**
|
||||
- Legacy Landlock + mount protections remain available as the legacy pipeline.
|
||||
- The bubblewrap pipeline is standardized on the vendored path.
|
||||
- During rollout, the bubblewrap pipeline is gated by the temporary feature
|
||||
flag `use_linux_sandbox_bwrap` (CLI `-c` alias for
|
||||
`features.use_linux_sandbox_bwrap`; legacy remains default when off).
|
||||
- When enabled, the bubblewrap pipeline applies `PR_SET_NO_NEW_PRIVS` and a
|
||||
- The default Linux sandbox pipeline is bubblewrap on the vendored path.
|
||||
- Set `features.use_legacy_landlock = true` (or CLI `-c use_legacy_landlock=true`)
|
||||
to force the legacy Landlock fallback.
|
||||
- When the default bubblewrap pipeline is active, it applies `PR_SET_NO_NEW_PRIVS` and a
|
||||
seccomp network filter in-process.
|
||||
- When enabled, the filesystem is read-only by default via `--ro-bind / /`.
|
||||
- When enabled, writable roots are layered with `--bind <root> <root>`.
|
||||
- When enabled, protected subpaths under writable roots (for example `.git`,
|
||||
- When the default bubblewrap pipeline is active, the filesystem is read-only by default via `--ro-bind / /`.
|
||||
- When the default bubblewrap pipeline is active, writable roots are layered with `--bind <root> <root>`.
|
||||
- When the default bubblewrap pipeline is active, protected subpaths under writable roots (for example `.git`,
|
||||
resolved `gitdir:`, and `.codex`) are re-applied as read-only via `--ro-bind`.
|
||||
- When enabled, symlink-in-path and non-existent protected paths inside
|
||||
- When the default bubblewrap pipeline is active, symlink-in-path and non-existent protected paths inside
|
||||
writable roots are blocked by mounting `/dev/null` on the symlink or first
|
||||
missing component.
|
||||
- When enabled, the helper explicitly isolates the user namespace via
|
||||
- When the default bubblewrap pipeline is active, the helper explicitly isolates the user namespace via
|
||||
`--unshare-user` and the PID namespace via `--unshare-pid`.
|
||||
- When enabled and network is restricted without proxy routing, the helper also
|
||||
- When the default bubblewrap pipeline is active and network is restricted without proxy routing, the helper also
|
||||
isolates the network namespace via `--unshare-net`.
|
||||
- In managed proxy mode, the helper uses `--unshare-net` plus an internal
|
||||
TCP->UDS->TCP routing bridge so tool traffic reaches only configured proxy
|
||||
endpoints.
|
||||
- In managed proxy mode, after the bridge is live, seccomp blocks new
|
||||
AF_UNIX/socketpair creation for the user command.
|
||||
- When enabled, it mounts a fresh `/proc` via `--proc /proc` by default, but
|
||||
- When the default bubblewrap pipeline is active, it mounts a fresh `/proc` via `--proc /proc` by default, but
|
||||
you can skip this in restrictive container environments with `--no-proc`.
|
||||
|
||||
**Notes**
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ use codex_protocol::protocol::SandboxPolicy;
|
|||
/// CLI surface for the Linux sandbox helper.
|
||||
///
|
||||
/// The type name remains `LandlockCommand` for compatibility with existing
|
||||
/// wiring, but the filesystem sandbox now uses bubblewrap.
|
||||
/// wiring, but bubblewrap is now the default filesystem sandbox and Landlock
|
||||
/// is the legacy fallback.
|
||||
pub struct LandlockCommand {
|
||||
/// It is possible that the cwd used in the context of the sandbox policy
|
||||
/// is different from the cwd of the process to spawn.
|
||||
|
|
@ -42,11 +43,11 @@ pub struct LandlockCommand {
|
|||
#[arg(long = "network-sandbox-policy", hide = true)]
|
||||
pub network_sandbox_policy: Option<NetworkSandboxPolicy>,
|
||||
|
||||
/// Opt-in: use the bubblewrap-based Linux sandbox pipeline.
|
||||
/// Opt-in: use the legacy Landlock Linux sandbox fallback.
|
||||
///
|
||||
/// When not set, we fall back to the legacy Landlock + mount pipeline.
|
||||
#[arg(long = "use-bwrap-sandbox", hide = true, default_value_t = false)]
|
||||
pub use_bwrap_sandbox: bool,
|
||||
/// When not set, the helper uses the default bubblewrap pipeline.
|
||||
#[arg(long = "use-legacy-landlock", hide = true, default_value_t = false)]
|
||||
pub use_legacy_landlock: bool,
|
||||
|
||||
/// Internal: apply seccomp and `no_new_privs` in the already-sandboxed
|
||||
/// process, then exec the user command.
|
||||
|
|
@ -92,7 +93,7 @@ pub fn run_main() -> ! {
|
|||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
use_legacy_landlock,
|
||||
apply_seccomp_then_exec,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
|
|
@ -103,7 +104,7 @@ pub fn run_main() -> ! {
|
|||
if command.is_empty() {
|
||||
panic!("No command specified to execute.");
|
||||
}
|
||||
ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_bwrap_sandbox);
|
||||
ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_legacy_landlock);
|
||||
let EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
|
|
@ -154,7 +155,7 @@ pub fn run_main() -> ! {
|
|||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
if use_bwrap_sandbox {
|
||||
if !use_legacy_landlock {
|
||||
// Outer stage: bubblewrap first, then re-enter this binary in the
|
||||
// sandboxed environment to apply seccomp. This path never falls back
|
||||
// to legacy Landlock on failure.
|
||||
|
|
@ -171,7 +172,6 @@ pub fn run_main() -> ! {
|
|||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
command,
|
||||
|
|
@ -256,9 +256,9 @@ fn resolve_sandbox_policies(
|
|||
}
|
||||
}
|
||||
|
||||
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_sandbox: bool) {
|
||||
if apply_seccomp_then_exec && !use_bwrap_sandbox {
|
||||
panic!("--apply-seccomp-then-exec requires --use-bwrap-sandbox");
|
||||
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_legacy_landlock: bool) {
|
||||
if apply_seccomp_then_exec && use_legacy_landlock {
|
||||
panic!("--apply-seccomp-then-exec is incompatible with --use-legacy-landlock");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +280,8 @@ fn run_bwrap_with_proc_fallback(
|
|||
network_mode,
|
||||
)
|
||||
{
|
||||
eprintln!("codex-linux-sandbox: bwrap could not mount /proc; retrying with --no-proc");
|
||||
// Keep the retry silent so sandbox-internal diagnostics do not leak into the
|
||||
// child process stderr stream.
|
||||
mount_proc = false;
|
||||
}
|
||||
|
||||
|
|
@ -470,7 +471,6 @@ struct InnerSeccompCommandArgs<'a> {
|
|||
sandbox_policy: &'a SandboxPolicy,
|
||||
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
use_bwrap_sandbox: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_route_spec: Option<String>,
|
||||
command: Vec<String>,
|
||||
|
|
@ -483,7 +483,6 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
|||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
command,
|
||||
|
|
@ -516,10 +515,7 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
|||
"--network-sandbox-policy".to_string(),
|
||||
network_policy_json,
|
||||
];
|
||||
if use_bwrap_sandbox {
|
||||
inner.push("--use-bwrap-sandbox".to_string());
|
||||
inner.push("--apply-seccomp-then-exec".to_string());
|
||||
}
|
||||
inner.push("--apply-seccomp-then-exec".to_string());
|
||||
if allow_network_for_proxy {
|
||||
inner.push("--allow-network-for-proxy".to_string());
|
||||
let proxy_route_spec = proxy_route_spec
|
||||
|
|
|
|||
|
|
@ -127,7 +127,6 @@ fn managed_proxy_inner_command_includes_route_spec() {
|
|||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
use_bwrap_sandbox: true,
|
||||
allow_network_for_proxy: true,
|
||||
proxy_route_spec: Some("{\"routes\":[]}".to_string()),
|
||||
command: vec!["/bin/true".to_string()],
|
||||
|
|
@ -145,7 +144,6 @@ fn inner_command_includes_split_policy_flags() {
|
|||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
use_bwrap_sandbox: true,
|
||||
allow_network_for_proxy: false,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
|
|
@ -163,7 +161,6 @@ fn non_managed_inner_command_omits_route_spec() {
|
|||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
use_bwrap_sandbox: true,
|
||||
allow_network_for_proxy: false,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
|
|
@ -181,7 +178,6 @@ fn managed_proxy_inner_command_requires_route_spec() {
|
|||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
use_bwrap_sandbox: true,
|
||||
allow_network_for_proxy: true,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
|
|
@ -244,8 +240,8 @@ fn resolve_sandbox_policies_rejects_partial_split_policies() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn apply_seccomp_then_exec_without_bwrap_panics() {
|
||||
let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, false));
|
||||
fn apply_seccomp_then_exec_with_legacy_landlock_panics() {
|
||||
let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, true));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
|
|
@ -253,5 +249,5 @@ fn apply_seccomp_then_exec_without_bwrap_panics() {
|
|||
fn valid_inner_stage_modes_do_not_panic() {
|
||||
ensure_inner_stage_mode_is_valid(false, false);
|
||||
ensure_inner_stage_mode_is_valid(false, true);
|
||||
ensure_inner_stage_mode_is_valid(true, true);
|
||||
ensure_inner_stage_mode_is_valid(true, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ async fn run_cmd_result_with_writable_roots(
|
|||
cmd: &[&str],
|
||||
writable_roots: &[PathBuf],
|
||||
timeout_ms: u64,
|
||||
use_bwrap_sandbox: bool,
|
||||
use_legacy_landlock: bool,
|
||||
network_access: bool,
|
||||
) -> Result<codex_core::exec::ExecToolCallOutput> {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
|
|
@ -96,7 +96,7 @@ async fn run_cmd_result_with_writable_roots(
|
|||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
timeout_ms,
|
||||
use_bwrap_sandbox,
|
||||
use_legacy_landlock,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ async fn run_cmd_result_with_policies(
|
|||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
timeout_ms: u64,
|
||||
use_bwrap_sandbox: bool,
|
||||
use_legacy_landlock: bool,
|
||||
) -> Result<codex_core::exec::ExecToolCallOutput> {
|
||||
let cwd = std::env::current_dir().expect("cwd should exist");
|
||||
let sandbox_cwd = cwd.clone();
|
||||
|
|
@ -133,7 +133,7 @@ async fn run_cmd_result_with_policies(
|
|||
network_sandbox_policy,
|
||||
sandbox_cwd.as_path(),
|
||||
&codex_linux_sandbox_exe,
|
||||
use_bwrap_sandbox,
|
||||
use_legacy_landlock,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
|
@ -155,7 +155,7 @@ async fn should_skip_bwrap_tests() -> bool {
|
|||
&["bash", "-lc", "true"],
|
||||
&[],
|
||||
NETWORK_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
|
|
@ -216,7 +216,7 @@ async fn test_dev_null_write() {
|
|||
// We have seen timeouts when running this test in CI on GitHub,
|
||||
// so we are using a generous timeout until we can diagnose further.
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
|
|
@ -240,7 +240,7 @@ async fn bwrap_populates_minimal_dev_nodes() {
|
|||
],
|
||||
&[],
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
|
|
@ -278,7 +278,7 @@ async fn bwrap_preserves_writable_dev_shm_bind_mount() {
|
|||
],
|
||||
&[PathBuf::from("/dev/shm")],
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
|
|
@ -442,7 +442,7 @@ async fn sandbox_blocks_git_and_codex_writes_inside_writable_root() {
|
|||
],
|
||||
&[tmpdir.path().to_path_buf()],
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
|
|
@ -458,7 +458,7 @@ async fn sandbox_blocks_git_and_codex_writes_inside_writable_root() {
|
|||
],
|
||||
&[tmpdir.path().to_path_buf()],
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
|
|
@ -495,7 +495,7 @@ async fn sandbox_blocks_codex_symlink_replacement_attack() {
|
|||
],
|
||||
&[tmpdir.path().to_path_buf()],
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
|
|
@ -548,7 +548,7 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() {
|
|||
file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await,
|
||||
"explicit split-policy carveout should be denied under bubblewrap",
|
||||
|
|
@ -599,7 +599,7 @@ async fn sandbox_blocks_root_read_carveouts_under_bwrap() {
|
|||
file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
LONG_TIMEOUT_MS,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await,
|
||||
"root-read carveout should be denied under bubblewrap",
|
||||
|
|
|
|||
|
|
@ -133,7 +133,6 @@ async fn run_linux_sandbox_direct(
|
|||
cwd.to_string_lossy().to_string(),
|
||||
"--sandbox-policy".to_string(),
|
||||
policy_json,
|
||||
"--use-bwrap-sandbox".to_string(),
|
||||
];
|
||||
if allow_network_for_proxy {
|
||||
args.push("--allow-network-for-proxy".to_string());
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ impl CliConfigOverrides {
|
|||
}
|
||||
|
||||
fn canonicalize_override_key(key: &str) -> String {
|
||||
if key == "use_linux_sandbox_bwrap" {
|
||||
"features.use_linux_sandbox_bwrap".to_string()
|
||||
if key == "use_legacy_landlock" {
|
||||
"features.use_legacy_landlock".to_string()
|
||||
} else {
|
||||
key.to_string()
|
||||
}
|
||||
|
|
@ -181,12 +181,12 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalizes_use_linux_sandbox_bwrap_alias() {
|
||||
fn canonicalizes_use_legacy_landlock_alias() {
|
||||
let overrides = CliConfigOverrides {
|
||||
raw_overrides: vec!["use_linux_sandbox_bwrap=true".to_string()],
|
||||
raw_overrides: vec!["use_legacy_landlock=true".to_string()],
|
||||
};
|
||||
let parsed = overrides.parse_overrides().expect("parse_overrides");
|
||||
assert_eq!(parsed[0].0.as_str(), "features.use_linux_sandbox_bwrap");
|
||||
assert_eq!(parsed[0].0.as_str(), "features.use_legacy_landlock");
|
||||
assert_eq!(parsed[0].1.as_bool(), Some(true));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue