Skip to content

feat(allowedpaths): support cross-root symlinks#169

Merged
matt-dz merged 9 commits intomainfrom
matt-dz/cross-root-symlinks
Apr 2, 2026
Merged

feat(allowedpaths): support cross-root symlinks#169
matt-dz merged 9 commits intomainfrom
matt-dz/cross-root-symlinks

Conversation

@matt-dz
Copy link
Copy Markdown
Collaborator

@matt-dz matt-dz commented Apr 2, 2026

Summary

  • When a symlink in one AllowedPaths root points to a file/directory in another allowed root, the read now succeeds instead of failing with "path escapes from parent"
  • On os.Root escape errors, the sandbox walks path components to find the escaping symlink, resolves its target, and retries through the matching allowed root
  • Handles both leaf symlinks and intermediate directory symlinks
  • Circular symlinks are detected and rejected after 10 hops

Closes #167

Test plan

🤖 Generated with Claude Code

…ifferent allowed root

When an os.Root operation fails with "path escapes from parent", the
sandbox now walks the relative path component by component to find the
escaping symlink, reads its target, resolves it to an absolute path,
and retries through whichever allowed root matches. This handles both
leaf symlinks (e.g. link.txt -> /other-root/file.txt) and intermediate
directory symlinks (e.g. pods/ -> /other-root/). A depth cap of 10 hops
prevents infinite loops from circular symlinks.

Closes #167

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt-dz
Copy link
Copy Markdown
Collaborator Author

matt-dz commented Apr 2, 2026

