From f091a7a55635b8b5eb0e8dd10f0c0fc24421a3ab Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 26 Jun 2026 11:09:57 +0200 Subject: [PATCH] async_hooks: clear context frame for thrown microtasks --- lib/internal/process/task_queues.js | 26 +++++++++---- ...microtask-async-context-frame-exception.js | 39 +++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-queue-microtask-async-context-frame-exception.js diff --git a/lib/internal/process/task_queues.js b/lib/internal/process/task_queues.js index 484fa4ea2f9d3e..06afba8bfb5006 100644 --- a/lib/internal/process/task_queues.js +++ b/lib/internal/process/task_queues.js @@ -145,14 +145,24 @@ function nextTick(callback) { } function runMicrotask() { - this.runInAsyncScope(() => { - const callback = this.callback; - try { - callback(); - } finally { - this.emitDestroy(); - } - }); + try { + this.runInAsyncScope(() => { + const callback = this.callback; + try { + callback(); + } finally { + this.emitDestroy(); + } + }); + } catch (error) { + // V8 restores the continuation-preserved embedder data for each + // microtask, but currently does not clear it on exception paths before + // reporting the exception. Clear it here so user code re-entered during + // exception formatting cannot observe this microtask's AsyncLocalStorage + // context. + AsyncContextFrame.set(undefined); + throw error; + } } const defaultMicrotaskResourceOpts = { requireManualDestroy: true }; diff --git a/test/parallel/test-queue-microtask-async-context-frame-exception.js b/test/parallel/test-queue-microtask-async-context-frame-exception.js new file mode 100644 index 00000000000000..659c50c56a6ba7 --- /dev/null +++ b/test/parallel/test-queue-microtask-async-context-frame-exception.js @@ -0,0 +1,39 @@ +// Flags: --async-context-frame +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const sensitive = { secret: 'sensitive' }; +let toPrimitiveStore = 'not called'; +let downstreamStore = 'not called'; + +const thrown = { + [Symbol.toPrimitive]: common.mustCall(() => { + toPrimitiveStore = asyncLocalStorage.getStore(); + queueMicrotask(common.mustCall(() => { + downstreamStore = asyncLocalStorage.getStore(); + assert.strictEqual(downstreamStore, undefined); + })); + return 'thrown'; + }), +}; + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err, thrown); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +})); + +asyncLocalStorage.run(sensitive, () => { + queueMicrotask(() => { + throw thrown; + }); +}); + +setImmediate(common.mustCall(() => { + assert.strictEqual(toPrimitiveStore, undefined); + assert.strictEqual(downstreamStore, undefined); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +}));