vfs: prevent stack overflow in recursive readdir on circular symlinks#64149
vfs: prevent stack overflow in recursive readdir on circular symlinks#64149AkshatOP wants to merge 1 commit into
Conversation
| const results = []; | ||
|
|
||
| const walk = (entry, currentPath, relativePath) => { | ||
| const walk = (entry, currentPath, relativePath, symlinkDepth) => { |
There was a problem hiding this comment.
Instead of keeping this recursive, can it be refactored to be iterative to avoid the risk entirely?
There was a problem hiding this comment.
alrightt... I switched #readdirRecursive to an iterative traversal with an explicit queue (same shape as fs.readdirSyncRecursive), so neither circular symlinks nor deeply nested trees can blow the call stack now.
I kept a per-entry symlink-hop counter bounded by kMaxSymlinkDepth, since the walk still follows directory symlinks and a cycle would otherwise grow the queue without end. It stops at the same point the real FS does (ELOOP) — the self-referential PoC returns the same 42 entries as fs.readdirSync(path, { recursive: true }). I went with the depth bound over a visited set to keep the output in parity with real fs, but happy to switch if you'd rather.
62229c0 to
2e4e1be
Compare
|
Thanks for the fix! I can confirm the iterative approach is the right call — |
|
friendly ping! anything else you'd like changed here? |
MemoryProvider recursive readdir walked the directory tree with a recursive helper. Rewrite it to traverse iteratively with an explicit stack so a deeply nested tree can no longer exhaust the call stack. The set of directories on the active traversal path is still tracked, so a circular symlink stops descending while its entry remains listed; the output and observable behavior are unchanged. Refs: nodejs#64168 Signed-off-by: AkshatOP <hunterdevil0987@gmail.com>
2e4e1be to
56ef7a4
Compare
|
Updated this PR per @jasnell's preference for an iterative walk (raised here and on #64168: "I'd much prefer to see if there's a way to make the walk iterative rather than recursive"). It's now a pure recursive→iterative refactor on top of the landed #64168 , #readdirRecursive uses an explicit stack, so deeply nested trees can't exhaust the call stack. The active-path cycle handling is unchanged, so all the tests from #64168 pass as-is and the output is identical (the self-referential symlink still returns ['dir','dir/loop','dir/nested.txt']). No behavior change, just no recursion. Happy to adjust anything. |
MemoryProvider#readdirSync with
recursive: truefollows symlinks to directories during traversal but did not bound the number of symlinks followed along a branch. A circular symlink therefore caused unbounded recursion in the internal walk() helper until the call stack was exhausted, crashing the process withRangeError: Maximum call stack size exceeded. Both the synchronous and promise-based variants were affected. The existing kMaxSymlinkDepth guard in #lookupEntry did not help, because walk() resolved each symlink target with a fresh depth of zero.Track the number of symlink hops along the current branch and stop recursing once it would exceed kMaxSymlinkDepth, mirroring the ELOOP guard in #lookupEntry and the behavior of the real filesystem, which follows directory symlinks until the OS symlink limit is reached. The entries themselves are still listed, so non-circular symlinks continue to be followed as before.
Fixes: #64148