Skip to content

fix: prevent infinite recursion from symlink cycles in workspace file…#1149

Closed
jasonjcwu wants to merge 2 commits intonesquena:masterfrom
jasonjcwu:fix/symlink-cycle-v2
Closed

fix: prevent infinite recursion from symlink cycles in workspace file…#1149
jasonjcwu wants to merge 2 commits intonesquena:masterfrom
jasonjcwu:fix/symlink-cycle-v2

Conversation

@jasonjcwu
Copy link
Copy Markdown
Contributor

… tree

When workspace contains symlinks pointing outside the workspace root (e.g. ln -s /root/obsidian ~/workspace/obsidian), the directory listing could recurse infinitely if the symlink target contains a symlink back to the workspace or its ancestors.

The original cycle detection only ran when the symlink target was inside the workspace root, missing the common case of external symlinks.

Changes:

  • Move cycle detection outside the workspace-relative check so it runs for all symlinks regardless of target location
  • Skip symlinks that point to the workspace root, the current directory, or any ancestor of the current directory
  • Properly type symlink entries with 'type: symlink' and 'is_dir' field so the frontend can render them correctly (folder icon + expand arrow)
  • Maintain workspace-relative paths for entries inside symlink targets so navigation continues to work across the workspace boundary

… tree

When workspace contains symlinks pointing outside the workspace root
(e.g. ln -s /root/obsidian ~/workspace/obsidian), the directory listing
could recurse infinitely if the symlink target contains a symlink back to
the workspace or its ancestors.

The original cycle detection only ran when the symlink target was inside
the workspace root, missing the common case of external symlinks.

Changes:
- Move cycle detection outside the workspace-relative check so it runs
  for all symlinks regardless of target location
- Skip symlinks that point to the workspace root, the current directory,
  or any ancestor of the current directory
- Properly type symlink entries with 'type: symlink' and 'is_dir' field
  so the frontend can render them correctly (folder icon + expand arrow)
- Maintain workspace-relative paths for entries inside symlink targets
  so navigation continues to work across the workspace boundary
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks for this fix, @fxd-jason! Symlink cycle detection in workspace file trees is a real footgun — this is a good catch.

The approach is correct:

  • Moving cycle detection outside the workspace-relative check so it fires for all symlinks (not just those pointing inside the workspace) addresses the root cause directly
  • Skipping symlinks pointing to workspace root, current dir, or any ancestor prevents the infinite recursion case with external symlinks (e.g. ln -s /root/obsidian ~/workspace/obsidian)
  • Adding type: symlink and is_dir to symlink entries so the frontend can render them correctly is a nice improvement on top of the bug fix

File changed:

  • api/workspace.py (+73/-11) — the symlink-aware directory listing logic

Suggestions for the test plan:
The PR doesn't mention automated tests covering this case. A test that creates a symlink cycle (or a symlink pointing outside the workspace root) and verifies the directory listing terminates would make this regression-proof going forward. Consider adding one in tests/ if not already covered.

Overall this looks like a solid fix for a real crash-inducing edge case. The scope is appropriately narrow — only api/workspace.py is touched.

7 test cases covering:
- External symlink dirs listed with correct type/is_dir/target
- Browsing into external symlink dirs via workspace-relative path
- Self-referencing symlinks filtered (ln -s . link)
- Ancestor symlinks filtered (ln -s .. link)
- Cycle symlinks inside symlink targets filtered
- Symlink file entries with size
- Path traversal still blocked with symlink support
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

The follow-up test commit is a great addition! The 7-case test suite in tests/ covering:

  • External symlink dirs listed with correct type/is_dir/target
  • Browsing into external symlink dirs via workspace-relative path
  • Self-referencing symlinks filtered (ln -s . link)
  • Ancestor symlinks filtered (ln -s .. link)
  • Cycle symlinks inside symlink targets filtered
  • Symlink file entries with size
  • Path traversal still blocked with symlink support

...addresses exactly the regression-proof coverage that was missing in the initial fix. This makes the cycle detection behavior locked against future regressions, which is important given how easy it is to accidentally revert ordering-sensitive detection logic.

The PR now covers both the fix and meaningful test coverage. Flagging for maintainer review and merge. 🙏

nesquena-hermes added a commit that referenced this pull request Apr 27, 2026
Merged as v0.50.227. 2634 tests passing, browser QA 21/21 (desktop + mobile). Full attribution below.

Thanks to all 12 contributors:
@jundev0001 (#1138), @franksong2702 (#1142, #1157, #1162), @dso2ng (#1143), @bergeouss (#1145, #1146, #1156, #1159), @jasonjcwu (#1149), @ccqqlo (#1161), @frap129 (#1165)

Two fixes applied during integration and two more by the independent reviewer (@nesquena):
- messages.js: per-turn cost delta capture order (#1159)
- workspace.py: symlink target blocked-roots check + HOME sanity guard (#1149, #1165)
- panels.js: cron unread counter bookkeeping (in-loop increment bug)
- tests/test_symlink_cycle_detection.py: register workspace before session/new
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Merged as v0.50.227 via batch PR #1168. Thank you @jasonjcwu! 🎉

JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 29, 2026
Merged as v0.50.227. 2634 tests passing, browser QA 21/21 (desktop + mobile). Full attribution below.

Thanks to all 12 contributors:
@jundev0001 (nesquena#1138), @franksong2702 (nesquena#1142, nesquena#1157, nesquena#1162), @dso2ng (nesquena#1143), @bergeouss (nesquena#1145, nesquena#1146, nesquena#1156, nesquena#1159), @jasonjcwu (nesquena#1149), @ccqqlo (nesquena#1161), @frap129 (nesquena#1165)

Two fixes applied during integration and two more by the independent reviewer (@nesquena):
- messages.js: per-turn cost delta capture order (nesquena#1159)
- workspace.py: symlink target blocked-roots check + HOME sanity guard (nesquena#1149, nesquena#1165)
- panels.js: cron unread counter bookkeeping (in-loop increment bug)
- tests/test_symlink_cycle_detection.py: register workspace before session/new
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.

3 participants