@codex conduct a comprehensive security and code review. beware that TOCTOU vulnerabilities are not a large deal because if a user can swap symlinks they likely already have root access to the system.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3988c28080

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread allowedpaths/sandbox.go
Comment thread allowedpaths/sandbox.go Outdated
Comment on lines +427 to +430
if err != nil && isPathEscapeError(err) {
if r, rel, ok := s.resolveFollowingSymlinks(absPath); ok {
f, err = r.Open(rel)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep loop detection working for Windows cross-root directories

This new fallback makes cross-root directory symlinks traversable, but Windows loop detection still depends on allowedpaths.FileIdentity, which uses sandbox.resolve(absPath) and cannot resolve those same cross-root symlink paths. In find -L, that drops to path-string cycle checks (builtins/find/find.go), which do not catch A/link -> B and B/link -> A because each descent has a new path, so traversal can grow until timeout instead of terminating on loop detection.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Opus 4.6] Valid concern, but this is a pre-existing limitation in FileIdentity on Windows, not introduced by this PR. The fix would be to update portable_windows.go:FileIdentity to fall back to resolveFollowingSymlinks when resolve fails, but that is Windows-only code we cannot test on this macOS machine. Recommend a follow-up PR with Windows CI coverage.

matt-dz and others added 2 commits April 2, 2026 12:03
… findings

- resolveFollowingSymlinks now delegates to resolveRootFollowingSymlinks
  instead of duplicating the component-walking logic
- Remove unused loop variable (for i, comp -> for i := range)
- Fix double negative in test assertion (assert.False x != 0 -> assert.Zero)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When Lstat fails on a component during symlink resolution (e.g. the
target file doesn't exist), return the current root and rel instead of
false. This lets the caller retry the operation and get the real error
(ENOENT) instead of the original "path escapes from parent".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt-dz
Copy link
Copy Markdown
Collaborator Author

matt-dz commented Apr 2, 2026

@codex conduct a comprehensive security and code review. beware that TOCTOU vulnerabilities are not a large deal because if a user can swap symlinks they likely already have root access to the system. additionally, only focus on reviewing issues on this PR, if there is a pre-existing issue, now is not the time to bring it up.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: feab3b26cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread allowedpaths/sandbox.go Outdated
The loop needs N+1 iterations for N symlink hops: N iterations to
resolve each symlink, plus 1 final iteration to confirm the resolved
path has no more symlinks and return success.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt-dz
Copy link
Copy Markdown
Collaborator Author

matt-dz commented Apr 2, 2026

@codex conduct a comprehensive security and code review. beware that TOCTOU vulnerabilities are not a large deal because if a user can swap symlinks they likely already have root access to the system. additionally, only focus on reviewing issues on this PR, if there is a pre-existing issue, now is not the time to bring it up.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc717470d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread allowedpaths/sandbox.go Outdated
Comment thread allowedpaths/sandbox.go Outdated
…llback

resolveRootFollowingSymlinks now accepts a preserveLast flag. When true,
the final path component is not resolved even if it is a symlink. Lstat
and Readlink pass true so they operate on the symlink itself rather than
its target. All other callers pass false for full resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt-dz
Copy link
Copy Markdown
Collaborator Author

matt-dz commented Apr 2, 2026

@codex conduct a comprehensive security and code review. beware that TOCTOU vulnerabilities are not a large deal because if a user can swap symlinks they likely already have root access to the system. additionally, only focus on reviewing issues on this PR, if there is a pre-existing issue, now is not the time to bring it up.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f891cfcee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread allowedpaths/sandbox.go
filepath.Dir on a trailing-slash path (e.g. "/dir/link/") strips only
the slash, producing the wrong parent for relative symlink resolution.
Clean absPath at entry to resolveRootFollowingSymlinks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt-dz
Copy link
Copy Markdown
Collaborator Author

matt-dz commented Apr 2, 2026

@codex conduct a comprehensive security and code review. beware that TOCTOU vulnerabilities are not a large deal because if a user can swap symlinks they likely already have root access to the system. additionally, only focus on reviewing issues on this PR, if there is a pre-existing issue, now is not the time to bring it up.

@chatgpt-codex-connector
Copy link
Copy Markdown

Security & Code Review (PR: feat(allowedpaths): support cross-root symlinks)

I reviewed the current PR state (including the follow-up fixes through b7c9fbf) and focused only on issues introduced by this PR’s changes.

Findings

1) P2 – Trailing-slash semantics regression in Lstat/Readlink cross-root fallback

When cross-root fallback is triggered, resolveRootFollowingSymlinks unconditionally normalizes the input with filepath.Clean(absPath), which strips trailing separators. That changes path semantics for Lstat and Readlink in cases where trailing slash should force directory interpretation (and commonly return ENOTDIR). allowedpaths/sandbox.goL139-L143

Lstat/Readlink now call that fallback with preserveLast=true, but after trailing slash removal they can incorrectly succeed on a terminal symlink path that should fail when called as .../link/. allowedpaths/sandbox.goL588-L600 allowedpaths/sandbox.goL615-L627

Why this is PR-scoped:
This behavior was introduced by the new cross-root resolution/fallback path and then reinforced by the trailing-slash cleanup fix in this PR series.

Suggested fix direction:
Preserve whether the original input had a trailing separator for operations where that matters (Lstat, Readlink), and enforce that semantics after cross-root root/rel resolution (or avoid stripping it before semantics checks).


I did not flag TOCTOU-style concerns per your guidance, and I did not raise unrelated pre-existing issues.


Checks run

  • go test ./allowedpaths -run CrossRoot -count=1
  • go test ./tests -run 'TestShellScenarios/(cross_root_symlink_read|cross_root_symlink_dir|cross_root_symlink_outside_blocked)' -count=1
  • go run /tmp/repro_lstat.go (custom repro: sb.Lstat("bridge/leaf.txt/", dir2) returned success + symlink mode)
  • go run /tmp/repro_native.go (baseline: native os.Lstat(".../l/") and os.Readlink(".../l/") return not a directory)

View task →

@matt-dz
Copy link
Copy Markdown
Collaborator Author

matt-dz commented Apr 2, 2026

@codex conduct a comprehensive security and code review. beware that TOCTOU vulnerabilities are not a large deal because if a user can swap symlinks they likely already have root access to the system. additionally, only focus on reviewing issues on this PR, if there is a pre-existing issue, now is not the time to bring it up.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@matt-dz matt-dz marked this pull request as ready for review April 2, 2026 18:33
Comment thread allowedpaths/sandbox.go
Comment thread allowedpaths/sandbox.go
Comment thread allowedpaths/sandbox.go
const (
modeRead = 0x04
modeWrite = 0x02
modeExecute = 0x01
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: are there constants for these in fs or another package?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Opus 4.6] Checked — Go does not export POSIX R_OK/W_OK/X_OK constants. syscall has them only in test files as unexported _R_OK etc. Our private modeRead/modeWrite/modeExecute constants are the idiomatic approach.

Comment thread allowedpaths/sandbox.go Outdated
matt-dz and others added 2 commits April 2, 2026 15:32
- Add TestIsPathEscapeError regression test to catch Go stdlib changes
- Extract openWithSymlinkFallback helper to deduplicate the repeated
  escape-check-and-retry pattern in readDirN, OpenDir, IsDirEmpty,
  and ReadDirLimited

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…function

Remove the duplicate resolve that returned *os.Root. The single resolve
now returns *root, and callers access .root where they need the os.Root
handle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FileIdentity in portable_windows.go used the old resolve() that
returned *os.Root. Updated to use .root on the returned *root entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt-dz matt-dz added this pull request to the merge queue Apr 2, 2026
Merged via the queue into main with commit 735a97d Apr 2, 2026
34 of 35 checks passed
@matt-dz matt-dz deleted the matt-dz/cross-root-symlinks branch April 2, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(allowedpaths): support symlinks whose targets resolve within a different allowed-path root

2 participants