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.
MemoryProvider#readdirSync with `recursive: true` follows symlinks to directories during traversal. The traversal was recursive and unbounded, so a circular symlink caused unbounded recursion in the internal walk() helper until the call stack was exhausted, crashing the process with `RangeError: Maximum call stack size exceeded`. Both the synchronous and promise-based variants were affected. Rewrite the traversal to be iterative, using an explicit queue instead of recursion so deeply nested trees can no longer exhaust the call stack. Each queued directory carries the number of symlink hops taken to reach it; a directory reached by following a symlink is not enqueued once that count would exceed kMaxSymlinkDepth, mirroring the ELOOP guard in #lookupEntry and the real filesystem, which follows directory symlinks until the OS symlink limit is reached. Entries are still listed, so non-circular symlinks continue to be followed as before. Fixes: nodejs#64148 Signed-off-by: AkshatOP <hunterdevil0987@gmail.com>
62229c0 to
2e4e1be
Compare
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