From 622531d96dfce6e075e880b999106c600acf677b Mon Sep 17 00:00:00 2001 From: Christian Aurich Date: Thu, 17 Sep 2026 19:03:32 -0300 Subject: [PATCH] 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 --- src/node_file.cc | 2 +- ...er-message-port-transfer-filehandle-fd0.js | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-worker-message-port-transfer-filehandle-fd0.js diff --git a/src/node_file.cc b/src/node_file.cc index 0179ad12c9d8..f231c30bbf3b 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -328,7 +328,7 @@ std::unique_ptr 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 000000000000..f13dccb7c6a9 --- /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);