support symlinks

This commit is contained in:
colby-oai
2026-04-28 17:25:01 -04:00
parent 5055167b82
commit fa8bd083fe
4 changed files with 42 additions and 33 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2621,7 +2621,6 @@ dependencies = [
"codex-utils-pty",
"ctor 0.6.3",
"futures",
"libc",
"pretty_assertions",
"reqwest",
"serde",

View File

@@ -43,9 +43,6 @@ tokio-tungstenite = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
codex-test-binary-support = { workspace = true }

View File

@@ -3,7 +3,6 @@ use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use serde::Deserialize;
use serde::Serialize;
use std::path::Path;
use tokio::io;
use tokio::io::AsyncWrite;
@@ -203,9 +202,10 @@ pub(crate) async fn run_direct_request(
}))
}
FsHelperRequest::ReadFileInfo(params) => {
let metadata = tokio::fs::symlink_metadata(params.path.as_path())
let file = open_read_file(params.path.as_path())
.await
.map_err(map_fs_error)?;
let metadata = file.metadata().await.map_err(map_fs_error)?;
validate_read_file_metadata(params.path.as_path(), &metadata).map_err(map_fs_error)?;
Ok(FsHelperPayload::ReadFileInfo(FsReadFileInfoResponse {
file_size_bytes: metadata.len(),
@@ -307,9 +307,9 @@ pub(crate) async fn run_direct_stream_request(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match request {
FsHelperRequest::ReadFileStream(params) => {
let metadata = tokio::fs::symlink_metadata(params.path.as_path()).await?;
let mut file = open_read_file(params.path.as_path()).await?;
let metadata = file.metadata().await?;
validate_read_file_metadata(params.path.as_path(), &metadata)?;
let mut file = open_read_file_no_follow(params.path.as_path()).await?;
tokio::io::copy(&mut file, stdout).await?;
Ok(())
}
@@ -339,31 +339,8 @@ fn validate_read_file_metadata(
Ok(())
}
async fn open_read_file_no_follow(path: &Path) -> io::Result<tokio::fs::File> {
#[cfg(unix)]
{
tokio::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
.await
}
#[cfg(windows)]
{
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
tokio::fs::OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
.await
}
#[cfg(not(any(unix, windows)))]
{
tokio::fs::File::open(path).await
}
async fn open_read_file(path: &std::path::Path) -> io::Result<tokio::fs::File> {
tokio::fs::File::open(path).await
}
fn map_fs_error(err: io::Error) -> JSONRPCErrorError {

View File

@@ -722,6 +722,42 @@ async fn file_system_sandboxed_read_body_rejects_symlink_to_denied_file(
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_sandboxed_read_body_allows_symlink_to_readable_file(
use_remote: bool,
) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
let tmp = TempDir::new()?;
let allowed_dir = tmp.path().join("allowed");
let target_path = allowed_dir.join("target.csv");
let symlink_path = allowed_dir.join("report.csv");
std::fs::create_dir_all(&allowed_dir)?;
std::fs::write(&target_path, "readable")?;
symlink(&target_path, &symlink_path)?;
let sandbox = read_only_sandbox(allowed_dir);
let body = file_system
.read_file_body(&absolute_path(symlink_path), Some(&sandbox))
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(body.file_name, "report.csv");
assert_eq!(body.file_size_bytes, "readable".len() as u64);
let chunks = body
.stream
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<std::io::Result<Vec<_>>>()?;
assert_eq!(chunks.concat(), b"readable");
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]