Skip to content

fix(DurableExecution): resolve symlinks at every path component in FileSystemSerializer containment check - #2563

Open
GarrettBeatty wants to merge 1 commit into
feature/durable-result-serializerfrom
fix/filesystemserializer-symlink-base
Open

fix(DurableExecution): resolve symlinks at every path component in FileSystemSerializer containment check#2563
GarrettBeatty wants to merge 1 commit into
feature/durable-result-serializerfrom
fix/filesystemserializer-symlink-base

Conversation

@GarrettBeatty

Copy link
Copy Markdown
Contributor

Addresses @jnunn-aws's review comment on #2561: #2561 (comment)

Problem

FileSystemSerializer.IsWithinBase runs both the base and the candidate through ResolveReal, but ResolveReal only followed a symlink on the leaf and its immediate parent. When the configured basePath is itself a symlink — e.g. an EFS mount exposed as /mnt/link -> /mnt/real — the two sides canonicalize asymmetrically:

  • Base /mnt/link — its leaf is the symlink, so it resolves to fullBase = /mnt/real/.
  • Candidate /mnt/link/<func>/<exec>/<inv>/<entity>.bin — the /mnt/link symlink sits several components above the leaf/parent, which ResolveReal never touched, so it stayed /mnt/link/....
  • fullPath.StartsWith(fullBase) is then false, so ValidateWriteDirWithinBase / ValidatePathWithinBase reject every offloaded write and read.

The macOS /var -> /private/var case in the code comment is safe only because that symlink sits above the base and is left unresolved symmetrically on both sides; a symlinked base leaf is not.

Fix

ResolveReal now follows a symlink at every existing component of the path (walking root → leaf, calling ResolveLinkTarget per segment). Base and candidate are canonicalized identically no matter where the symlink sits, so a symlinked mount root used as the base resolves the same way the candidate paths built under it do. Non-existent trailing components (a per-execution directory validated before it is created) can't be symlinks and are appended lexically. This also strengthens escape detection: a symlink planted at any interior component that points outside the base is now caught, not just one on the leaf/parent.

Test

Added SymlinkedBasePath_RoundTripsAndStaysContained: creates link -> real, constructs the serializer with the symlink as the base, and asserts a value round-trips and the payload physically lands under the real target. The test fails before this change (containment guard rejects the write) and passes after. It soft-skips where symlink creation requires elevation (Windows without Developer Mode).

All existing FileSystemSerializerTests pass (net10.0).

…leSystemSerializer containment check

IsWithinBase resolved the base path through ResolveReal, which only followed
a symlink on the leaf and its immediate parent. When the configured base path
itself is a symlink (e.g. an EFS mount exposed as /mnt/link -> /mnt/real), the
base resolved to /mnt/real while candidate paths built under /mnt/link kept the
unresolved symlink component (it sits above their leaf/parent). The resulting
StartsWith prefix check failed, so every offloaded read and write was rejected.

Resolve symlinks at every existing component of the path so the base and the
candidates are canonicalized identically regardless of where a symlink sits.
Add a test with a symlinked base leaf; it fails before this change.
@GarrettBeatty GarrettBeatty added the Release Not Needed Add this label if a PR does not need to be released. label Sep 4, 2026
@GarrettBeatty
GarrettBeatty marked this pull request as ready for review September 4, 2026 16:57
@GarrettBeatty
GarrettBeatty requested review from a team as code owners September 4, 2026 16:58
@GarrettBeatty
GarrettBeatty requested review from normj and removed request for a team September 4, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Release Not Needed Add this label if a PR does not need to be released.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants