Commit 513981d1d5f for nodejs

commit 513981d1d5fa705ec6a0d2b98ef8313a8c02d234
Author: Christian Aurich Zanettini Martins <christian.aurichzm@gmail.com>
Date:   Mon Sep 21 00:09:28 2026 -0300

    fs: close fd 0 on discarded FileHandle transfer

    `FileHandle::TransferData` uses `-1` to indicate that it no longer owns
    a file descriptor. However, its destructor only closes descriptors
    greater than 0.

    If a transferred `FileHandle` owns fd 0 and the message is discarded,
    the descriptor is left open.

    Treat fd 0 like any other valid descriptor and only skip `-1`.

    Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66095
    Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
    Reviewed-By: James M Snell <jasnell@gmail.com>

diff --git a/src/node_file.cc b/src/node_file.cc
index 02e83a72e04..a95cd5eba14 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -328,7 +328,7 @@ std::unique_ptr<worker::TransferData> FileHandle::TransferForMessaging() {
 FileHandle::TransferData::TransferData(int fd) : fd_(fd) {}

 FileHandle::TransferData::~TransferData() {
-  if (fd_ > 0) {
+  if (fd_ >= 0) {
     uv_fs_t close_req;
     CHECK_NE(fd_, -1);
     FS_SYNC_TRACE_BEGIN(close);
diff --git a/test/parallel/test-worker-message-port-transfer-filehandle-fd0.js b/test/parallel/test-worker-message-port-transfer-filehandle-fd0.js
new file mode 100644
index 00000000000..f13dccb7c6a
--- /dev/null
+++ b/test/parallel/test-worker-message-port-transfer-filehandle-fd0.js
@@ -0,0 +1,46 @@
+'use strict';
+
+// A FileHandle whose transfer is discarded has to close its file descriptor.
+// Descriptor 0 is an ordinary descriptor once stdin has been closed, so the
+// scenario runs in a child process that can afford to lose its stdin.
+
+const common = require('../common');
+
+if (common.isWindows)
+  common.skip('descriptor numbering after closing stdin is POSIX-specific');
+
+const assert = require('assert');
+const fs = require('fs');
+
+if (process.argv[2] === 'child') {
+  const vm = require('vm');
+  const { MessageChannel, moveMessagePortToContext } =
+    require('worker_threads');
+
+  (async function() {
+    fs.closeSync(0);
+    const fh = await fs.promises.open(__filename);
+    assert.strictEqual(fh.fd, 0);
+
+    const { port1, port2 } = new MessageChannel();
+    // A port living in another context cannot receive the handle, so the
+    // message is discarded on delivery and takes the descriptor with it.
+    const moved = moveMessagePortToContext(port2, vm.createContext());
+    const discarded = new Promise((resolve) => {
+      moved.onmessageerror = resolve;
+    });
+    moved.start();
+
+    port1.postMessage(fh, [ fh ]);
+    await discarded;
+
+    assert.throws(() => fs.fstatSync(0), { code: 'EBADF' });
+    port1.close();
+  })().then(common.mustCall());
+  return;
+}
+
+const { spawnSync } = require('child_process');
+const result = spawnSync(process.execPath, [ __filename, 'child' ],
+                         { stdio: [ 'ignore', 'inherit', 'inherit' ] });
+assert.strictEqual(result.status, 0);