Files
codex/codex-rs/exec-server/src/regular_file.rs
pakrym-oai e3e5ad2847 Harden unsandboxed patch filesystem access (#39659)
## Why

An `apply_patch` path can be replaced with a symlink after verification, allowing an unsandboxed patch operation to reach a different file than the one that was approved.

## What changed

- Add `follow_symlinks` options to executor filesystem reads, writes, metadata lookups, directory creation, and removal, including the corresponding `followSymlinks` protocol fields.
- Implement no-follow filesystem operations on Unix and Windows that reject links in any path component and restrict file access to regular files.
- Run `apply_patch` with symlink traversal disabled when an otherwise-required sandbox is bypassed, while retaining the existing follow-symlink default for standalone callers.

## Testing

- Cover leaf and ancestor symlinks across patch add, update, delete, and move operations, including a path swap after verification.
- Exercise local and remote no-follow filesystem behavior, concurrent directory creation, special-file rejection, and Windows reparse points.

GitOrigin-RevId: 43fd479084891493ce13564fbd894b98f329c6dd
2026-08-20 08:10:08 +00:00

83 lines
2.4 KiB
Rust

use std::io;
use std::path::Path;
use tokio::io::AsyncReadExt;
pub(crate) async fn open(path: &Path) -> io::Result<tokio::fs::File> {
let mut options = tokio::fs::OpenOptions::new();
options.read(true);
configure_open(&mut options);
let file = options.open(path).await?;
if !is_disk_file(&file) || !file.metadata().await?.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("path `{}` is not a file", path.display()),
));
}
Ok(file)
}
/// Reads a regular UTF-8 file without following a symlink at its final path component.
pub async fn read_sensitive_file_to_string(path: &Path) -> io::Result<String> {
let mut options = tokio::fs::OpenOptions::new();
options.read(true);
configure_open(&mut options);
#[cfg(unix)]
options.custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW);
#[cfg(windows)]
{
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
let mut file = options.open(path).await?;
let metadata = file.metadata().await?;
if !is_disk_file(&file) || !metadata.is_file() || metadata.file_type().is_symlink() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("path `{}` is not a regular file", path.display()),
));
}
let mut contents = String::new();
file.read_to_string(&mut contents).await?;
Ok(contents)
}
#[cfg(unix)]
fn configure_open(options: &mut tokio::fs::OpenOptions) {
options.custom_flags(libc::O_NONBLOCK);
}
#[cfg(windows)]
fn configure_open(options: &mut tokio::fs::OpenOptions) {
use windows_sys::Win32::Storage::FileSystem::SECURITY_IDENTIFICATION;
options.security_qos_flags(SECURITY_IDENTIFICATION);
}
#[cfg(not(any(unix, windows)))]
fn configure_open(_options: &mut tokio::fs::OpenOptions) {}
#[cfg(windows)]
pub(crate) fn is_disk_file(file: &impl std::os::windows::io::AsRawHandle) -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Storage::FileSystem::FILE_TYPE_DISK;
use windows_sys::Win32::Storage::FileSystem::GetFileType;
// SAFETY: `file` owns this handle for the duration of the call.
unsafe { GetFileType(file.as_raw_handle() as HANDLE) == FILE_TYPE_DISK }
}
#[cfg(not(windows))]
fn is_disk_file(_file: &tokio::fs::File) -> bool {
true
}
#[cfg(test)]
#[path = "regular_file_tests.rs"]
mod tests;