From fa8bd083feb50a024609ec818a66bdf1e6f0b02d Mon Sep 17 00:00:00 2001 From: colby-oai <228809017+colby-oai@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:25:01 -0400 Subject: [PATCH] support symlinks --- codex-rs/Cargo.lock | 1 - codex-rs/exec-server/Cargo.toml | 3 -- codex-rs/exec-server/src/fs_helper.rs | 35 ++++------------------ codex-rs/exec-server/tests/file_system.rs | 36 +++++++++++++++++++++++ 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 879541120e..eaee6ed294 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2621,7 +2621,6 @@ dependencies = [ "codex-utils-pty", "ctor 0.6.3", "futures", - "libc", "pretty_assertions", "reqwest", "serde", diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 8615713f51..4dc77f8552 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -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 } diff --git a/codex-rs/exec-server/src/fs_helper.rs b/codex-rs/exec-server/src/fs_helper.rs index faa9615cc6..97ab14da19 100644 --- a/codex-rs/exec-server/src/fs_helper.rs +++ b/codex-rs/exec-server/src/fs_helper.rs @@ -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> { 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 { - #[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::open(path).await } fn map_fs_error(err: io::Error) -> JSONRPCErrorError { diff --git a/codex-rs/exec-server/tests/file_system.rs b/codex-rs/exec-server/tests/file_system.rs index 6ca1772e51..be51b8c1d9 100644 --- a/codex-rs/exec-server/tests/file_system.rs +++ b/codex-rs/exec-server/tests/file_system.rs @@ -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::>() + .await + .into_iter() + .collect::>>()?; + assert_eq!(chunks.concat(), b"readable"); + + Ok(()) +} + #[test_case(false ; "local")] #[test_case(true ; "remote")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]