Skip to content

node:vfs MemoryProvider: readdirSync({recursive:true}) crashes via circular symlinks (stack overflow) #64148

Description

@jojin1709

node:vfs MemoryProvider: readdirSync({recursive:true}) stack overflow via circular symlinks

Affected: node:vfs MemoryProvider (experimental)
Node version: v27.0.0-nightly20260625c5635b82c9
Flag required: --experimental-vfs

Summary

MemoryProvider#readdirSync with recursive: true crashes the Node.js process with a stack overflow (RangeError: Maximum call stack size exceeded) when the virtual filesystem contains circular symlinks. Both sync and async variants are affected. The process exits with code 1 when the error is uncaught.

Root Cause

The internal walk() function inside #readdirRecursive (memory.js ~line 643) follows symlinks to directories recursively but does not track visited paths or entries. This completely bypasses the kMaxSymlinkDepth = 40 guard that exists in #lookupEntry, causing unbounded recursion until the call stack is exhausted.

// walk() calls itself with no cycle detection:
if (resolvedChild.isDirectory()) {
  const childPath = pathPosix.join(currentPath, name);
  walk(resolvedChild, childPath, childRelative); // infinite if circular symlink
}

PoC 1 — Self-referential symlink

const vfs = require('node:vfs');
const mem = new vfs.MemoryProvider();
const sandbox = vfs.create(mem);

sandbox.mkdirSync('/dir');
sandbox.symlinkSync('/dir', '/dir/loop'); // /dir/loop -> /dir

// Crashes: RangeError: Maximum call stack size exceeded
sandbox.readdirSync('/', { recursive: true });

PoC 2 — Mutual circular chain (a -> b -> a)

const vfs = require('node:vfs');
const mem = new vfs.MemoryProvider();
const sandbox = vfs.create(mem);

sandbox.mkdirSync('/a');
sandbox.mkdirSync('/b');
sandbox.symlinkSync('/a', '/b/link_to_a');
sandbox.symlinkSync('/b', '/a/link_to_b');

// Also crashes
sandbox.readdirSync('/', { recursive: true });

PoC 3 — Async variant also affected

const vfs = require('node:vfs');
const mem = new vfs.MemoryProvider();
const sandbox = vfs.create(mem);

sandbox.mkdirSync('/dir');
sandbox.symlinkSync('/dir', '/dir/loop');

// Also crashes
sandbox.promises.readdir('/', { recursive: true })
  .catch(e => console.log(e.message)); // Maximum call stack size exceeded

Stack trace (uncaught)

node:path:1332

join(...args) {

^

RangeError: Maximum call stack size exceeded

at Object.join (node:path:1332:7)

at #lookupEntry (node:internal/vfs/providers/memory:315:31)

at walk (node:internal/vfs/providers/memory:643:43)

at walk (node:internal/vfs/providers/memory:650:11)

at walk (node:internal/vfs/providers/memory:650:11)

at walk (node:internal/vfs/providers/memory:650:11)

Suggested Fix

Track visited entries inside walk() to detect cycles:

const visited = new Set();
const walk = (entry, currentPath, relativePath) => {
  if (visited.has(entry)) return; // break cycle
  visited.add(entry);
  // ... rest of existing walk logic
};

Impact

Any application calling readdirSync or promises.readdir with recursive: true on a MemoryProvider that contains circular symlinks will crash the Node.js process. In server-side contexts where the VFS is user-influenced, this enables a Denial of Service. The existing kMaxSymlinkDepth guard does not protect against this code path.

Note: symlinkSync in MemoryProvider does not validate circular targets, making this easy to trigger.

Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    vfsIssues and PRs related to the virtual filesystem subsystem.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions