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
2 changes: 2 additions & 0 deletions codex-rs/Cargo.lock

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

386 changes: 9 additions & 377 deletions codex-rs/core/src/exec.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion codex-rs/core/src/sandboxing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ ExecRequest for execution.
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::StdoutStream;
use crate::exec::WindowsSandboxFilesystemOverrides;
use crate::exec::execute_exec_request;
#[cfg(target_os = "macos")]
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
Expand All @@ -24,6 +23,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;

Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core/src/session/turn_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ impl TurnContext {
FileSystemSandboxContext {
permissions,
cwd: Some(cwd.clone()),
workspace_roots: self
.config
.effective_workspace_roots()
.iter()
.map(PathUri::from_abs_path)
.collect(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self
.config
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core/src/tools/runtimes/apply_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::policy_transforms::effective_permission_profile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use futures::future::BoxFuture;
use std::path::PathBuf;
use std::time::Instant;
Expand Down Expand Up @@ -99,6 +100,11 @@ impl ApplyPatchRuntime {
Some(FileSystemSandboxContext {
permissions,
cwd: Some(attempt.sandbox_cwd.clone()),
workspace_roots: attempt
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
windows_sandbox_level: attempt.windows_sandbox_level,
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
use_legacy_landlock: attempt.use_legacy_landlock,
Expand Down
63 changes: 46 additions & 17 deletions codex-rs/exec-server/src/fs_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_sandboxing::SandboxCommand;
use codex_sandboxing::SandboxDirectSpawnTransformRequest;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
Expand Down Expand Up @@ -84,15 +85,15 @@ impl FileSystemSandboxRunner {
&file_system_policy,
network_policy,
);
let command = self.sandbox_exec_request(&permission_profile, &cwd.uri, sandbox)?;
let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox)?;
Comment thread
iceweasel-oai marked this conversation as resolved.
let request_json = serde_json::to_vec(&request).map_err(json_error)?;
run_command(command, request_json).await
}

fn sandbox_exec_request(
&self,
permission_profile: &PermissionProfile,
cwd: &PathUri,
cwd: &SandboxCwd,
sandbox_context: &FileSystemSandboxContext,
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
let helper = &self.runtime_paths.codex_self_exe;
Expand All @@ -108,22 +109,36 @@ impl FileSystemSandboxRunner {
let command = SandboxCommand {
program: helper.as_path().as_os_str().to_owned(),
args: vec![CODEX_FS_HELPER_ARG1.to_string()],
cwd: cwd.clone(),
cwd: cwd.uri.clone(),
env: self.helper_env.clone(),
additional_permissions: None,
};
let native_workspace_roots = sandbox_context
.workspace_roots
.iter()
.map(native_workspace_root)
.collect::<Result<Vec<_>, _>>()?;
let workspace_roots = if native_workspace_roots.is_empty() {
std::slice::from_ref(&cwd.native)
} else {
native_workspace_roots.as_slice()
};
sandbox_manager
.transform(SandboxTransformRequest {
command,
permissions: permission_profile,
sandbox,
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: cwd,
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
.transform_for_direct_spawn(SandboxDirectSpawnTransformRequest {
workspace_roots,
transform: SandboxTransformRequest {
command,
permissions: permission_profile,
sandbox,
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: &cwd.uri,
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_private_desktop: sandbox_context
.windows_sandbox_private_desktop,
},
})
.map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))
}
Expand Down Expand Up @@ -157,6 +172,14 @@ fn native_sandbox_cwd(cwd: &PathUri) -> Result<AbsolutePathBuf, JSONRPCErrorErro
})
}

fn native_workspace_root(root: &PathUri) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
root.to_abs_path().map_err(|err| {
invalid_request(format!(
"file system sandbox workspace root is not native to this exec-server host: {err}"
))
})
}

fn helper_read_roots(runtime_paths: &ExecServerRuntimePaths) -> Vec<AbsolutePathBuf> {
let mut roots = Vec::new();
for path in std::iter::once(runtime_paths.codex_self_exe.as_path())
Expand Down Expand Up @@ -516,15 +539,21 @@ mod tests {
let runner = FileSystemSandboxRunner::new(runtime_paths);
let native_cwd = AbsolutePathBuf::current_dir().expect("cwd");
let cwd = PathUri::from_abs_path(&native_cwd);
let file_system_policy =
restricted_policy(vec![path_entry(native_cwd, FileSystemAccessMode::Write)]);
let file_system_policy = restricted_policy(vec![path_entry(
native_cwd.clone(),
FileSystemAccessMode::Write,
)]);
let network_policy = NetworkSandboxPolicy::Restricted;
let permission_profile =
PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy);
let sandbox_context = sandbox_context_with_cwd(&file_system_policy, cwd.clone());
let sandbox_cwd = SandboxCwd {
uri: cwd,
native: native_cwd,
};

let request = runner
.sandbox_exec_request(&permission_profile, &cwd, &sandbox_context)
.sandbox_exec_request(&permission_profile, &sandbox_cwd, &sandbox_context)
.expect("sandbox exec request");

assert_eq!(request.env.get(&path_key), Some(&path));
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/exec-server/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub(crate) mod exec_server;
pub(crate) const DELAYED_OUTPUT_AFTER_EXIT_PARENT_ARG: &str =
"--codex-test-delayed-output-after-exit-parent";

const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox";
const DELAYED_OUTPUT_AFTER_EXIT_CHILD_ARG: &str = "--codex-test-delayed-output-after-exit-child";

#[ctor]
Expand All @@ -27,6 +28,9 @@ pub static TEST_BINARY_DISPATCH_GUARD: Option<TestBinaryDispatchGuard> = {
if argv1 == Some(CODEX_FS_HELPER_ARG1) {
return TestBinaryDispatchMode::DispatchArg0Only;
}
if argv1 == Some(CODEX_WINDOWS_SANDBOX_ARG1) {
return TestBinaryDispatchMode::DispatchArg0Only;
}
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
return TestBinaryDispatchMode::DispatchArg0Only;
}
Expand Down
60 changes: 60 additions & 0 deletions codex-rs/exec-server/tests/file_system_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ use std::path::Path;
use std::process::Command;

use anyhow::Result;
use codex_exec_server::FileSystemSandboxContext;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_path_uri::PathUri;
use test_case::test_case;

use crate::support::FileSystemImplementation;
use crate::support::create_file_system_context;

fn create_directory_junction(target: &Path, alias: &Path) -> Result<()> {
let output = Command::new("cmd")
Expand Down Expand Up @@ -53,3 +58,58 @@ async fn file_system_sandboxed_canonicalize_resolves_directory_junction(
)
.await
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy() -> Result<()> {
let context = create_file_system_context(FileSystemImplementation::Remote).await?;
let file_system = context.file_system;
let tmp = tempfile::TempDir::new()?;
let readonly_dir = tmp.path().join("readonly");
std::fs::create_dir_all(&readonly_dir)?;

let mut sandbox = read_only_sandbox_for_cwd(readonly_dir.clone())?;
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;

let readable_file = readonly_dir.join("readable.txt");
std::fs::write(&readable_file, b"readable")?;
let read_result = file_system
.read_file(&PathUri::from_path(&readable_file)?, Some(&sandbox))
.await;
// Some local Windows hosts cannot create restricted tokens. Reaching that
// error still proves the remote fs helper went through the Windows sandbox
// launcher; before the wrapper fix this read would have run unsandboxed.
if is_unsupported_restricted_token_host(&read_result) {
return Ok(());
}
assert_eq!(read_result?, b"readable");

let blocked_file = readonly_dir.join("blocked.txt");
let error = file_system
.write_file(
&PathUri::from_path(&blocked_file)?,
b"blocked".to_vec(),
Some(&sandbox),
)
.await
.expect_err("write outside the sandbox should fail");
assert!(
!blocked_file.exists(),
"sandboxed fs helper must not create blocked file after error: {error}"
);

Ok(())
}

fn read_only_sandbox_for_cwd(cwd: std::path::PathBuf) -> Result<FileSystemSandboxContext> {
Ok(FileSystemSandboxContext::from_legacy_sandbox_policy(
SandboxPolicy::new_read_only_policy(),
PathUri::from_path(cwd)?,
)?)
}

fn is_unsupported_restricted_token_host<T>(result: &std::io::Result<T>) -> bool {
result.as_ref().err().is_some_and(|err| {
err.to_string()
.contains("windows sandbox failed: CreateRestrictedToken failed: 87")
})
}
4 changes: 4 additions & 0 deletions codex-rs/file-system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub struct FileSystemSandboxContext {
pub permissions: PermissionProfile,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<PathUri>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_roots: Vec<PathUri>,
pub windows_sandbox_level: WindowsSandboxLevel,
#[serde(default)]
pub windows_sandbox_private_desktop: bool,
Expand Down Expand Up @@ -93,6 +95,7 @@ impl FileSystemSandboxContext {
Self {
permissions,
cwd,
workspace_roots: Vec::new(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
use_legacy_landlock: false,
Expand All @@ -113,6 +116,7 @@ impl FileSystemSandboxContext {
pub fn drop_cwd_if_unused(mut self) -> Self {
if !self.has_cwd_dependent_permissions() {
self.cwd = None;
self.workspace_roots.clear();
}
self
}
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/sandboxing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ codex-network-proxy = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-windows-sandbox = { workspace = true }
dunce = { workspace = true }
libc = { workspace = true }
serde_json = { workspace = true }
Expand All @@ -25,6 +26,9 @@ tracing = { workspace = true, features = ["log"] }
url = { workspace = true }
which = { workspace = true }

[target.'cfg(windows)'.dependencies]
codex-utils-home-dir = { workspace = true }

[dev-dependencies]
anyhow = { workspace = true }
pretty_assertions = { workspace = true }
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/sandboxing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ mod manager;
pub mod policy_transforms;
#[cfg(target_os = "macos")]
pub mod seatbelt;
mod windows;

#[cfg(target_os = "linux")]
pub use bwrap::find_system_bwrap_in_path;
#[cfg(target_os = "linux")]
pub use bwrap::system_bwrap_warning;
pub use manager::SandboxCommand;
pub use manager::SandboxDirectSpawnTransformRequest;
pub use manager::SandboxExecRequest;
pub use manager::SandboxManager;
pub use manager::SandboxTransformError;
Expand All @@ -20,6 +22,12 @@ pub use manager::SandboxablePreference;
pub use manager::compatibility_sandbox_policy_for_permission_profile;
pub use manager::get_platform_sandbox;
pub use manager::with_managed_mitm_ca_readable_root;
pub use windows::WindowsSandboxFilesystemOverrides;
pub use windows::permission_profile_supports_windows_restricted_token_sandbox;
pub use windows::resolve_windows_elevated_filesystem_overrides;
pub use windows::resolve_windows_restricted_token_filesystem_overrides;
pub use windows::unsupported_windows_restricted_token_sandbox_reason;
pub use windows::windows_sandbox_uses_elevated_backend;

use codex_protocol::error::CodexErr;

Expand Down Expand Up @@ -48,6 +56,10 @@ impl From<SandboxTransformError> for CodexErr {
SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation(
"seatbelt sandbox is only available on macOS".to_string(),
),
#[cfg(target_os = "windows")]
SandboxTransformError::WindowsSandboxPreparation(message) => {
CodexErr::UnsupportedOperation(message)
}
}
}
}
Loading
Loading