Repository navigation
fs.glob: return in cache-seen check aborts sibling processing, makes test-fs-glob.mjs flaky #62897
Description
Activity
- addedconfirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.fsIssues and PRs related to file-system APIs and the fs module.Issues and PRs related to file-system APIs and the fs module.
on Apr 22, 2026 Confirmed at HEAD
acb1bd7107b8c1f26cfb4dd41e2a0cebd64bbfb0.Sync bug site at
lib/internal/fs/glob.js:438-440; async atlib/internal/fs/glob.js:648-650. Both have the same shape: thereturnsits insidefor (const index of pattern.indexes), which itself sits insidefor (let i = 0; i < children.length; i++), so it exits the whole method and aborts the remaining children iteration.Your claim that the top-of-method
cache.addguard already prevents reprocessing checks out atglob.js:358-362andglob.js:559-563for the two methods.One wrinkle worth surfacing on delete vs. replacing
returnwithcontinue: the inner check readsthis.#cache.seen(entryPath, pattern, index)(the child path), while the top-of-methodthis.#cache.add(path, pattern)keys on the parent path. Deletion relies on the recursion eventually calling#addSubpatterns(entryPath, ...)and short-circuiting there via the top guard.continuewould instead skip the current (child, index) combo directly, which is a strictly tighter optimization over what deletion's recursion-dedup produces. Both land the fix;continueis the smaller diff and probably matches the original author's intent (an inner-loop skip that was typoed as a method-exit).Happy to take the Node side if it's useful, otherwise looking forward to your PR.
Thanks for confirming @truffle-dev, looks like there is already a PR open at #62901.
Reacted by TruffleThanks @bartlomieju. #62901 takes the delete path; the top-of-method
cache.addcarries the dedup. Watching it through.Hi, I'd like to work on this issue. The existing PR #62901 appears to be stalled (failing CI, no recent activity) — I've asked the author there if they plan to continue. If there's no response in about a week, I'll open a PR with the fix plus a regression test. Please let me know if anyone is already actively on this.
- added a commit that references this issue
on Aug 21, 2026 - added a commit that references this issue
on Aug 23, 2026 - added 2 commits that reference this issue
on Sep 7, 2026
Version
Reproduced on Node.js v25.x (current main), present since the glob implementation was added.
Platform
Subsystem
fs
What steps will reproduce the bug?
The bug is in lib/internal/fs/glob.js, in both #addSubpatterns and #iterateSubpatterns. The cache.seen check inside the children iteration loop uses return, which exits the entire method instead of skipping just the current child:
This can be triggered with */../ patterns. Minimal reproduction:
On Linux with ext4/tmpfs, this frequently fails with a/c or other entries missing. On macOS (APFS), readdir ordering is more stable so it may require more iterations or a different directory structure.
How often does it reproduce? Is there a required condition?
Depends on readdir ordering, which varies by filesystem. On Linux CI (ext4/tmpfs) it reproduces frequently. The required condition is a directory tree deep enough that the .. handler in the GLOBSTAR branch queues a path that was already queued by a parent directory's processing.
What is the expected behavior? Why is that the expected behavior?
globSync('a/**/../*', { cwd })should return all entries reachable via**/../*regardless of readdir ordering. Thecache.seencheck should skip only the already-processed child, not abort processing of all remaining siblings.What do you see instead?
Depending on readdir ordering, some entries in the parent directory are missing from the results. The
test-fs-glob.mjstest case fora/!(symlink)/**/../*is affected -- a/c and a/symlink can be missing.Additional information
I discovered this in Deno, due to flakiness when running
test-fs-glob.mjs.Root cause
When a pattern contains
**/../*:a/cwith GLOBSTAR queues three items via the..handler and #addSubpattern:a/cwith{*},awith{*}, anda/c/dwith{**}a/c/dis popped first. Its childc(directory) triggers the..handler again, which addsa/cwith{*}to subpatterns a second timea/c {*}gets pushed onto the queue after a{*}, so LIFO pops it before a{*}a/c {*}is processed, adding*tocache["a/c"]{*}is finally processed and iterates children ofa/, childchasentryPath = "a/c"which is now in the cache.cache.seenreturns true and return exits the method, skipping all remaining siblings (e.g.,a/x,a/z,a/symlink)Which siblings are skipped depends on readdir ordering, hence the flakiness.
Suggested fix
Remove the cache.seen check from the children iteration loop entirely:
The cache.add call at the top of #addSubpatterns/#iterateSubpatterns already prevents reprocessing and infinite recursion.
The children-level check is a redundant optimization that incorrectly prevents entries from being added to results when they
were previously traversed from a different context (e.g., a/b was recursed into via its own queue entry, but still needs to
appear as a result when the parent a matches * against its children).
This needs to be applied in both #addSubpatterns (sync) and #iterateSubpatterns (async).
Fix PR in Deno: denoland/deno#33372
I used AI to generate this report and the fix in Deno.