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:
starr-openai
2026-04-12 18:36:03 -07:00
committed by GitHub
parent 7c1e41c8b6
commit d626dc3895
52 changed files with 2313 additions and 895 deletions

View File

@@ -11,7 +11,10 @@ use anyhow::Result;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment;
use codex_exec_server::ExecServerRuntimePaths;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::LocalFileSystem;
use codex_exec_server::ReadDirectoryEntry;
use codex_exec_server::RemoveOptions;
use codex_protocol::protocol::ReadOnlyAccess;
@@ -38,9 +41,15 @@ async fn create_file_system_context(use_remote: bool) -> Result<FileSystemContex
_server: Some(server),
})
} else {
let environment = Environment::create(/*exec_server_url*/ None).await?;
let codex = codex_utils_cargo_bin::cargo_bin("codex")?;
#[cfg(target_os = "linux")]
let codex_linux_sandbox_exe =
Some(codex_utils_cargo_bin::cargo_bin("codex-linux-sandbox")?);
#[cfg(not(target_os = "linux"))]
let codex_linux_sandbox_exe = None;
let runtime_paths = ExecServerRuntimePaths::new(codex, codex_linux_sandbox_exe)?;
Ok(FileSystemContext {
file_system: environment.get_filesystem(),
file_system: Arc::new(LocalFileSystem::with_runtime_paths(runtime_paths)),
_server: None,
})
}
@@ -58,18 +67,18 @@ fn absolute_path(path: std::path::PathBuf) -> AbsolutePathBuf {
}
}
fn read_only_sandbox_policy(readable_root: std::path::PathBuf) -> SandboxPolicy {
SandboxPolicy::ReadOnly {
fn read_only_sandbox(readable_root: std::path::PathBuf) -> FileSystemSandboxContext {
FileSystemSandboxContext::new(SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![absolute_path(readable_root)],
},
network_access: false,
}
})
}
fn workspace_write_sandbox_policy(writable_root: std::path::PathBuf) -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
fn workspace_write_sandbox(writable_root: std::path::PathBuf) -> FileSystemSandboxContext {
FileSystemSandboxContext::new(SandboxPolicy::WorkspaceWrite {
writable_roots: vec![absolute_path(writable_root)],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
@@ -78,6 +87,42 @@ fn workspace_write_sandbox_policy(writable_root: std::path::PathBuf) -> SandboxP
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
})
}
fn assert_sandbox_denied(error: &std::io::Error) {
assert!(
matches!(
error.kind(),
std::io::ErrorKind::InvalidInput | std::io::ErrorKind::PermissionDenied
),
"unexpected sandbox error kind: {error:?}",
);
let message = error.to_string();
assert!(
message.contains("is not permitted")
|| message.contains("Operation not permitted")
|| message.contains("Permission denied"),
"unexpected sandbox error message: {message}",
);
}
fn assert_normalized_path_rejected(error: &std::io::Error) {
match error.kind() {
std::io::ErrorKind::NotFound => assert!(
error.to_string().contains("No such file or directory"),
"unexpected not-found message: {error}",
),
std::io::ErrorKind::InvalidInput | std::io::ErrorKind::PermissionDenied => {
let message = error.to_string();
assert!(
message.contains("is not permitted")
|| message.contains("Operation not permitted")
|| message.contains("Permission denied"),
"unexpected rejection message: {message}",
);
}
other => panic!("unexpected normalized-path error kind: {other:?}: {error:?}"),
}
}
@@ -93,7 +138,7 @@ async fn file_system_get_metadata_returns_expected_fields(use_remote: bool) -> R
std::fs::write(&file_path, "hello")?;
let metadata = file_system
.get_metadata(&absolute_path(file_path))
.get_metadata(&absolute_path(file_path), /*sandbox*/ None)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(metadata.is_directory, false);
@@ -122,6 +167,7 @@ async fn file_system_methods_cover_surface_area(use_remote: bool) -> Result<()>
.create_directory(
&absolute_path(nested_dir.clone()),
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -130,6 +176,7 @@ async fn file_system_methods_cover_surface_area(use_remote: bool) -> Result<()>
.write_file(
&absolute_path(nested_file.clone()),
b"hello from trait".to_vec(),
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -137,18 +184,19 @@ async fn file_system_methods_cover_surface_area(use_remote: bool) -> Result<()>
.write_file(
&absolute_path(source_file.clone()),
b"hello from source root".to_vec(),
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
let nested_file_contents = file_system
.read_file(&absolute_path(nested_file.clone()))
.read_file(&absolute_path(nested_file.clone()), /*sandbox*/ None)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(nested_file_contents, b"hello from trait");
let nested_file_text = file_system
.read_file_text(&absolute_path(nested_file.clone()))
.read_file_text(&absolute_path(nested_file.clone()), /*sandbox*/ None)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(nested_file_text, "hello from trait");
@@ -158,6 +206,7 @@ async fn file_system_methods_cover_surface_area(use_remote: bool) -> Result<()>
&absolute_path(nested_file),
&absolute_path(copied_file.clone()),
CopyOptions { recursive: false },
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -168,6 +217,7 @@ async fn file_system_methods_cover_surface_area(use_remote: bool) -> Result<()>
&absolute_path(source_dir.clone()),
&absolute_path(copied_dir.clone()),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -177,7 +227,7 @@ async fn file_system_methods_cover_surface_area(use_remote: bool) -> Result<()>
);
let mut entries = file_system
.read_directory(&absolute_path(source_dir))
.read_directory(&absolute_path(source_dir), /*sandbox*/ None)
.await
.with_context(|| format!("mode={use_remote}"))?;
entries.sort_by(|left, right| left.file_name.cmp(&right.file_name));
@@ -204,6 +254,7 @@ async fn file_system_methods_cover_surface_area(use_remote: bool) -> Result<()>
recursive: true,
force: true,
},
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -228,6 +279,7 @@ async fn file_system_copy_rejects_directory_without_recursive(use_remote: bool)
&absolute_path(source_dir),
&absolute_path(tmp.path().join("dest")),
CopyOptions { recursive: false },
/*sandbox*/ None,
)
.await;
let error = match error {
@@ -246,7 +298,7 @@ async fn file_system_copy_rejects_directory_without_recursive(use_remote: bool)
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_read_with_sandbox_policy_allows_readable_root(use_remote: bool) -> Result<()> {
async fn file_system_sandboxed_read_allows_readable_root(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -255,10 +307,10 @@ async fn file_system_read_with_sandbox_policy_allows_readable_root(use_remote: b
let file_path = allowed_dir.join("note.txt");
std::fs::create_dir_all(&allowed_dir)?;
std::fs::write(&file_path, "sandboxed hello")?;
let sandbox_policy = read_only_sandbox_policy(allowed_dir);
let sandbox = read_only_sandbox(allowed_dir);
let contents = file_system
.read_file_with_sandbox_policy(&absolute_path(file_path), Some(&sandbox_policy))
.read_file(&absolute_path(file_path), Some(&sandbox))
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(contents, b"sandboxed hello");
@@ -269,9 +321,7 @@ async fn file_system_read_with_sandbox_policy_allows_readable_root(use_remote: b
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_write_with_sandbox_policy_rejects_unwritable_path(
use_remote: bool,
) -> Result<()> {
async fn file_system_sandboxed_write_rejects_unwritable_path(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -280,26 +330,19 @@ async fn file_system_write_with_sandbox_policy_rejects_unwritable_path(
let blocked_path = tmp.path().join("blocked.txt");
std::fs::create_dir_all(&allowed_dir)?;
let sandbox_policy = read_only_sandbox_policy(allowed_dir);
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system
.write_file_with_sandbox_policy(
.write_file(
&absolute_path(blocked_path.clone()),
b"nope".to_vec(),
Some(&sandbox_policy),
Some(&sandbox),
)
.await
{
Ok(()) => anyhow::bail!("write should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/write is not permitted for path {}",
blocked_path.display()
)
);
assert_sandbox_denied(&error);
assert!(!blocked_path.exists());
Ok(())
@@ -308,9 +351,7 @@ async fn file_system_write_with_sandbox_policy_rejects_unwritable_path(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_read_with_sandbox_policy_rejects_symlink_escape(
use_remote: bool,
) -> Result<()> {
async fn file_system_sandboxed_read_rejects_symlink_escape(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -323,25 +364,15 @@ async fn file_system_read_with_sandbox_policy_rejects_symlink_escape(
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = allowed_dir.join("link").join("secret.txt");
let sandbox_policy = read_only_sandbox_policy(allowed_dir);
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system
.read_file_with_sandbox_policy(
&absolute_path(requested_path.clone()),
Some(&sandbox_policy),
)
.read_file(&absolute_path(requested_path.clone()), Some(&sandbox))
.await
{
Ok(_) => anyhow::bail!("read should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/read is not permitted for path {}",
requested_path.display()
)
);
assert_sandbox_denied(&error);
Ok(())
}
@@ -349,7 +380,7 @@ async fn file_system_read_with_sandbox_policy_rejects_symlink_escape(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_read_with_sandbox_policy_rejects_symlink_parent_dotdot_escape(
async fn file_system_sandboxed_read_rejects_symlink_parent_dotdot_escape(
use_remote: bool,
) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
@@ -365,15 +396,17 @@ async fn file_system_read_with_sandbox_policy_rejects_symlink_parent_dotdot_esca
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = absolute_path(allowed_dir.join("link").join("..").join("secret.txt"));
let sandbox_policy = read_only_sandbox_policy(allowed_dir);
let error = match file_system
.read_file_with_sandbox_policy(&requested_path, Some(&sandbox_policy))
.await
{
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system.read_file(&requested_path, Some(&sandbox)).await {
Ok(_) => anyhow::bail!("read should fail after path normalization"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
// AbsolutePathBuf normalizes `link/../secret.txt` to `allowed/secret.txt`
// before the request reaches the filesystem layer. Depending on whether
// the platform/runtime resolves that normalized path through a top-level
// symlink alias, the request can surface as either "missing file" or an
// upfront sandbox rejection.
assert_normalized_path_rejected(&error);
Ok(())
}
@@ -381,9 +414,7 @@ async fn file_system_read_with_sandbox_policy_rejects_symlink_parent_dotdot_esca
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_write_with_sandbox_policy_rejects_symlink_escape(
use_remote: bool,
) -> Result<()> {
async fn file_system_sandboxed_write_rejects_symlink_escape(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -395,26 +426,19 @@ async fn file_system_write_with_sandbox_policy_rejects_symlink_escape(
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = allowed_dir.join("link").join("blocked.txt");
let sandbox_policy = workspace_write_sandbox_policy(allowed_dir);
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.write_file_with_sandbox_policy(
.write_file(
&absolute_path(requested_path.clone()),
b"nope".to_vec(),
Some(&sandbox_policy),
Some(&sandbox),
)
.await
{
Ok(()) => anyhow::bail!("write should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/write is not permitted for path {}",
requested_path.display()
)
);
assert_sandbox_denied(&error);
assert!(!outside_dir.join("blocked.txt").exists());
Ok(())
@@ -423,9 +447,7 @@ async fn file_system_write_with_sandbox_policy_rejects_symlink_escape(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_create_directory_with_sandbox_policy_rejects_symlink_escape(
use_remote: bool,
) -> Result<()> {
async fn file_system_create_directory_rejects_symlink_escape(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -437,26 +459,19 @@ async fn file_system_create_directory_with_sandbox_policy_rejects_symlink_escape
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = allowed_dir.join("link").join("created");
let sandbox_policy = workspace_write_sandbox_policy(allowed_dir);
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.create_directory_with_sandbox_policy(
.create_directory(
&absolute_path(requested_path.clone()),
CreateDirectoryOptions { recursive: false },
Some(&sandbox_policy),
Some(&sandbox),
)
.await
{
Ok(()) => anyhow::bail!("create_directory should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/write is not permitted for path {}",
requested_path.display()
)
);
assert_sandbox_denied(&error);
assert!(!outside_dir.join("created").exists());
Ok(())
@@ -465,9 +480,7 @@ async fn file_system_create_directory_with_sandbox_policy_rejects_symlink_escape
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_get_metadata_with_sandbox_policy_rejects_symlink_escape(
use_remote: bool,
) -> Result<()> {
async fn file_system_get_metadata_rejects_symlink_escape(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -480,25 +493,15 @@ async fn file_system_get_metadata_with_sandbox_policy_rejects_symlink_escape(
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = allowed_dir.join("link").join("secret.txt");
let sandbox_policy = read_only_sandbox_policy(allowed_dir);
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system
.get_metadata_with_sandbox_policy(
&absolute_path(requested_path.clone()),
Some(&sandbox_policy),
)
.get_metadata(&absolute_path(requested_path.clone()), Some(&sandbox))
.await
{
Ok(_) => anyhow::bail!("get_metadata should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/read is not permitted for path {}",
requested_path.display()
)
);
assert_sandbox_denied(&error);
Ok(())
}
@@ -506,9 +509,7 @@ async fn file_system_get_metadata_with_sandbox_policy_rejects_symlink_escape(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_read_directory_with_sandbox_policy_rejects_symlink_escape(
use_remote: bool,
) -> Result<()> {
async fn file_system_read_directory_rejects_symlink_escape(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -521,25 +522,15 @@ async fn file_system_read_directory_with_sandbox_policy_rejects_symlink_escape(
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = allowed_dir.join("link");
let sandbox_policy = read_only_sandbox_policy(allowed_dir);
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system
.read_directory_with_sandbox_policy(
&absolute_path(requested_path.clone()),
Some(&sandbox_policy),
)
.read_directory(&absolute_path(requested_path.clone()), Some(&sandbox))
.await
{
Ok(_) => anyhow::bail!("read_directory should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/read is not permitted for path {}",
requested_path.display()
)
);
assert_sandbox_denied(&error);
Ok(())
}
@@ -547,9 +538,7 @@ async fn file_system_read_directory_with_sandbox_policy_rejects_symlink_escape(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_copy_with_sandbox_policy_rejects_symlink_escape_destination(
use_remote: bool,
) -> Result<()> {
async fn file_system_copy_rejects_symlink_escape_destination(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -562,27 +551,20 @@ async fn file_system_copy_with_sandbox_policy_rejects_symlink_escape_destination
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_destination = allowed_dir.join("link").join("copied.txt");
let sandbox_policy = workspace_write_sandbox_policy(allowed_dir.clone());
let sandbox = workspace_write_sandbox(allowed_dir.clone());
let error = match file_system
.copy_with_sandbox_policy(
.copy(
&absolute_path(allowed_dir.join("source.txt")),
&absolute_path(requested_destination.clone()),
CopyOptions { recursive: false },
Some(&sandbox_policy),
Some(&sandbox),
)
.await
{
Ok(()) => anyhow::bail!("copy should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/write is not permitted for path {}",
requested_destination.display()
)
);
assert_sandbox_denied(&error);
assert!(!outside_dir.join("copied.txt").exists());
Ok(())
@@ -591,9 +573,7 @@ async fn file_system_copy_with_sandbox_policy_rejects_symlink_escape_destination
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_remove_with_sandbox_policy_removes_symlink_not_target(
use_remote: bool,
) -> Result<()> {
async fn file_system_remove_removes_symlink_not_target(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -607,15 +587,15 @@ async fn file_system_remove_with_sandbox_policy_removes_symlink_not_target(
let symlink_path = allowed_dir.join("link");
symlink(&outside_file, &symlink_path)?;
let sandbox_policy = workspace_write_sandbox_policy(allowed_dir);
let sandbox = workspace_write_sandbox(allowed_dir);
file_system
.remove_with_sandbox_policy(
.remove(
&absolute_path(symlink_path.clone()),
RemoveOptions {
recursive: false,
force: false,
},
Some(&sandbox_policy),
Some(&sandbox),
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -630,9 +610,7 @@ async fn file_system_remove_with_sandbox_policy_removes_symlink_not_target(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_copy_with_sandbox_policy_preserves_symlink_source(
use_remote: bool,
) -> Result<()> {
async fn file_system_copy_preserves_symlink_source(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -647,13 +625,13 @@ async fn file_system_copy_with_sandbox_policy_preserves_symlink_source(
std::fs::write(&outside_file, "outside")?;
symlink(&outside_file, &source_symlink)?;
let sandbox_policy = workspace_write_sandbox_policy(allowed_dir.clone());
let sandbox = workspace_write_sandbox(allowed_dir.clone());
file_system
.copy_with_sandbox_policy(
.copy(
&absolute_path(source_symlink),
&absolute_path(copied_symlink.clone()),
CopyOptions { recursive: false },
Some(&sandbox_policy),
Some(&sandbox),
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -668,9 +646,7 @@ async fn file_system_copy_with_sandbox_policy_preserves_symlink_source(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_remove_with_sandbox_policy_rejects_symlink_escape(
use_remote: bool,
) -> Result<()> {
async fn file_system_remove_rejects_symlink_escape(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -684,29 +660,22 @@ async fn file_system_remove_with_sandbox_policy_rejects_symlink_escape(
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = allowed_dir.join("link").join("secret.txt");
let sandbox_policy = workspace_write_sandbox_policy(allowed_dir);
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.remove_with_sandbox_policy(
.remove(
&absolute_path(requested_path.clone()),
RemoveOptions {
recursive: false,
force: false,
},
Some(&sandbox_policy),
Some(&sandbox),
)
.await
{
Ok(()) => anyhow::bail!("remove should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/write is not permitted for path {}",
requested_path.display()
)
);
assert_sandbox_denied(&error);
assert_eq!(std::fs::read_to_string(outside_file)?, "outside");
Ok(())
@@ -715,9 +684,7 @@ async fn file_system_remove_with_sandbox_policy_rejects_symlink_escape(
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_copy_with_sandbox_policy_rejects_symlink_escape_source(
use_remote: bool,
) -> Result<()> {
async fn file_system_copy_rejects_symlink_escape_source(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
@@ -732,27 +699,20 @@ async fn file_system_copy_with_sandbox_policy_rejects_symlink_escape_source(
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_source = allowed_dir.join("link").join("secret.txt");
let sandbox_policy = workspace_write_sandbox_policy(allowed_dir);
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.copy_with_sandbox_policy(
.copy(
&absolute_path(requested_source.clone()),
&absolute_path(requested_destination.clone()),
CopyOptions { recursive: false },
Some(&sandbox_policy),
Some(&sandbox),
)
.await
{
Ok(()) => anyhow::bail!("copy should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/read is not permitted for path {}",
requested_source.display()
)
);
assert_sandbox_denied(&error);
assert!(!requested_destination.exists());
Ok(())
@@ -776,6 +736,7 @@ async fn file_system_copy_rejects_copying_directory_into_descendant(
&absolute_path(source_dir.clone()),
&absolute_path(source_dir.join("nested").join("copy")),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await;
let error = match error {
@@ -810,6 +771,7 @@ async fn file_system_copy_preserves_symlinks_in_recursive_copy(use_remote: bool)
&absolute_path(source_dir),
&absolute_path(copied_dir.clone()),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -855,6 +817,7 @@ async fn file_system_copy_ignores_unknown_special_files_in_recursive_copy(
&absolute_path(source_dir),
&absolute_path(copied_dir.clone()),
CopyOptions { recursive: true },
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
@@ -891,6 +854,7 @@ async fn file_system_copy_rejects_standalone_fifo_source(use_remote: bool) -> Re
&absolute_path(fifo_path),
&absolute_path(tmp.path().join("copied")),
CopyOptions { recursive: false },
/*sandbox*/ None,
)
.await;
let error = match error {