mirror of
https://github.com/openai/codex.git
synced 2026-05-04 13:21:54 +03:00
Run exec-server fs operations through sandbox helper (#17294)
## Summary - run exec-server filesystem RPCs requiring sandboxing through a `codex-fs` arg0 helper over stdin/stdout - keep direct local filesystem execution for `DangerFullAccess` and external sandbox policies - remove the standalone exec-server binary path in favor of top-level arg0 dispatch/runtime paths - add sandbox escape regression coverage for local and remote filesystem paths ## Validation - `just fmt` - `git diff --check` - remote devbox: `cd codex-rs && bazel test --bes_backend= --bes_results_url= //codex-rs/exec-server:all` (6/6 passed) --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -13,23 +10,240 @@ use tokio::io;
|
||||
|
||||
use crate::CopyOptions;
|
||||
use crate::CreateDirectoryOptions;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::FileMetadata;
|
||||
use crate::FileSystemResult;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::ReadDirectoryEntry;
|
||||
use crate::RemoveOptions;
|
||||
use crate::sandboxed_file_system::SandboxedFileSystem;
|
||||
|
||||
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
|
||||
|
||||
pub static LOCAL_FS: LazyLock<Arc<dyn ExecutorFileSystem>> =
|
||||
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem) });
|
||||
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem::unsandboxed()) });
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct LocalFileSystem;
|
||||
pub(crate) struct DirectFileSystem;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct UnsandboxedFileSystem {
|
||||
file_system: DirectFileSystem,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LocalFileSystem {
|
||||
unsandboxed: UnsandboxedFileSystem,
|
||||
sandboxed: Option<SandboxedFileSystem>,
|
||||
}
|
||||
|
||||
impl LocalFileSystem {
|
||||
pub fn unsandboxed() -> Self {
|
||||
Self {
|
||||
unsandboxed: UnsandboxedFileSystem::default(),
|
||||
sandboxed: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_runtime_paths(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
unsandboxed: UnsandboxedFileSystem::default(),
|
||||
sandboxed: Some(SandboxedFileSystem::new(runtime_paths)),
|
||||
}
|
||||
}
|
||||
|
||||
fn sandboxed(&self) -> io::Result<&SandboxedFileSystem> {
|
||||
self.sandboxed.as_ref().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"sandboxed filesystem operations require configured runtime paths",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn file_system_for<'a>(
|
||||
&'a self,
|
||||
sandbox: Option<&'a FileSystemSandboxContext>,
|
||||
) -> io::Result<(
|
||||
&'a dyn ExecutorFileSystem,
|
||||
Option<&'a FileSystemSandboxContext>,
|
||||
)> {
|
||||
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
|
||||
Ok((self.sandboxed()?, sandbox))
|
||||
} else {
|
||||
Ok((&self.unsandboxed, sandbox))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for LocalFileSystem {
|
||||
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>> {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.read_file(path, sandbox).await
|
||||
}
|
||||
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.write_file(path, contents, sandbox).await
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.create_directory(path, options, sandbox).await
|
||||
}
|
||||
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.get_metadata(path, sandbox).await
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.read_directory(path, sandbox).await
|
||||
}
|
||||
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.remove(path, options, sandbox).await
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system
|
||||
.copy(source_path, destination_path, options, sandbox)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for UnsandboxedFileSystem {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system.read_file(path, /*sandbox*/ None).await
|
||||
}
|
||||
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.write_file(path, contents, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.create_directory(path, options, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system.get_metadata(path, /*sandbox*/ None).await
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.read_directory(path, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.remove(path, options, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.copy(
|
||||
source_path,
|
||||
destination_path,
|
||||
options,
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for DirectFileSystem {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let metadata = tokio::fs::metadata(path.as_path()).await?;
|
||||
if metadata.len() > MAX_READ_FILE_BYTES {
|
||||
return Err(io::Error::new(
|
||||
@@ -40,34 +254,23 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
tokio::fs::read(path.as_path()).await
|
||||
}
|
||||
|
||||
async fn read_file_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
enforce_read_access(path, sandbox_policy)?;
|
||||
self.read_file(path).await
|
||||
}
|
||||
|
||||
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
|
||||
tokio::fs::write(path.as_path(), contents).await
|
||||
}
|
||||
|
||||
async fn write_file_with_sandbox_policy(
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_write_access(path, sandbox_policy)?;
|
||||
self.write_file(path, contents).await
|
||||
reject_sandbox_context(sandbox)?;
|
||||
tokio::fs::write(path.as_path(), contents).await
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
if options.recursive {
|
||||
tokio::fs::create_dir_all(path.as_path()).await?;
|
||||
} else {
|
||||
@@ -76,17 +279,12 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_directory_with_sandbox_policy(
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
create_directory_options: CreateDirectoryOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_write_access(path, sandbox_policy)?;
|
||||
self.create_directory(path, create_directory_options).await
|
||||
}
|
||||
|
||||
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let metadata = tokio::fs::metadata(path.as_path()).await?;
|
||||
Ok(FileMetadata {
|
||||
is_directory: metadata.is_dir(),
|
||||
@@ -96,19 +294,12 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_metadata_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
enforce_read_access(path, sandbox_policy)?;
|
||||
self.get_metadata(path).await
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let mut entries = Vec::new();
|
||||
let mut read_dir = tokio::fs::read_dir(path.as_path()).await?;
|
||||
while let Some(entry) = read_dir.next_entry().await? {
|
||||
@@ -122,16 +313,13 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn read_directory_with_sandbox_policy(
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
enforce_read_access(path, sandbox_policy)?;
|
||||
self.read_directory(path).await
|
||||
}
|
||||
|
||||
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
match tokio::fs::symlink_metadata(path.as_path()).await {
|
||||
Ok(metadata) => {
|
||||
let file_type = metadata.file_type();
|
||||
@@ -151,22 +339,14 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
remove_options: RemoveOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_write_access_preserving_leaf(path, sandbox_policy)?;
|
||||
self.remove(path, remove_options).await
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let source_path = source_path.to_path_buf();
|
||||
let destination_path = destination_path.to_path_buf();
|
||||
tokio::task::spawn_blocking(move || -> FileSystemResult<()> {
|
||||
@@ -211,164 +391,26 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
.await
|
||||
.map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))?
|
||||
}
|
||||
|
||||
async fn copy_with_sandbox_policy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
copy_options: CopyOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_copy_source_read_access(source_path, sandbox_policy)?;
|
||||
enforce_write_access(destination_path, sandbox_policy)?;
|
||||
self.copy(source_path, destination_path, copy_options).await
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_read_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_read_path_with_cwd,
|
||||
"read",
|
||||
AccessPathMode::ResolveAll,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_write_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_write_path_with_cwd,
|
||||
"write",
|
||||
AccessPathMode::ResolveAll,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_write_access_preserving_leaf(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_write_path_with_cwd,
|
||||
"write",
|
||||
AccessPathMode::PreserveLeaf,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_copy_source_read_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
let path_mode = match std::fs::symlink_metadata(path.as_path()) {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => AccessPathMode::PreserveLeaf,
|
||||
_ => AccessPathMode::ResolveAll,
|
||||
};
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_read_path_with_cwd,
|
||||
"read",
|
||||
path_mode,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
fn enforce_read_access_for_cwd(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox_cwd: &AbsolutePathBuf,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_cwd(
|
||||
path,
|
||||
sandbox_policy,
|
||||
sandbox_cwd,
|
||||
FileSystemSandboxPolicy::can_read_path_with_cwd,
|
||||
"read",
|
||||
AccessPathMode::ResolveAll,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_access_for_current_dir(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
is_allowed: fn(&FileSystemSandboxPolicy, &Path, &Path) -> bool,
|
||||
access_kind: &str,
|
||||
path_mode: AccessPathMode,
|
||||
) -> FileSystemResult<()> {
|
||||
let Some(sandbox_policy) = sandbox_policy else {
|
||||
return Ok(());
|
||||
};
|
||||
let cwd = current_sandbox_cwd()?;
|
||||
enforce_access(
|
||||
path,
|
||||
sandbox_policy,
|
||||
cwd.as_path(),
|
||||
is_allowed,
|
||||
access_kind,
|
||||
path_mode,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
fn enforce_access_for_cwd(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox_cwd: &AbsolutePathBuf,
|
||||
is_allowed: fn(&FileSystemSandboxPolicy, &Path, &Path) -> bool,
|
||||
access_kind: &str,
|
||||
path_mode: AccessPathMode,
|
||||
) -> FileSystemResult<()> {
|
||||
let Some(sandbox_policy) = sandbox_policy else {
|
||||
return Ok(());
|
||||
};
|
||||
let cwd = resolve_existing_path(sandbox_cwd.as_path())?;
|
||||
enforce_access(
|
||||
path,
|
||||
sandbox_policy,
|
||||
cwd.as_path(),
|
||||
is_allowed,
|
||||
access_kind,
|
||||
path_mode,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
is_allowed: fn(&FileSystemSandboxPolicy, &Path, &Path) -> bool,
|
||||
access_kind: &str,
|
||||
path_mode: AccessPathMode,
|
||||
) -> FileSystemResult<()> {
|
||||
let resolved_path = resolve_path_for_access_check(path.as_path(), path_mode)?;
|
||||
let file_system_policy =
|
||||
canonicalize_file_system_policy_paths(FileSystemSandboxPolicy::from(sandbox_policy))?;
|
||||
if is_allowed(&file_system_policy, resolved_path.as_path(), sandbox_cwd) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
|
||||
if sandbox.is_some() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"fs/{access_kind} is not permitted for path {}",
|
||||
path.as_path().display()
|
||||
),
|
||||
))
|
||||
"direct filesystem operations do not accept sandbox context",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum AccessPathMode {
|
||||
ResolveAll,
|
||||
PreserveLeaf,
|
||||
fn reject_platform_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
|
||||
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"sandboxed filesystem operations require configured runtime paths",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {
|
||||
@@ -395,28 +437,11 @@ fn destination_is_same_or_descendant_of_source(
|
||||
destination: &Path,
|
||||
) -> io::Result<bool> {
|
||||
let source = std::fs::canonicalize(source)?;
|
||||
let destination = resolve_path_for_access_check(destination, AccessPathMode::ResolveAll)?;
|
||||
let destination = resolve_existing_path(destination)?;
|
||||
Ok(destination.starts_with(&source))
|
||||
}
|
||||
|
||||
fn resolve_path_for_access_check(path: &Path, path_mode: AccessPathMode) -> io::Result<PathBuf> {
|
||||
match path_mode {
|
||||
AccessPathMode::ResolveAll => resolve_existing_path(path),
|
||||
AccessPathMode::PreserveLeaf => preserve_leaf_path_for_access_check(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn preserve_leaf_path_for_access_check(path: &Path) -> io::Result<PathBuf> {
|
||||
let Some(file_name) = path.file_name() else {
|
||||
return resolve_existing_path(path);
|
||||
};
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("/"));
|
||||
let mut resolved_parent = resolve_existing_path(parent)?;
|
||||
resolved_parent.push(file_name);
|
||||
Ok(resolved_parent)
|
||||
}
|
||||
|
||||
fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
|
||||
pub(crate) fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
|
||||
let mut unresolved_suffix = Vec::new();
|
||||
let mut existing_path = path;
|
||||
while !existing_path.exists() {
|
||||
@@ -437,33 +462,12 @@ fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn current_sandbox_cwd() -> io::Result<PathBuf> {
|
||||
pub(crate) fn current_sandbox_cwd() -> io::Result<PathBuf> {
|
||||
let cwd = std::env::current_dir()
|
||||
.map_err(|err| io::Error::other(format!("failed to read current dir: {err}")))?;
|
||||
resolve_existing_path(cwd.as_path())
|
||||
}
|
||||
|
||||
fn canonicalize_file_system_policy_paths(
|
||||
mut file_system_policy: FileSystemSandboxPolicy,
|
||||
) -> io::Result<FileSystemSandboxPolicy> {
|
||||
for entry in &mut file_system_policy.entries {
|
||||
if let FileSystemPath::Path { path } = &mut entry.path {
|
||||
*path = canonicalize_absolute_path(path)?;
|
||||
}
|
||||
}
|
||||
Ok(file_system_policy)
|
||||
}
|
||||
|
||||
fn canonicalize_absolute_path(path: &AbsolutePathBuf) -> io::Result<AbsolutePathBuf> {
|
||||
let resolved = resolve_existing_path(path.as_path())?;
|
||||
AbsolutePathBuf::from_absolute_path(resolved.as_path()).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("path must stay absolute after canonicalization: {err}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn copy_symlink(source: &Path, target: &Path) -> io::Result<()> {
|
||||
let link_target = std::fs::read_link(source)?;
|
||||
#[cfg(unix)]
|
||||
@@ -508,29 +512,11 @@ fn system_time_to_unix_ms(time: SystemTime) -> i64 {
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
fn absolute_path(path: PathBuf) -> AbsolutePathBuf {
|
||||
match AbsolutePathBuf::try_from(path) {
|
||||
Ok(path) => path,
|
||||
Err(err) => panic!("absolute path: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_only_sandbox_policy(readable_roots: Vec<PathBuf>) -> SandboxPolicy {
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: readable_roots.into_iter().map(absolute_path).collect(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_for_access_check_rejects_symlink_parent_dotdot_escape() -> io::Result<()> {
|
||||
fn resolve_existing_path_handles_symlink_parent_dotdot_escape() -> io::Result<()> {
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let allowed_dir = temp_dir.path().join("allowed");
|
||||
let outside_dir = temp_dir.path().join("outside");
|
||||
@@ -538,13 +524,12 @@ mod tests {
|
||||
std::fs::create_dir_all(&outside_dir)?;
|
||||
symlink(&outside_dir, allowed_dir.join("link"))?;
|
||||
|
||||
let resolved = resolve_path_for_access_check(
|
||||
let resolved = resolve_existing_path(
|
||||
allowed_dir
|
||||
.join("link")
|
||||
.join("..")
|
||||
.join("secret.txt")
|
||||
.as_path(),
|
||||
AccessPathMode::ResolveAll,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
@@ -553,29 +538,6 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforce_read_access_uses_explicit_sandbox_cwd() -> io::Result<()> {
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let workspace_dir = temp_dir.path().join("workspace");
|
||||
let other_dir = temp_dir.path().join("other");
|
||||
let note_path = workspace_dir.join("note.txt");
|
||||
std::fs::create_dir_all(&workspace_dir)?;
|
||||
std::fs::create_dir_all(&other_dir)?;
|
||||
std::fs::write(¬e_path, "hello")?;
|
||||
|
||||
let sandbox_policy = read_only_sandbox_policy(vec![]);
|
||||
let sandbox_cwd = absolute_path(workspace_dir);
|
||||
let other_cwd = absolute_path(other_dir);
|
||||
let note_path = absolute_path(note_path);
|
||||
|
||||
enforce_read_access_for_cwd(¬e_path, Some(&sandbox_policy), &sandbox_cwd)?;
|
||||
|
||||
let error = enforce_read_access_for_cwd(¬e_path, Some(&sandbox_policy), &other_cwd)
|
||||
.expect_err("read should be rejected outside provided cwd");
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
|
||||
Reference in New Issue
Block a user