Commit 06550c777f9 for nodejs
commit 06550c777f950aca2d53550fbf120fe94be846d1
Author: Harjoth Khara <harjoth.khara@gmail.com>
Date: Sun Oct 18 18:41:35 2026 -0700
vfs: enforce readonly state on open handles
Signed-off-by: harjoth <harjoth.khara@gmail.com>
Fixes: https://github.com/nodejs/node/issues/64401
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/64403
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/lib/internal/vfs/file_handle.js b/lib/internal/vfs/file_handle.js
index 4d28abde535..e86525b8c29 100644
--- a/lib/internal/vfs/file_handle.js
+++ b/lib/internal/vfs/file_handle.js
@@ -19,6 +19,7 @@ const {
} = require('internal/errors');
const {
createEBADF,
+ createEROFS,
} = require('internal/vfs/errors');
const { stringToFlags, toUnixTimestamp } = require('internal/fs/utils');
const { parseFileMode } = require('internal/validators');
@@ -429,6 +430,7 @@ class MemoryFileHandle extends VirtualFileHandle {
#size;
#entry;
#getStats;
+ #isReadOnly;
#checkClosed(syscall) {
if (this.closed) {
@@ -443,13 +445,15 @@ class MemoryFileHandle extends VirtualFileHandle {
* @param {Buffer} content The initial file content
* @param {object} entry The entry object (for updating content)
* @param {Function} getStats Function to get updated stats
+ * @param {Function} [isReadOnly] Returns the provider's read-only state
*/
- constructor(path, flags, mode, content, entry, getStats) {
+ constructor(path, flags, mode, content, entry, getStats, isReadOnly) {
super(path, flags, mode);
this.#content = content;
this.#size = content.length;
this.#entry = entry;
this.#getStats = getStats;
+ this.#isReadOnly = isReadOnly;
// O_TRUNC empties the file at open time. O_APPEND does not move the
// read offset: it only forces writes to the end, so the position stays
@@ -464,11 +468,16 @@ class MemoryFileHandle extends VirtualFileHandle {
}
/**
- * Throws EBADF if the handle was not opened for writing.
+ * Throws EBADF if the handle was not opened for writing, or EROFS if the
+ * provider is read-only.
+ * @param {string} syscall The system call name
*/
- #checkWritable() {
+ #checkWritable(syscall) {
if (!this[kAccess].writable) {
- throw createEBADF('write');
+ throw createEBADF(syscall);
+ }
+ if (this.#isReadOnly?.()) {
+ throw createEROFS(syscall, this.path);
}
}
@@ -571,7 +580,7 @@ class MemoryFileHandle extends VirtualFileHandle {
*/
writeSync(buffer, offset, length, position) {
this.#checkClosed('write');
- this.#checkWritable();
+ this.#checkWritable('write');
// In append mode, always write at the end
const useCurrentPosition = isCurrentPosition(position);
@@ -740,7 +749,7 @@ class MemoryFileHandle extends VirtualFileHandle {
*/
truncateSync(len = 0) {
this.#checkClosed('ftruncate');
- this.#checkWritable();
+ this.#checkWritable('ftruncate');
if (len < this.#size) {
// Zero out truncated region to avoid stale data
diff --git a/lib/internal/vfs/providers/memory.js b/lib/internal/vfs/providers/memory.js
index a491d602628..668ac9c8e83 100644
--- a/lib/internal/vfs/providers/memory.js
+++ b/lib/internal/vfs/providers/memory.js
@@ -501,7 +501,16 @@ class MemoryProvider extends VirtualProvider {
}
const getStats = (size) => this.#createStats(entry, size);
- return new MemoryFileHandle(normalized, flags, mode ?? entry.mode, entry.content, entry, getStats);
+ const isReadOnly = () => this.readonly;
+ return new MemoryFileHandle(
+ normalized,
+ flags,
+ mode ?? entry.mode,
+ entry.content,
+ entry,
+ getStats,
+ isReadOnly,
+ );
}
async open(path, flags, mode) {
diff --git a/test/parallel/test-vfs-memory-provider.js b/test/parallel/test-vfs-memory-provider.js
index 9b479549cce..3591166153c 100644
--- a/test/parallel/test-vfs-memory-provider.js
+++ b/test/parallel/test-vfs-memory-provider.js
@@ -583,6 +583,8 @@ const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/file.txt', 'content');
myVfs.mkdirSync('/dir', { recursive: true });
+ const fd = myVfs.openSync('/file.txt', 'r+');
+ const appendFd = myVfs.openSync('/file.txt', 'a');
// Set to readonly
myVfs.provider.setReadOnly();
@@ -629,12 +631,30 @@ const vfs = require('node:vfs');
assert.throws(() => {
myVfs.symlinkSync('/file.txt', '/link');
}, { code: 'EROFS' });
+
+ // File descriptors opened before setReadOnly() must not bypass it.
+ assert.throws(() => {
+ myVfs.writeSync(fd, Buffer.from('new'), 0, 3, 0);
+ }, { code: 'EROFS' });
+
+ assert.throws(() => {
+ myVfs.ftruncateSync(fd, 0);
+ }, { code: 'EROFS' });
+
+ assert.throws(() => {
+ myVfs.writeSync(appendFd, Buffer.from('new'), 0, 3, null);
+ }, { code: 'EROFS' });
+
+ assert.strictEqual(myVfs.readFileSync('/file.txt', 'utf8'), 'content');
+ myVfs.closeSync(fd);
+ myVfs.closeSync(appendFd);
}
// Test async operations on readonly MemoryProvider
(async () => {
const myVfs = vfs.create();
myVfs.writeFileSync('/readonly.txt', 'content');
+ const handle = await myVfs.provider.open('/readonly.txt', 'r+');
myVfs.provider.setReadOnly();
await assert.rejects(
@@ -661,4 +681,27 @@ const vfs = require('node:vfs');
myVfs.promises.copyFile('/readonly.txt', '/copy.txt'),
{ code: 'EROFS' }
);
+
+ await assert.rejects(
+ handle.write(Buffer.from('new'), 0, 3, 0),
+ { code: 'EROFS' }
+ );
+
+ await assert.rejects(
+ handle.writeFile('new'),
+ { code: 'EROFS' }
+ );
+
+ await assert.rejects(
+ handle.truncate(0),
+ { code: 'EROFS' }
+ );
+
+ await assert.rejects(
+ handle.writev([Buffer.from('n'), Buffer.from('ew')], 0),
+ { code: 'EROFS' }
+ );
+
+ assert.strictEqual(await handle.readFile('utf8'), 'content');
+ await handle.close();
})().then(common.mustCall());