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
This commit is contained in:
pakrym-oai
2026-08-20 04:57:40 +00:00
committed by copyberry
parent 4e1a772a7d
commit e3e5ad2847
88 changed files with 2782 additions and 302 deletions

View File

@@ -361,6 +361,8 @@ pub struct TerminateResponse {
#[serde(rename_all = "camelCase")]
pub struct FsReadFileParams {
pub path: PathUri,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub follow_symlinks: Option<bool>,
pub sandbox: Option<FileSystemSandboxContext>,
}
@@ -414,6 +416,8 @@ pub struct FsCloseResponse {}
pub struct FsWriteFileParams {
pub path: PathUri,
pub data_base64: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub follow_symlinks: Option<bool>,
pub sandbox: Option<FileSystemSandboxContext>,
}
@@ -426,6 +430,8 @@ pub struct FsWriteFileResponse {}
pub struct FsCreateDirectoryParams {
pub path: PathUri,
pub recursive: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub follow_symlinks: Option<bool>,
pub sandbox: Option<FileSystemSandboxContext>,
/// Atomically restrict a newly created, non-recursive directory to its owner.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -440,6 +446,8 @@ pub struct FsCreateDirectoryResponse {}
#[serde(rename_all = "camelCase")]
pub struct FsGetMetadataParams {
pub path: PathUri,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub follow_symlinks: Option<bool>,
pub sandbox: Option<FileSystemSandboxContext>,
}
@@ -504,6 +512,8 @@ pub struct FsRemoveParams {
pub path: PathUri,
pub recursive: Option<bool>,
pub force: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub follow_symlinks: Option<bool>,
pub sandbox: Option<FileSystemSandboxContext>,
}