Skip to content

Commit de554dd

Browse files
AkshatOPaduh95
authored andcommitted
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: #64168 Signed-off-by: AkshatOP <hunterdevil0987@gmail.com> PR-URL: #64149 Fixes: #64148 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent c770d35 commit de554dd

2 files changed

Lines changed: 89 additions & 45 deletions

File tree

lib/internal/vfs/providers/memory.js

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ArrayFrom,
5+
ArrayPrototypePop,
56
ArrayPrototypePush,
67
DateNow,
78
SafeMap,
@@ -613,59 +614,73 @@ class MemoryProvider extends VirtualProvider {
613614
*/
614615
#readdirRecursive(dirEntry, dirPath, withFileTypes) {
615616
const results = [];
617+
// Directories on the current traversal path. A directory reached again
618+
// through a symlink cycle is not descended into (but is still listed).
616619
const active = new SafeSet();
617620

618-
const walk = (entry, currentPath, relativePath) => {
619-
if (active.has(entry)) {
620-
return;
621+
// Traverse depth-first with an explicit stack instead of recursion, so a
622+
// deeply nested tree cannot exhaust the call stack. Each frame is a
623+
// directory being walked together with a snapshot of its children and the
624+
// index of the next child to visit.
625+
const enter = (entry, currentPath, relativePath) => {
626+
this.#ensurePopulated(entry, currentPath);
627+
active.add(entry);
628+
ArrayPrototypePush(stack, {
629+
entry,
630+
currentPath,
631+
relativePath,
632+
children: ArrayFrom(entry.children),
633+
index: 0,
634+
});
635+
};
636+
637+
const stack = [];
638+
enter(dirEntry, dirPath, '');
639+
640+
while (stack.length > 0) {
641+
const frame = stack[stack.length - 1];
642+
if (frame.index >= frame.children.length) {
643+
active.delete(frame.entry);
644+
ArrayPrototypePop(stack);
645+
continue;
621646
}
622647

623-
active.add(entry);
624-
try {
625-
this.#ensurePopulated(entry, currentPath);
626-
627-
for (const { 0: name, 1: childEntry } of entry.children) {
628-
const childRelative = relativePath ?
629-
relativePath + '/' + name : name;
630-
631-
if (withFileTypes) {
632-
let type;
633-
if (childEntry.isSymbolicLink()) {
634-
type = UV_DIRENT_LINK;
635-
} else if (childEntry.isDirectory()) {
636-
type = UV_DIRENT_DIR;
637-
} else {
638-
type = UV_DIRENT_FILE;
639-
}
640-
ArrayPrototypePush(results,
641-
new Dirent(childRelative, type, dirPath));
642-
} else {
643-
ArrayPrototypePush(results, childRelative);
644-
}
648+
const { 0: name, 1: childEntry } = frame.children[frame.index++];
649+
const childRelative = frame.relativePath ?
650+
frame.relativePath + '/' + name : name;
645651

646-
// Follow symlinks to directories for recursive traversal.
647-
// Track the active traversal path to avoid symlink cycles.
648-
let resolvedChild = childEntry;
649-
if (childEntry.isSymbolicLink()) {
650-
const targetPath = this.#resolveSymlinkTarget(
651-
pathPosix.join(currentPath, name), childEntry.target,
652-
);
653-
const result = this.#lookupEntry(targetPath, true, 0);
654-
if (result.entry) {
655-
resolvedChild = result.entry;
656-
}
657-
}
658-
if (resolvedChild.isDirectory()) {
659-
const childPath = pathPosix.join(currentPath, name);
660-
walk(resolvedChild, childPath, childRelative);
661-
}
652+
if (withFileTypes) {
653+
let type;
654+
if (childEntry.isSymbolicLink()) {
655+
type = UV_DIRENT_LINK;
656+
} else if (childEntry.isDirectory()) {
657+
type = UV_DIRENT_DIR;
658+
} else {
659+
type = UV_DIRENT_FILE;
662660
}
663-
} finally {
664-
active.delete(entry);
661+
ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath));
662+
} else {
663+
ArrayPrototypePush(results, childRelative);
665664
}
666-
};
667665

668-
walk(dirEntry, dirPath, '');
666+
// Follow symlinks to directories for recursive traversal, skipping any
667+
// directory already on the active path to avoid symlink cycles.
668+
let resolvedChild = childEntry;
669+
if (childEntry.isSymbolicLink()) {
670+
const targetPath = this.#resolveSymlinkTarget(
671+
pathPosix.join(frame.currentPath, name), childEntry.target,
672+
);
673+
const result = this.#lookupEntry(targetPath, true, 0);
674+
if (result.entry) {
675+
resolvedChild = result.entry;
676+
}
677+
}
678+
if (resolvedChild.isDirectory() && !active.has(resolvedChild)) {
679+
enter(resolvedChild, pathPosix.join(frame.currentPath, name),
680+
childRelative);
681+
}
682+
}
683+
669684
return results;
670685
}
671686

test/parallel/test-vfs-readdir-symlink-recursive.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,32 @@ assert.ok(
105105
assert.ok(dirents.some((d) => d.name === 'sub' && d.isDirectory()));
106106
assert.ok(dirents.some((d) => d.name === 'lnk' && d.isSymbolicLink()));
107107
}
108+
109+
// Recursive readdir on a deeply nested tree must not exhaust the call stack.
110+
// The iterative traversal introduced in this fix handles arbitrarily deep trees
111+
// without recursion, so this should complete without a RangeError.
112+
{
113+
const DEPTH = 1000;
114+
const v = vfs.create();
115+
116+
// Build /deep/0/1/2/.../999/leaf.txt
117+
let path = '/deep';
118+
v.mkdirSync(path);
119+
for (let i = 0; i < DEPTH; i++) {
120+
path += `/${i}`;
121+
v.mkdirSync(path);
122+
}
123+
v.writeFileSync(`${path}/leaf.txt`, 'deep');
124+
125+
const entries = v.readdirSync('/deep', { recursive: true });
126+
127+
// Every intermediate directory and the leaf file must appear.
128+
assert.strictEqual(entries.length, DEPTH + 1);
129+
130+
// Build the expected relative path to the leaf file.
131+
const expectedLeaf = Array.from({ length: DEPTH }, (_, i) => i).join('/') + '/leaf.txt';
132+
assert.ok(
133+
entries.includes(expectedLeaf),
134+
`Expected '${expectedLeaf}' in deep-tree entries`,
135+
);
136+
}

0 commit comments

Comments
 (0)