From 0a655cbc2994d02308a032bc3e021b19d27c8863 Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Sat, 6 Nov 2021 13:44:37 +0530 Subject: [PATCH] async_hooks: use correct resource for AsyncLocalStorage Fixes: https://github.com/nodejs/node/issues/40693 Signed-off-by: Darshan Sen --- lib/async_hooks.js | 27 ++++++++++++--- .../test-async-local-storage-dgram.js | 24 +++++++++++++ .../test-async-local-storage-socket.js | 25 ++++++++++++++ .../test-async-local-storage-tlssocket.js | 34 +++++++++++++++++++ 4 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 test/async-hooks/test-async-local-storage-dgram.js create mode 100644 test/async-hooks/test-async-local-storage-socket.js create mode 100644 test/async-hooks/test-async-local-storage-tlssocket.js diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 04135ec41c223b..a5760efbfed156 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -252,10 +252,29 @@ class AsyncResource { } } +function lookupResourceWithStorage() { + // When a TCP/UDP object is created, its owner_symbol is uninitialized, so + // calling executionAsyncResource() would return the TCP/UDP object itself. + // So the store is initially saved on the TCP/UDP object. Later on, when the + // Socket/TLSSocket object is created, the TCP/UDP object is wrapped by the + // Socket/TLSSocket object in the _handle property and the owner_symbol of the + // TCP/UDP object is set to the corresponding Socket/TLSSocket object. So + // calling executionAsyncResource() now would return the Socket/TLSSocket + // object, which does not contain the storage that was saved initially. Hence, + // in the case of a Socket/TLSSocket object, we must use the underlying + // TCP/UDP object, if available, for storage. + let resource = executionAsyncResource(); + if ((resource.constructor.name === 'Socket' || + resource.constructor.name === 'TLSSocket') && + resource._handle != null) + resource = resource._handle; + return resource; +} + const storageList = []; const storageHook = createHook({ init(asyncId, type, triggerAsyncId, resource) { - const currentResource = executionAsyncResource(); + const currentResource = lookupResourceWithStorage(); // Value of currentResource is always a non null object for (let i = 0; i < storageList.length; ++i) { storageList[i]._propagate(resource, currentResource); @@ -299,7 +318,7 @@ class AsyncLocalStorage { enterWith(store) { this._enable(); - const resource = executionAsyncResource(); + const resource = lookupResourceWithStorage(); resource[this.kResourceStore] = store; } @@ -311,7 +330,7 @@ class AsyncLocalStorage { this._enable(); - const resource = executionAsyncResource(); + const resource = lookupResourceWithStorage(); const oldStore = resource[this.kResourceStore]; resource[this.kResourceStore] = store; @@ -337,7 +356,7 @@ class AsyncLocalStorage { getStore() { if (this.enabled) { - const resource = executionAsyncResource(); + const resource = lookupResourceWithStorage(); return resource[this.kResourceStore]; } } diff --git a/test/async-hooks/test-async-local-storage-dgram.js b/test/async-hooks/test-async-local-storage-dgram.js new file mode 100644 index 00000000000000..d9b3d1e32a0f84 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-dgram.js @@ -0,0 +1,24 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const dgram = require('dgram'); +const { AsyncLocalStorage } = require('async_hooks'); + +dgram.createSocket('udp4') + .on('message', function(msg, rinfo) { this.send(msg, rinfo.port); }) + .on('listening', function() { + const asyncLocalStorage = new AsyncLocalStorage(); + const store = { val: 'abcd' }; + asyncLocalStorage.run(store, () => { + const client = dgram.createSocket('udp4'); + client.on('message', (msg, rinfo) => { + assert.deepStrictEqual(asyncLocalStorage.getStore(), store); + client.close(); + this.close(); + }); + client.send('Hello, world!', this.address().port); + }); + }) + .bind(0); diff --git a/test/async-hooks/test-async-local-storage-socket.js b/test/async-hooks/test-async-local-storage-socket.js new file mode 100644 index 00000000000000..dd89157d0f04de --- /dev/null +++ b/test/async-hooks/test-async-local-storage-socket.js @@ -0,0 +1,25 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const net = require('net'); +const { AsyncLocalStorage } = require('async_hooks'); + +net + .createServer((socket) => { + socket.write('Hello, world!'); + socket.pipe(socket); + }) + .listen(0, function() { + const asyncLocalStorage = new AsyncLocalStorage(); + const store = { val: 'abcd' }; + asyncLocalStorage.run({ val: 'abcd' }, () => { + const client = net.connect({ port: this.address().port }); + client.on('data', () => { + assert.deepStrictEqual(asyncLocalStorage.getStore(), store); + client.end(); + this.close(); + }); + }); + }); diff --git a/test/async-hooks/test-async-local-storage-tlssocket.js b/test/async-hooks/test-async-local-storage-tlssocket.js new file mode 100644 index 00000000000000..219390e5e26700 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-tlssocket.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); +const { AsyncLocalStorage } = require('async_hooks'); + +const options = { + cert: fixtures.readKey('rsa_cert.crt'), + key: fixtures.readKey('rsa_private.pem'), + rejectUnauthorized: false +}; + +tls + .createServer(options, (socket) => { + socket.write('Hello, world!'); + socket.pipe(socket); + }) + .listen(0, function() { + const asyncLocalStorage = new AsyncLocalStorage(); + const store = { val: 'abcd' }; + asyncLocalStorage.run({ val: 'abcd' }, () => { + const client = tls.connect({ port: this.address().port, ...options }); + client.on('data', () => { + assert.deepStrictEqual(asyncLocalStorage.getStore(), store); + client.end(); + this.close(); + }); + }); + });