From 56ef7a44eeb6548dda29f1199be702c3767cf1e0 Mon Sep 17 00:00:00 2001 From: AkshatOP Date: Sun, 5 Jul 2026 12:14:51 +0530 Subject: [PATCH] vfs: make recursive readdir iterative 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: https://github.com/nodejs/node/pull/64168 Signed-off-by: AkshatOP --- lib/internal/vfs/providers/memory.js | 105 +++++++++++++++------------ 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/lib/internal/vfs/providers/memory.js b/lib/internal/vfs/providers/memory.js index 995b8f236200d5..cb3b639259d9b4 100644 --- a/lib/internal/vfs/providers/memory.js +++ b/lib/internal/vfs/providers/memory.js @@ -2,6 +2,7 @@ const { ArrayFrom, + ArrayPrototypePop, ArrayPrototypePush, DateNow, SafeMap, @@ -612,59 +613,73 @@ class MemoryProvider extends VirtualProvider { */ #readdirRecursive(dirEntry, dirPath, withFileTypes) { const results = []; + // Directories on the current traversal path. A directory reached again + // through a symlink cycle is not descended into (but is still listed). const active = new SafeSet(); - const walk = (entry, currentPath, relativePath) => { - if (active.has(entry)) { - return; + // Traverse depth-first with an explicit stack instead of recursion, so a + // deeply nested tree cannot exhaust the call stack. Each frame is a + // directory being walked together with a snapshot of its children and the + // index of the next child to visit. + const enter = (entry, currentPath, relativePath) => { + this.#ensurePopulated(entry, currentPath); + active.add(entry); + ArrayPrototypePush(stack, { + entry, + currentPath, + relativePath, + children: ArrayFrom(entry.children), + index: 0, + }); + }; + + const stack = []; + enter(dirEntry, dirPath, ''); + + while (stack.length > 0) { + const frame = stack[stack.length - 1]; + if (frame.index >= frame.children.length) { + active.delete(frame.entry); + ArrayPrototypePop(stack); + continue; } - active.add(entry); - try { - this.#ensurePopulated(entry, currentPath); - - for (const { 0: name, 1: childEntry } of entry.children) { - const childRelative = relativePath ? - relativePath + '/' + name : name; - - if (withFileTypes) { - let type; - if (childEntry.isSymbolicLink()) { - type = UV_DIRENT_LINK; - } else if (childEntry.isDirectory()) { - type = UV_DIRENT_DIR; - } else { - type = UV_DIRENT_FILE; - } - ArrayPrototypePush(results, - new Dirent(childRelative, type, dirPath)); - } else { - ArrayPrototypePush(results, childRelative); - } + const { 0: name, 1: childEntry } = frame.children[frame.index++]; + const childRelative = frame.relativePath ? + frame.relativePath + '/' + name : name; - // Follow symlinks to directories for recursive traversal. - // Track the active traversal path to avoid symlink cycles. - let resolvedChild = childEntry; - if (childEntry.isSymbolicLink()) { - const targetPath = this.#resolveSymlinkTarget( - pathPosix.join(currentPath, name), childEntry.target, - ); - const result = this.#lookupEntry(targetPath, true, 0); - if (result.entry) { - resolvedChild = result.entry; - } - } - if (resolvedChild.isDirectory()) { - const childPath = pathPosix.join(currentPath, name); - walk(resolvedChild, childPath, childRelative); - } + if (withFileTypes) { + let type; + if (childEntry.isSymbolicLink()) { + type = UV_DIRENT_LINK; + } else if (childEntry.isDirectory()) { + type = UV_DIRENT_DIR; + } else { + type = UV_DIRENT_FILE; } - } finally { - active.delete(entry); + ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath)); + } else { + ArrayPrototypePush(results, childRelative); } - }; - walk(dirEntry, dirPath, ''); + // Follow symlinks to directories for recursive traversal, skipping any + // directory already on the active path to avoid symlink cycles. + let resolvedChild = childEntry; + if (childEntry.isSymbolicLink()) { + const targetPath = this.#resolveSymlinkTarget( + pathPosix.join(frame.currentPath, name), childEntry.target, + ); + const result = this.#lookupEntry(targetPath, true, 0); + if (result.entry) { + resolvedChild = result.entry; + } + } + if (resolvedChild.isDirectory() && !active.has(resolvedChild)) { + enter(resolvedChild, pathPosix.join(frame.currentPath, name), + childRelative); + } + } + return results; }