Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions crates/domain/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,12 @@ impl WorkspaceExecutionInfo {
}
}

/// Overlay after a successful Linux helper probe. Restricted network
/// advertises OS enforcement; Enabled stays unenforced in this WP.
pub fn with_linux_command_sandbox(mut self, network_restricted: bool) -> Self {
/// Overlay after a successful Linux helper probe. Restricted deny and
/// Enabled managed-proxy both advertise OS enforcement.
pub fn with_linux_command_sandbox(mut self) -> Self {
self.isolation.command_sandbox = CommandSandboxState::LinuxSandbox;
if network_restricted {
self.network.enforcement = NetworkEnforcementState::Enforced;
self.network.client_may_escalate = false;
}
self.network.enforcement = NetworkEnforcementState::Enforced;
self.network.client_may_escalate = false;
self
}
}
Expand Down Expand Up @@ -335,7 +333,7 @@ mod tests {
exec: true,
},
)
.with_linux_command_sandbox(true);
.with_linux_command_sandbox();
assert_eq!(
exec.isolation.command_sandbox,
CommandSandboxState::LinuxSandbox
Expand All @@ -347,6 +345,23 @@ mod tests {
assert_eq!(json["network"]["enforcement"], "enforced");
}

#[test]
fn linux_sandbox_overlay_enforces_enabled_network() {
let exec = WorkspaceExecutionInfo::from_effective(
environment(ClientEnvironmentKind::Host, true, true, true),
EffectivePermissionInfo {
read: true,
write: true,
exec: true,
},
NetworkPolicyState::Enabled,
)
.with_linux_command_sandbox();
assert_eq!(exec.network.policy, NetworkPolicyState::Enabled);
assert_eq!(exec.network.enforcement, NetworkEnforcementState::Enforced);
assert!(!exec.network.client_may_escalate);
}

#[test]
fn host_read_only_denies_write_and_exec() {
let exec = compose(
Expand Down
17 changes: 15 additions & 2 deletions crates/linux-sandbox-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ use serde::{Deserialize, Serialize};
/// JSON request/response version for helper `prepare`. Not UDS.
pub const SANDBOX_HELPER_PROTOCOL: u32 = 1;

/// This work package only hard-denies network. `Enabled` / proxy is later.
/// Restricted is isolated netns + Restricted seccomp. Enabled is isolated
/// netns plus the helper-owned managed proxy (`--allow-network-for-proxy`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SandboxNetwork {
Restricted,
Enabled,
}

/// Stdin JSON for `codespace-linux-sandbox prepare`.
Expand Down Expand Up @@ -60,6 +62,17 @@ mod tests {
serde_json::from_value::<SandboxPrepareRequest>(json).unwrap(),
req
);

let enabled = SandboxPrepareRequest {
network: SandboxNetwork::Enabled,
..req.clone()
};
let json = serde_json::to_value(&enabled).unwrap();
assert_eq!(json["network"], "enabled");
assert_eq!(
serde_json::from_value::<SandboxPrepareRequest>(json).unwrap(),
enabled
);
}

#[test]
Expand Down Expand Up @@ -87,7 +100,7 @@ mod tests {
"workspace_root": "/",
"command_cwd": "/",
"writable_workspace": false,
"network": "enabled",
"network": "open",
"argv": ["/bin/true"]
});
assert!(serde_json::from_value::<SandboxPrepareRequest>(json).is_err());
Expand Down
2 changes: 2 additions & 0 deletions crates/linux-sandbox/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/linux-sandbox/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ resolver = "2"
[dependencies]
codespace-linux-sandbox-protocol = { path = "../linux-sandbox-protocol" }
codex-linux-sandbox = { path = "../../third_party/codex/codex-rs/linux-sandbox" }
codex-network-proxy = { path = "../../third_party/codex/codex-rs/network-proxy" }
codex-protocol = { path = "../../third_party/codex/codex-rs/protocol" }
codex-sandboxing = { path = "../../third_party/codex/codex-rs/sandboxing" }
codex-utils-path-uri = { path = "../../third_party/codex/codex-rs/utils/path-uri" }
libc = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time", "io-util", "sync"] }
# Resolver guards. Codex pin 6b9826e is validated against the Rama
# 0.3.0-alpha.4 train. Leaf crates use ^0.3.0-alpha.4, so a fresh
# resolve can pick stable 0.3.0 and break OpaqueError. These keys are
Expand Down
9 changes: 5 additions & 4 deletions crates/linux-sandbox/src/codex.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Linux `codex_linux_sandbox::run_main`. Non-Linux compiles this module
//! but must not call the upstream entry (it panics). After `run --plan`,
//! this process `exec`s itself with Codex argv so the managed PID is
//! unchanged.
//! but must not call the upstream entry (it panics). Restricted
//! `run --plan` `exec`s this process with Codex argv so the managed PID
//! is unchanged. Enabled spawn+waits instead so `NetworkProxy` can live.

use std::path::PathBuf;

