mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Run fs helper through Windows sandbox wrapper
This commit is contained in:
Generated
+3
@@ -2846,8 +2846,11 @@ dependencies = [
|
||||
"codex-shell-command",
|
||||
"codex-test-binary-support",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-home-dir",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-rustls-provider",
|
||||
"codex-windows-sandbox",
|
||||
"ctor 0.6.3",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
|
||||
@@ -49,9 +49,14 @@ tokio-tungstenite = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
codex-windows-sandbox = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-test-binary-support = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
ctor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -59,11 +59,15 @@ impl FileSystemSandboxRunner {
|
||||
request: FsHelperRequest,
|
||||
) -> Result<FsHelperPayload, JSONRPCErrorError> {
|
||||
let cwd = sandbox_cwd(sandbox)?;
|
||||
let helper = self.helper_exe_for_launch()?;
|
||||
let mut file_system_policy = sandbox.permissions.file_system_sandbox_policy();
|
||||
// The sandboxed child re-enters Codex as the filesystem helper. On Windows
|
||||
// that executable can be a materialized copy under CODEX_HOME/.sandbox-bin,
|
||||
// so derive helper read roots from the exact helper path we will launch.
|
||||
let helper_read_roots = if sandbox.use_legacy_landlock {
|
||||
Vec::new()
|
||||
} else {
|
||||
helper_read_roots(&self.runtime_paths)
|
||||
helper_read_roots(&helper, self.runtime_paths.codex_linux_sandbox_exe.as_ref())
|
||||
};
|
||||
add_helper_runtime_permissions(&mut file_system_policy, &helper_read_roots, cwd.as_path());
|
||||
normalize_file_system_policy_root_aliases(&mut file_system_policy);
|
||||
@@ -73,18 +77,48 @@ impl FileSystemSandboxRunner {
|
||||
&file_system_policy,
|
||||
network_policy,
|
||||
);
|
||||
let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox)?;
|
||||
let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox, &helper)?;
|
||||
let request_json = serde_json::to_vec(&request).map_err(json_error)?;
|
||||
run_command(command, request_json).await
|
||||
}
|
||||
|
||||
fn helper_exe_for_launch(&self) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows sandbox launch grants are prepared around the executable
|
||||
// path that the sandbox will spawn. When exec-server is embedded or
|
||||
// hosted, `current_exe()` can point at the host process instead of
|
||||
// the configured Codex binary, so materialize the runtime-provided
|
||||
// helper and use that exact path for both launch and read roots.
|
||||
let codex_home = codex_utils_home_dir::find_codex_home().map_err(|err| {
|
||||
internal_error(format!(
|
||||
"windows fs sandbox helper failed to resolve CODEX_HOME: {err}"
|
||||
))
|
||||
})?;
|
||||
let helper = codex_windows_sandbox::resolve_exe_for_launch(
|
||||
self.runtime_paths.codex_self_exe.as_path(),
|
||||
codex_home.as_path(),
|
||||
);
|
||||
AbsolutePathBuf::from_absolute_path(helper.as_path()).map_err(|err| {
|
||||
internal_error(format!(
|
||||
"windows fs sandbox helper path is not absolute: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Ok(self.runtime_paths.codex_self_exe.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_exec_request(
|
||||
&self,
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &AbsolutePathBuf,
|
||||
sandbox_context: &FileSystemSandboxContext,
|
||||
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
|
||||
let helper = &self.runtime_paths.codex_self_exe;
|
||||
helper: &AbsolutePathBuf,
|
||||
) -> Result<PreparedFsHelperCommand, JSONRPCErrorError> {
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
@@ -101,7 +135,7 @@ impl FileSystemSandboxRunner {
|
||||
env: self.helper_env.clone(),
|
||||
additional_permissions: None,
|
||||
};
|
||||
sandbox_manager
|
||||
let mut command = sandbox_manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command,
|
||||
permissions: permission_profile,
|
||||
@@ -114,7 +148,43 @@ impl FileSystemSandboxRunner {
|
||||
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}")))
|
||||
.map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))?;
|
||||
#[cfg(target_os = "windows")]
|
||||
if command.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
|
||||
let request_file = wrap_windows_sandbox_command(&mut command, helper)?;
|
||||
return Ok(PreparedFsHelperCommand::with_windows_wrapper_request_file(
|
||||
command,
|
||||
request_file,
|
||||
));
|
||||
}
|
||||
Ok(PreparedFsHelperCommand::new(command))
|
||||
}
|
||||
}
|
||||
|
||||
struct PreparedFsHelperCommand {
|
||||
request: SandboxExecRequest,
|
||||
#[cfg(target_os = "windows")]
|
||||
_windows_wrapper_request_file: Option<WindowsSandboxWrapperRequestFile>,
|
||||
}
|
||||
|
||||
impl PreparedFsHelperCommand {
|
||||
fn new(request: SandboxExecRequest) -> Self {
|
||||
Self {
|
||||
request,
|
||||
#[cfg(target_os = "windows")]
|
||||
_windows_wrapper_request_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn with_windows_wrapper_request_file(
|
||||
request: SandboxExecRequest,
|
||||
request_file: WindowsSandboxWrapperRequestFile,
|
||||
) -> Self {
|
||||
Self {
|
||||
request,
|
||||
_windows_wrapper_request_file: Some(request_file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,10 +204,13 @@ fn sandbox_cwd(sandbox: &FileSystemSandboxContext) -> Result<AbsolutePathBuf, JS
|
||||
.map_err(|err| invalid_request(format!("current directory is not absolute: {err}")))
|
||||
}
|
||||
|
||||
fn helper_read_roots(runtime_paths: &ExecServerRuntimePaths) -> Vec<AbsolutePathBuf> {
|
||||
fn helper_read_roots(
|
||||
codex_self_exe: &AbsolutePathBuf,
|
||||
codex_linux_sandbox_exe: Option<&AbsolutePathBuf>,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
let mut roots = Vec::new();
|
||||
for path in std::iter::once(runtime_paths.codex_self_exe.as_path())
|
||||
.chain(runtime_paths.codex_linux_sandbox_exe.as_deref())
|
||||
for path in std::iter::once(codex_self_exe.as_path())
|
||||
.chain(codex_linux_sandbox_exe.map(AbsolutePathBuf::as_path))
|
||||
{
|
||||
if let Some(parent) = path.parent()
|
||||
&& let Ok(root) = AbsolutePathBuf::from_absolute_path(parent)
|
||||
@@ -247,20 +320,11 @@ fn bazel_bwrap_env_key_is_allowed(_key: &str) -> bool {
|
||||
}
|
||||
|
||||
async fn run_command(
|
||||
command: SandboxExecRequest,
|
||||
command: PreparedFsHelperCommand,
|
||||
request_json: Vec<u8>,
|
||||
) -> Result<FsHelperPayload, JSONRPCErrorError> {
|
||||
let mut child = spawn_command(command)?;
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| internal_error("failed to open fs sandbox helper stdin".to_string()))?;
|
||||
stdin.write_all(&request_json).await.map_err(io_error)?;
|
||||
stdin.shutdown().await.map_err(io_error)?;
|
||||
drop(stdin);
|
||||
|
||||
let output = child.wait_with_output().await.map_err(io_error)?;
|
||||
if !output.status.success() {
|
||||
let output = run_child_command(command, request_json).await?;
|
||||
if !output.success {
|
||||
return Err(internal_error(format!(
|
||||
"fs sandbox helper failed with status {status}: {stderr}",
|
||||
status = output.status,
|
||||
@@ -274,7 +338,36 @@ async fn run_command(
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_command(
|
||||
struct FsHelperCommandOutput {
|
||||
success: bool,
|
||||
status: String,
|
||||
stdout: Vec<u8>,
|
||||
stderr: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn run_child_command(
|
||||
command: PreparedFsHelperCommand,
|
||||
request_json: Vec<u8>,
|
||||
) -> Result<FsHelperCommandOutput, JSONRPCErrorError> {
|
||||
let mut child = spawn_child_command(command.request)?;
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| internal_error("failed to open fs sandbox helper stdin".to_string()))?;
|
||||
stdin.write_all(&request_json).await.map_err(io_error)?;
|
||||
stdin.shutdown().await.map_err(io_error)?;
|
||||
drop(stdin);
|
||||
|
||||
let output = child.wait_with_output().await.map_err(io_error)?;
|
||||
Ok(FsHelperCommandOutput {
|
||||
success: output.status.success(),
|
||||
status: output.status.to_string(),
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_child_command(
|
||||
SandboxExecRequest {
|
||||
command: argv,
|
||||
cwd,
|
||||
@@ -304,6 +397,90 @@ fn spawn_command(
|
||||
command.spawn().map_err(io_error)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn wrap_windows_sandbox_command(
|
||||
request: &mut SandboxExecRequest,
|
||||
helper: &AbsolutePathBuf,
|
||||
) -> Result<WindowsSandboxWrapperRequestFile, JSONRPCErrorError> {
|
||||
let codex_home = codex_utils_home_dir::find_codex_home().map_err(|err| {
|
||||
internal_error(format!(
|
||||
"windows fs sandbox helper failed to resolve CODEX_HOME: {err}"
|
||||
))
|
||||
})?;
|
||||
let wrapper_request =
|
||||
codex_windows_sandbox::create_windows_sandbox_wrapper_request_for_permission_profile(
|
||||
std::mem::take(&mut request.command),
|
||||
request.cwd.clone(),
|
||||
request.permission_profile.clone(),
|
||||
request.windows_sandbox_level,
|
||||
request.windows_sandbox_private_desktop,
|
||||
codex_home.to_path_buf(),
|
||||
);
|
||||
let request_file =
|
||||
WindowsSandboxWrapperRequestFile::create(codex_home.as_path(), &wrapper_request)?;
|
||||
let mut args = codex_windows_sandbox::create_windows_sandbox_command_args_for_request_file(
|
||||
request_file.path.as_path(),
|
||||
);
|
||||
request.command = Vec::with_capacity(1 + args.len());
|
||||
request
|
||||
.command
|
||||
.push(helper.as_path().to_string_lossy().into_owned());
|
||||
request.command.append(&mut args);
|
||||
request.sandbox = codex_sandboxing::SandboxType::None;
|
||||
request.arg0 = None;
|
||||
Ok(request_file)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
struct WindowsSandboxWrapperRequestFile {
|
||||
path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl WindowsSandboxWrapperRequestFile {
|
||||
fn create(
|
||||
codex_home: &std::path::Path,
|
||||
request: &codex_windows_sandbox::WindowsSandboxWrapperRequest,
|
||||
) -> Result<Self, JSONRPCErrorError> {
|
||||
let request_dir = codex_windows_sandbox::sandbox_dir(codex_home);
|
||||
std::fs::create_dir_all(&request_dir).map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to create windows fs sandbox wrapper request dir {}: {err}",
|
||||
request_dir.display()
|
||||
))
|
||||
})?;
|
||||
let path = request_dir.join(format!(
|
||||
"fs-helper-wrapper-request-{}.json",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let request_json = serde_json::to_vec(request).map_err(json_error)?;
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&path)
|
||||
.map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to create windows fs sandbox wrapper request file {}: {err}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
std::io::Write::write_all(&mut file, &request_json).map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to write windows fs sandbox wrapper request file {}: {err}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl Drop for WindowsSandboxWrapperRequestFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn io_error(err: std::io::Error) -> JSONRPCErrorError {
|
||||
internal_error(err.to_string())
|
||||
}
|
||||
@@ -387,7 +564,10 @@ mod tests {
|
||||
|
||||
add_helper_runtime_permissions(
|
||||
&mut policy,
|
||||
&helper_read_roots(&runtime_paths),
|
||||
&helper_read_roots(
|
||||
&runtime_paths.codex_self_exe,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
),
|
||||
cwd.as_path(),
|
||||
);
|
||||
|
||||
@@ -498,10 +678,15 @@ mod tests {
|
||||
let sandbox_context = sandbox_context_with_cwd(&file_system_policy, cwd.clone());
|
||||
|
||||
let request = runner
|
||||
.sandbox_exec_request(&permission_profile, &cwd, &sandbox_context)
|
||||
.sandbox_exec_request(
|
||||
&permission_profile,
|
||||
&cwd,
|
||||
&sandbox_context,
|
||||
&runner.runtime_paths.codex_self_exe,
|
||||
)
|
||||
.expect("sandbox exec request");
|
||||
|
||||
assert_eq!(request.env.get(&path_key), Some(&path));
|
||||
assert_eq!(request.request.env.get(&path_key), Some(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -556,7 +741,10 @@ mod tests {
|
||||
|
||||
add_helper_runtime_permissions(
|
||||
&mut policy,
|
||||
&helper_read_roots(&runtime_paths),
|
||||
&helper_read_roots(
|
||||
&runtime_paths.codex_self_exe,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
),
|
||||
cwd.as_path(),
|
||||
);
|
||||
|
||||
@@ -581,7 +769,10 @@ mod tests {
|
||||
|
||||
add_helper_runtime_permissions(
|
||||
&mut policy,
|
||||
&helper_read_roots(&runtime_paths),
|
||||
&helper_read_roots(
|
||||
&runtime_paths.codex_self_exe,
|
||||
runtime_paths.codex_linux_sandbox_exe.as_ref(),
|
||||
),
|
||||
cwd.as_path(),
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_test_binary_support::TestBinaryDispatchGuard;
|
||||
use codex_test_binary_support::TestBinaryDispatchMode;
|
||||
use codex_test_binary_support::configure_test_binary_dispatch;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_windows_sandbox::CODEX_WINDOWS_SANDBOX_ARG1;
|
||||
use ctor::ctor;
|
||||
|
||||
pub(crate) mod exec_server;
|
||||
@@ -27,6 +29,10 @@ pub static TEST_BINARY_DISPATCH_GUARD: Option<TestBinaryDispatchGuard> = {
|
||||
if argv1 == Some(CODEX_FS_HELPER_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if argv1 == Some(CODEX_WINDOWS_SANDBOX_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod common;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::FileSystemSandboxContext;
|
||||
use codex_exec_server::LocalFileSystem;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serial_test::serial;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
original: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &std::ffi::OsStr) -> Self {
|
||||
let original = std::env::var_os(key);
|
||||
unsafe {
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.original {
|
||||
Some(value) => std::env::set_var(self.key, value),
|
||||
None => std::env::remove_var(self.key),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SandboxedFileSystem {
|
||||
_codex_home: TempDir,
|
||||
_guard: EnvVarGuard,
|
||||
file_system: LocalFileSystem,
|
||||
}
|
||||
|
||||
fn sandboxed_file_system() -> Result<SandboxedFileSystem> {
|
||||
let (codex_exe, _) = common::current_test_binary_helper_paths()?;
|
||||
sandboxed_file_system_with_codex_exe(codex_exe)
|
||||
}
|
||||
|
||||
fn sandboxed_file_system_with_codex_exe(
|
||||
codex_exe: std::path::PathBuf,
|
||||
) -> Result<SandboxedFileSystem> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str());
|
||||
let file_system = LocalFileSystem::with_runtime_paths(ExecServerRuntimePaths::new(
|
||||
codex_exe, /*codex_linux_sandbox_exe*/ None,
|
||||
)?);
|
||||
|
||||
Ok(SandboxedFileSystem {
|
||||
_codex_home: codex_home,
|
||||
_guard: guard,
|
||||
file_system,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_is_elevated() -> bool {
|
||||
let status = std::process::Command::new("powershell.exe")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"$principal = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent(); if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 } else { exit 1 }",
|
||||
])
|
||||
.status();
|
||||
status.is_ok_and(|status| status.success())
|
||||
}
|
||||
|
||||
fn stage_windows_sandbox_helpers(codex_home: &Path) -> Result<bool> {
|
||||
let helper_dir = codex_home.join(".sandbox-bin");
|
||||
std::fs::create_dir_all(&helper_dir)
|
||||
.with_context(|| format!("create helper dir {}", helper_dir.display()))?;
|
||||
for helper_name in ["codex-windows-sandbox-setup", "codex-command-runner"] {
|
||||
let helper = match codex_utils_cargo_bin::cargo_bin(helper_name) {
|
||||
Ok(helper) => helper,
|
||||
Err(err) => {
|
||||
eprintln!("skipping elevated fs-helper test: {helper_name} unavailable: {err}");
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
std::fs::copy(
|
||||
&helper,
|
||||
helper_dir.join(Path::new(helper_name).with_extension("exe")),
|
||||
)
|
||||
.with_context(|| format!("stage Windows sandbox helper {}", helper.display()))?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn temp_workspace() -> Result<(TempDir, AbsolutePathBuf)> {
|
||||
let dir = TempDir::new()?;
|
||||
let path = AbsolutePathBuf::try_from(std::fs::canonicalize(dir.path())?)?;
|
||||
Ok((dir, path))
|
||||
}
|
||||
|
||||
fn root_read_entry() -> FileSystemSandboxEntry {
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}
|
||||
}
|
||||
|
||||
fn path_entry(path: AbsolutePathBuf, access: FileSystemAccessMode) -> FileSystemSandboxEntry {
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access,
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_from_entries(
|
||||
workspace: AbsolutePathBuf,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
entries: Vec<FileSystemSandboxEntry>,
|
||||
) -> FileSystemSandboxContext {
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(entries);
|
||||
let permissions = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
let mut sandbox =
|
||||
FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, workspace);
|
||||
sandbox.windows_sandbox_level = windows_sandbox_level;
|
||||
sandbox
|
||||
}
|
||||
|
||||
fn workspace_write_sandbox(
|
||||
workspace: AbsolutePathBuf,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> FileSystemSandboxContext {
|
||||
sandbox_from_entries(
|
||||
workspace.clone(),
|
||||
windows_sandbox_level,
|
||||
vec![
|
||||
root_read_entry(),
|
||||
path_entry(workspace, FileSystemAccessMode::Write),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn workspace_write_sandbox_with_read_only_path(
|
||||
workspace: AbsolutePathBuf,
|
||||
read_only_path: AbsolutePathBuf,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> FileSystemSandboxContext {
|
||||
sandbox_from_entries(
|
||||
workspace.clone(),
|
||||
windows_sandbox_level,
|
||||
vec![
|
||||
root_read_entry(),
|
||||
path_entry(workspace, FileSystemAccessMode::Write),
|
||||
path_entry(read_only_path, FileSystemAccessMode::Read),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial(codex_home)]
|
||||
async fn restricted_token_fs_helper_uses_configured_helper_path() -> Result<()> {
|
||||
let helper_dir = TempDir::new()?;
|
||||
let current_exe = std::env::current_exe()?;
|
||||
let configured_helper = helper_dir.path().join("configured-codex-helper.exe");
|
||||
std::fs::copy(¤t_exe, &configured_helper)?;
|
||||
let sandboxed = sandboxed_file_system_with_codex_exe(configured_helper.clone())?;
|
||||
let (_workspace_dir, workspace) = temp_workspace()?;
|
||||
let file_path = workspace.join("allowed.txt");
|
||||
let sandbox = workspace_write_sandbox(workspace, WindowsSandboxLevel::RestrictedToken);
|
||||
|
||||
sandboxed
|
||||
.file_system
|
||||
.write_file(&file_path, b"allowed".to_vec(), Some(&sandbox))
|
||||
.await?;
|
||||
|
||||
let materialized_helper = sandboxed._codex_home.path().join(".sandbox-bin").join(
|
||||
configured_helper
|
||||
.file_name()
|
||||
.expect("configured helper name"),
|
||||
);
|
||||
assert_eq!(std::fs::read(&file_path)?, b"allowed");
|
||||
assert_eq!(materialized_helper.exists(), true);
|
||||
assert_eq!(
|
||||
materialized_helper.file_name(),
|
||||
configured_helper.file_name()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial(codex_home)]
|
||||
async fn restricted_token_fs_helper_rejects_missing_read_only_carveout() -> Result<()> {
|
||||
let sandboxed = sandboxed_file_system()?;
|
||||
let (_workspace_dir, workspace) = temp_workspace()?;
|
||||
let protected_dir = workspace.join("protected");
|
||||
let blocked_path = protected_dir.join("blocked.txt");
|
||||
let sandbox = workspace_write_sandbox_with_read_only_path(
|
||||
workspace,
|
||||
protected_dir,
|
||||
WindowsSandboxLevel::RestrictedToken,
|
||||
);
|
||||
|
||||
let error = sandboxed
|
||||
.file_system
|
||||
.write_file(&blocked_path, b"blocked".to_vec(), Some(&sandbox))
|
||||
.await
|
||||
.expect_err("read-only carveout should reject writes even before it exists");
|
||||
assert!(
|
||||
error.to_string().contains("Access is denied")
|
||||
|| error.to_string().contains("Permission denied")
|
||||
|| error.to_string().contains("Operation not permitted")
|
||||
|| error.to_string().contains("is not permitted"),
|
||||
"unexpected sandbox denial message: {error}",
|
||||
);
|
||||
assert_eq!(blocked_path.exists(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial(codex_home)]
|
||||
async fn elevated_fs_helper_rejects_unenforceable_reopened_writable_descendant() -> Result<()> {
|
||||
let sandboxed = sandboxed_file_system()?;
|
||||
let (_workspace_dir, workspace) = temp_workspace()?;
|
||||
let read_only_dir = workspace.join("read-only");
|
||||
let reopened_dir = read_only_dir.join("writable");
|
||||
let target_path = reopened_dir.join("blocked.txt");
|
||||
let sandbox = sandbox_from_entries(
|
||||
workspace.clone(),
|
||||
WindowsSandboxLevel::Elevated,
|
||||
vec![
|
||||
root_read_entry(),
|
||||
path_entry(workspace, FileSystemAccessMode::Write),
|
||||
path_entry(read_only_dir, FileSystemAccessMode::Read),
|
||||
path_entry(reopened_dir, FileSystemAccessMode::Write),
|
||||
],
|
||||
);
|
||||
|
||||
let error = sandboxed
|
||||
.file_system
|
||||
.write_file(&target_path, b"blocked".to_vec(), Some(&sandbox))
|
||||
.await
|
||||
.expect_err("elevated fs helper should reject unenforceable reopened descendants");
|
||||
|
||||
assert!(
|
||||
error.to_string().contains(
|
||||
"windows elevated sandbox cannot reopen writable descendants under read-only carveouts"
|
||||
),
|
||||
"unexpected elevated sandbox error: {error}",
|
||||
);
|
||||
assert_eq!(target_path.exists(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial(codex_home)]
|
||||
async fn elevated_fs_helper_allows_writable_path_when_setup_is_available() -> Result<()> {
|
||||
if !process_is_elevated() {
|
||||
eprintln!("skipping elevated fs-helper test: test process is not elevated");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let sandboxed = sandboxed_file_system()?;
|
||||
if !stage_windows_sandbox_helpers(sandboxed._codex_home.path())? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (_workspace_dir, workspace) = temp_workspace()?;
|
||||
let file_path = workspace.join("allowed-elevated.txt");
|
||||
let sandbox = workspace_write_sandbox(workspace, WindowsSandboxLevel::Elevated);
|
||||
|
||||
sandboxed
|
||||
.file_system
|
||||
.write_file(&file_path, b"allowed elevated".to_vec(), Some(&sandbox))
|
||||
.await?;
|
||||
|
||||
assert_eq!(std::fs::read(&file_path)?, b"allowed elevated");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user