From 6e2fc834a7c7adf76a903bf072d9f79773e6e2d1 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 6 May 2026 08:08:49 +0300 Subject: [PATCH] fs: cancel in-flight stat on abort Signed-off-by: Mert Can Altin --- lib/fs.js | 1 + src/node_file.cc | 8 ++++ test/parallel/test-fs-stat-cancel-inflight.js | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 test/parallel/test-fs-stat-cancel-inflight.js diff --git a/lib/fs.js b/lib/fs.js index 3c0bef6cf506..92934139dc20 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -392,6 +392,7 @@ function bindSignalToReq(req, signal, callback) { let aborted = false; const onAbort = () => { aborted = true; + req.cancel(); callback(new AbortError(undefined, { cause: signal.reason })); }; kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; diff --git a/src/node_file.cc b/src/node_file.cc index b2e765346cf1..699110f7b965 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -758,6 +758,12 @@ void NewFSReqCallback(const FunctionCallbackInfo& args) { new FSReqCallback(binding_data, args.This(), args[0]->IsTrue()); } +void CancelFSReq(const FunctionCallbackInfo& args) { + FSReqBase* req_wrap; + ASSIGN_OR_RETURN_UNWRAP(&req_wrap, args.This()); + req_wrap->Cancel(); +} + FSReqAfterScope::FSReqAfterScope(FSReqBase* wrap, uv_fs_t* req) : wrap_(wrap), req_(req), @@ -4618,6 +4624,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, fst->InstanceTemplate()->SetInternalFieldCount( FSReqBase::kInternalFieldCount); fst->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data)); + SetProtoMethod(isolate, fst, "cancel", CancelFSReq); SetConstructorFunction(isolate, target, "FSReqCallback", fst); // Create FunctionTemplate for FileHandleReadWrap. There’s no need @@ -4734,6 +4741,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(HandleToFd); #endif registry->Register(NewFSReqCallback); + registry->Register(CancelFSReq); registry->Register(FileHandle::New); registry->Register(FileHandle::Close); diff --git a/test/parallel/test-fs-stat-cancel-inflight.js b/test/parallel/test-fs-stat-cancel-inflight.js new file mode 100644 index 000000000000..b863165301d4 --- /dev/null +++ b/test/parallel/test-fs-stat-cancel-inflight.js @@ -0,0 +1,37 @@ +'use strict'; + +// Aborting a queued stat must cancel the libuv request instead of running it. +// The threadpool is saturated so the request is still queued when it is +// cancelled; the queue is FIFO, so the stat cannot start before the blocker. + +const common = require('../common'); + +if (!common.hasCrypto) common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { spawnSync } = require('child_process'); + +if (process.argv[2] !== 'child') { + const result = spawnSync( + process.execPath, + ['--expose-internals', __filename, 'child'], + { env: { ...process.env, UV_THREADPOOL_SIZE: '1' }, encoding: 'utf8' }, + ); + assert.strictEqual(result.status, 0, result.stderr); + return; +} + +const { internalBinding } = require('internal/test/binding'); +const binding = internalBinding('fs'); + +// Occupy the only worker thread so the stat below cannot start. +crypto.pbkdf2('secret', 'salt', 500_000, 32, 'sha512', common.mustCall()); + +const req = new binding.FSReqCallback(false); +req.oncomplete = common.mustCall((err) => { + // Without cancellation the stat would run and report success. + assert.strictEqual(err?.code, 'ECANCELED'); +}); +binding.stat(__filename, false, req, true); +req.cancel();