From 65b00406cbba8c95435a020d9b1663e85fd2c917 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Sat, 4 Jul 2026 06:55:17 -0700 Subject: [PATCH] vfs: reject rename into descendant directory MemoryProvider allowed renaming a directory into its own descendant, which detached the subtree from the root. Reject this case with EINVAL before mutating the tree. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 --- lib/internal/vfs/providers/memory.js | 6 ++++++ test/parallel/test-vfs-rename.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/internal/vfs/providers/memory.js b/lib/internal/vfs/providers/memory.js index 995b8f236200d5..b306c774cdfab7 100644 --- a/lib/internal/vfs/providers/memory.js +++ b/lib/internal/vfs/providers/memory.js @@ -7,6 +7,7 @@ const { SafeMap, SafeSet, StringPrototypeReplaceAll, + StringPrototypeStartsWith, Symbol, } = primordials; @@ -794,6 +795,11 @@ class MemoryProvider extends VirtualProvider { // Get the entry (without following symlinks for the entry itself) const entry = this.#getEntry(normalizedOld, 'rename', false); + if (entry.isDirectory() && + StringPrototypeStartsWith(normalizedNew, `${normalizedOld}/`)) { + throw createEINVAL('rename', oldPath); + } + // Validate destination parent exists (do not auto-create) const newParent = this.#ensureParent(normalizedNew, false, 'rename'); const newName = pathPosix.basename(normalizedNew); diff --git a/test/parallel/test-vfs-rename.js b/test/parallel/test-vfs-rename.js index 26506f01dd7338..69daba43c11ddf 100644 --- a/test/parallel/test-vfs-rename.js +++ b/test/parallel/test-vfs-rename.js @@ -44,3 +44,17 @@ const vfs = require('node:vfs'); assert.strictEqual(myVfs.existsSync('/d/a.txt'), false); assert.strictEqual(myVfs.existsSync('/d/b.txt'), true); } + +// Renaming a directory into its own descendant throws EINVAL +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/a/b', { recursive: true }); + myVfs.writeFileSync('/a/file.txt', 'data'); + + assert.throws(() => myVfs.renameSync('/a', '/a/b/c'), { code: 'EINVAL' }); + assert.deepStrictEqual(myVfs.readdirSync('/'), ['a']); + assert.strictEqual(myVfs.existsSync('/a'), true); + assert.strictEqual(myVfs.existsSync('/a/b'), true); + assert.strictEqual(myVfs.existsSync('/a/b/c'), false); + assert.strictEqual(myVfs.readFileSync('/a/file.txt', 'utf8'), 'data'); +}