Skip to content

Fix symlink sandbox escape in fs path resolver (#47)#57

Draft
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-AnGpM
Draft

Fix symlink sandbox escape in fs path resolver (#47)#57
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-AnGpM

Conversation

@TYRMars
Copy link
Copy Markdown
Owner

@TYRMars TYRMars commented May 30, 2026

Summary

Fixes #47.

resolve_under (the shared path sandbox used by fs.*, code.grep, and fs.patch) was purely lexical: it rejected absolute paths and .. components but never resolved symlinks. A symlink that lives inside JARVIS_FS_ROOT but points outside it was followed transparently, escaping the sandbox. Because the read-side tools (fs.read / fs.list / code.grep) are not approval-gated, this was a no-prompt host-file read path (e.g. an attacker-planted link -> /etc in a dependency checkout the agent merely reads).

Fix

After the existing lexical checks and root.join(rel):

  • Canonicalize the root and the candidate to resolve any symlinks.
  • The target may not exist yet (fs.write creating a new file), so canonicalize the longest existing prefix and re-append the not-yet-created tail — those trailing components don't exist on disk and therefore can't be symlinks.
  • Reject when the resolved path no longer starts_with the canonical root.
  • On success, return the lexical join so callers' write targets are byte-for-byte unchanged (avoids surprising /tmp/private/tmp rewrites on macOS).

This matches the canonicalization already done by shell.rs / workspace.rs.

Tests

Added to sandbox.rs:

  • rejects_symlink_inside_root_pointing_outside — symlink → existing file, → not-yet-existing path, and the symlink dir itself all rejected.
  • accepts_symlink_inside_root_pointing_inside — intra-root symlink still works.
  • accepts_real_nested_path_under_existing_root — sanity for the canonicalize path.

cargo test -p harness-tools sandbox:: and cargo clippy -p harness-tools --all-targets -- -D warnings are green.

Note: 5 unrelated memory_sync / memory_include tests fail in this environment because they run git commit, which the container's commit-signing hook rejects with HTTP 400 — independent of this change.


Generated by Claude Code

resolve_under was purely lexical: it rejected absolute paths and ".."
components but never resolved symlinks, so a symlink living inside
JARVIS_FS_ROOT but pointing outside it was followed transparently.
Because the read-side tools (fs.read / fs.list / code.grep) are not
approval-gated, this was a no-prompt host-file read path.

Canonicalize both the root and the candidate's longest existing prefix
(re-appending any not-yet-created tail, which cannot be a symlink) and
reject paths that no longer start with the canonical root. Returns the
lexical join on success so callers' write targets are unchanged.

Fixes #47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fs sandbox resolve_under is purely lexical — symlinks inside the root escape JARVIS_FS_ROOT via ungated fs.read/code.grep

2 participants