Expand All @@ -18,7 +18,8 @@ pub fn run_main() {
}

/// Replace this process with the same binary and `argv` (Codex flags).
/// Does not spawn an extra child. Inherits the env the runner applied.
/// Restricted `run --plan` uses this so the managed PID stays the helper.
/// Enabled starts `NetworkProxy` in the helper and spawn+waits instead.
pub fn exec_self(argv: &[String]) -> ! {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("codespace-linux-sandbox"));
#[cfg(unix)]
Expand Down
9 changes: 8 additions & 1 deletion crates/linux-sandbox/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod codex;
mod plan;
mod prepare;
mod probe;
mod proxy;

fn main() {
let mut args = std::env::args();
Expand All @@ -30,7 +31,13 @@ fn run_plan(args: &[String]) {
}
};
match plan::load_and_unlink(&plan_path) {
Ok(argv) => codex::exec_self(&argv),
Ok(argv) => {
if argv.iter().any(|arg| arg == "--allow-network-for-proxy") {
proxy::run_codex_with_proxy(&argv);
} else {
codex::exec_self(&argv);
}
}
Err(message) => {
eprintln!("{message}");
std::process::exit(1);
Expand Down
44 changes: 40 additions & 4 deletions crates/linux-sandbox/src/prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,27 @@ pub(crate) fn linux_sandbox_args(
&profile,
&policy_cwd,
/*use_legacy_landlock*/ false,
/*allow_network_for_proxy*/ false,
matches!(req.network, SandboxNetwork::Enabled),
)
}))
.map_err(|_| "failed to build linux sandbox argv".to_string())?;
if args.iter().any(|arg| {
arg == "--allow-network-for-proxy"
|| arg == "--proxy-route-spec"
arg == "--proxy-route-spec"
|| arg == "--not-a-security-boundary"
|| arg == "--use-legacy-landlock"
}) {
return Err("linux sandbox argv included a forbidden helper flag".into());
}
let has_proxy_flag = args.iter().any(|arg| arg == "--allow-network-for-proxy");
match req.network {
SandboxNetwork::Restricted if has_proxy_flag => {
return Err("restricted network plan must not enable the managed proxy".into());
}
SandboxNetwork::Enabled if !has_proxy_flag => {
return Err("enabled network plan must include --allow-network-for-proxy".into());
}
SandboxNetwork::Restricted | SandboxNetwork::Enabled => {}
}
Ok(args)
}

Expand Down Expand Up @@ -136,6 +145,7 @@ fn permission_profile(
let file_system = FileSystemSandboxPolicy::restricted(entries);
let network = match req.network {
SandboxNetwork::Restricted => NetworkSandboxPolicy::Restricted,
SandboxNetwork::Enabled => NetworkSandboxPolicy::Enabled,
};
Ok(PermissionProfile::from_runtime_permissions(
&file_system,
Expand Down Expand Up @@ -168,12 +178,16 @@ mod tests {
use serde_json::Value;

fn req(root: &Path, writable: bool) -> SandboxPrepareRequest {
req_network(root, writable, SandboxNetwork::Restricted)
}

fn req_network(root: &Path, writable: bool, network: SandboxNetwork) -> SandboxPrepareRequest {
SandboxPrepareRequest {
protocol: SANDBOX_HELPER_PROTOCOL,
workspace_root: root.to_string_lossy().into_owned(),
command_cwd: root.to_string_lossy().into_owned(),
writable_workspace: writable,
network: SandboxNetwork::Restricted,
network,
argv: vec!["/bin/echo".into(), "hi".into()],
}
}
Expand All @@ -186,6 +200,10 @@ mod tests {
linux_sandbox_args(&req(root, writable), &dummy_helper()).expect("prepare")
}

fn launch_args_network(root: &Path, network: SandboxNetwork) -> Vec<String> {
linux_sandbox_args(&req_network(root, true, network), &dummy_helper()).expect("prepare")
}

fn profile_json(args: &[String]) -> Value {
let idx = args
.iter()
Expand Down Expand Up @@ -247,6 +265,24 @@ mod tests {
);
}

#[test]
fn enabled_plan_has_allow_network_for_proxy_only() {
let dir = tempfile::tempdir().unwrap();
let args = launch_args_network(dir.path(), SandboxNetwork::Enabled);
assert!(
args.iter().any(|arg| arg == "--allow-network-for-proxy"),
"Enabled must request the loopback proxy bridge: {args:?}"
);
assert!(
!args.iter().any(|arg| arg == "--proxy-route-spec"
|| arg == "--not-a-security-boundary"
|| arg == "--use-legacy-landlock"),
"proxy route spec is attached at helper run time, not in the plan: {args:?}"
);
let profile = profile_json(&args);
assert_eq!(profile["network"], "enabled");
}

#[test]
fn permission_profile_reads_helper_for_inner_reexec() {
let dir = tempfile::tempdir().unwrap();
Expand Down
Loading
Loading