Version
main
Platform
Subsystem
vfs
What steps will reproduce the bug?
import { create } from 'node:vfs';
const fs = create();
fs.mkdirSync('/a/b', { recursive: true });
fs.writeFileSync('/a/keep.txt', 'keep');
fs.symlinkSync('/a/b', '/alias');
try {
fs.renameSync('/a', '/alias/moved');
console.log('rename succeeded');
} catch (error) {
console.log('rename threw:', error.code);
}
console.log('/a exists:', fs.existsSync('/a'));
console.log('original data exists:', fs.existsSync('/a/keep.txt'));
console.log('/alias exists:', fs.existsSync('/alias'));
How often does it reproduce? Is there a required condition?
Always
What is the expected behavior? Why is that the expected behavior?
The rename should throw EINVAL and leave the directory tree unchanged, matching native fs. /alias/moved resolves to /a/b/moved, so this attempts to move /a into its own descendant.
What do you see instead?
rename succeeded
/a exists: false
original data exists: false
/alias exists: false
The operation detaches /a and its contents from the filesystem root. /alias remains a symlink but becomes dangling.
Additional information
fs equivalent
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rename-repro-'));
const p = (name) => path.join(root, name);
try {
fs.mkdirSync(p('a/b'), { recursive: true });
fs.writeFileSync(p('a/keep.txt'), 'keep');
fs.symlinkSync(p('a/b'), p('alias'), 'dir');
try {
fs.renameSync(p('a'), p('alias/moved'));
console.log('rename succeeded');
} catch (error) {
console.log('rename threw:', error.code);
}
console.log('/a exists:', fs.existsSync(p('a')));
console.log('original data exists:', fs.existsSync(p('a/keep.txt')));
console.log('/alias exists:', fs.existsSync(p('alias')));
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
Output
rename threw: EINVAL
/a exists: true
original data exists: true
/alias exists: true
Version
main
Platform
Subsystem
vfs
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
Always
What is the expected behavior? Why is that the expected behavior?
The rename should throw
EINVALand leave the directory tree unchanged, matching nativefs./alias/movedresolves to/a/b/moved, so this attempts to move/ainto its own descendant.What do you see instead?
The operation detaches
/aand its contents from the filesystem root./aliasremains a symlink but becomes dangling.Additional information
fs equivalent
Output