Skip to content

fix: prevent out of memory crash on cyclic symbolic links - #1396

Open
superphosphate wants to merge 3 commits into
bash-lsp:mainfrom
superphosphate:fix/cyclic-symlink-oom
Open

fix: prevent out of memory crash on cyclic symbolic links#1396
superphosphate wants to merge 3 commits into
bash-lsp:mainfrom
superphosphate:fix/cyclic-symlink-oom

Conversation

@superphosphate

Copy link
Copy Markdown

Summary

fast-glob follows symbolic links while walking the workspace (followSymbolicLinks: true). A repository that contains a cyclic symbolic link (for example fwupd's src/tests/sys) therefore makes the server walk the same directory over and over again until Node runs out of memory:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

This adds a fast-glob file system adapter that records the real path of every directory that is read and returns an empty directory when the same real path is encountered again. Symbolic links keep working, cycles are broken.

Related issues

fixes #1235

What changed

  • server/src/util/fs.ts: getFilePaths now passes a cycle-safe fs adapter to fast-glob.
  • server/src/util/__tests__/fs.test.ts: new tests for glob matching, following symbolic links, cyclic symbolic links and the maxItems limit.

Verification

  • pnpm exec tsc -b passes.
  • pnpm exec jest --runInBand server/src/util/__tests__/fs.test.ts: 4/4 pass.
  • Without the fix, the does not follow cyclic symbolic links test fails with paths such as loop/loop/loop/.../script.sh, which is exactly the runaway behaviour from the issue.
  • pnpm exec eslint server/src/util/fs.ts server/src/util/__tests__/fs.test.ts passes.
  • Full suite: 142 passed (4 more than the 138 passed on a clean checkout). The 7 failing suites (server.test.ts, analyzer.test.ts, sourcing, sh, shfmt, executables, shellcheck) fail identically on a clean checkout in this environment, so they are unrelated to this change.

Notes

  • Directories reachable through more than one link are now read only once, which also avoids doing the same work twice.
  • The test creates the cycle with a junction on Windows (no administrator rights required) and a directory symlink on other platforms.

fast-glob follows symbolic links while walking the workspace, so a cyclic
symbolic link made the language server read the same directory over and
over again until the process ran out of memory. Read every directory by its
real path only once, which breaks cycles while keeping symbolic links
working.
Copilot AI lite review requested due to automatic review settings September 9, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The cycle-breaking strategy can silently omit matches for certain glob patterns when the same real directory is reachable via multiple non-cyclic symlink paths, and the intended semantics should be clarified/adjusted.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR prevents an out-of-memory crash during workspace globbing when the repository contains cyclic symbolic links by adding cycle detection to the fast-glob filesystem adapter used by getFilePaths.

Changes:

  • Added a cycle-safe fast-glob filesystem adapter that tracks visited directories by realpath and stops traversing repeated realpaths.
  • Wired the adapter into getFilePaths while keeping followSymbolicLinks: true.
  • Added Jest tests covering basic glob matching, following symlinks, cyclic symlinks, and the maxItems limit.
File summaries
File Description
server/src/util/fs.ts Adds a cycle-safe fast-glob FS adapter and uses it in getFilePaths to prevent infinite traversal via symlink cycles.
server/src/util/tests/fs.test.ts Adds regression tests for symlink traversal behavior (including cycles) and maxItems limiting.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread server/src/util/fs.ts
Comment on lines +43 to +47
fs.realpath(directoryPath, (realPathError, realPath) => {
if (realPathError == null && !isFirstReadOf(realPath)) {
done(null, [])
return
}
Skipping every already-read real path also dropped matches for the same
directory reachable through several distinct symbolic links. Compare the
real path with the real paths of the ancestor directories instead, so cycles
are still broken while `{a,b}/**` matches both prefixes.
@superphosphate

Copy link
Copy Markdown
Author

Addressed the review comment in e6e9f64.

The adapter now skips a directory only when its real path equals the real path of one of its ancestors, instead of skipping every already-read real path. That still breaks cycles (the walk can only descend, so an infinite walk has to revisit an ancestor) but no longer drops matches for the same directory reachable through several distinct symbolic links, for example {a,b}/** when both a and b point at the same target.

Added two regression tests:

  • follows several symbolic links to the same directory (the {a,b}/** case from the review)
  • does not follow cyclic symbolic links to an ancestor

Verification: tsc -b passes, 6/6 fs tests pass, eslint passes.

@skovhus

skovhus commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Feels odd that we need to do a fix on our side, did you investigate if there are any fast-glob issues related to this or workarounds? Also fine to switch library if there are better options now.

Point at mrmlnc/fast-glob#74 in the adapter doc so the reason for the
cycle guard (and why the upstream workarounds were not used) is recorded
in the code.
@superphosphate

superphosphate commented Sep 10, 2026

Copy link
Copy Markdown
Author

@skovhus good question — I checked before hand-rolling the adapter.

So the adapter keeps followSymbolicLinks: true and only skips a directory whose real path matches one of its ancestors — the minimal condition that breaks a cycle, since the walk only descends. I've added a pointer to the fast-glob issue in the code comment (ade07f6).

If you'd prefer migrating to tinyglobby (or the built-in fs.glob once Node 20 is dropped), I'm happy to open a follow-up PR for that instead.

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.

OOM error from cyclic symlinks in repo

3 participants