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());