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