From bdf75a6c4e107595868b798b696a96ffe2c8c0e6 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Sun, 29 Mar 2026 14:56:01 +0300 Subject: [PATCH 1/3] src: handle null backing store in ArrayBufferViewContents::Read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/nodejs/node/issues/62342 src: handle null backing store in ArrayBufferViewContents::Read PR-URL: https://github.com/nodejs/node/pull/62343 Fixes: https://github.com/nodejs/node/issues/62342 Reviewed-By: René Reviewed-By: Filip Skokan Reviewed-By: Anna Henningsen Reviewed-By: Matteo Collina --- src/util-inl.h | 4 +++- test/parallel/test-crypto-authenticated.js | 23 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/util-inl.h b/src/util-inl.h index f8cccfef6b65b3..d59e30a635b08b 100644 --- a/src/util-inl.h +++ b/src/util-inl.h @@ -591,7 +591,9 @@ void ArrayBufferViewContents::Read(v8::Local abv) { static_assert(sizeof(T) == 1, "Only supports one-byte data at the moment"); length_ = abv->ByteLength(); if (length_ > sizeof(stack_storage_) || abv->HasBuffer()) { - data_ = static_cast(abv->Buffer()->Data()) + abv->ByteOffset(); + auto buf_data = abv->Buffer()->Data(); + data_ = buf_data != nullptr ? static_cast(buf_data) + abv->ByteOffset() + : stack_storage_; } else { abv->CopyContents(stack_storage_, sizeof(stack_storage_)); data_ = stack_storage_; diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index e8fedf2d5d5072..9778ea548e81d7 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -772,3 +772,26 @@ for (const test of TEST_CASES) { decipher.final(); }, /Unsupported state or unable to authenticate data/); } + +// Refs: https://github.com/nodejs/node/issues/62342 +{ + const key = crypto.randomBytes(16); + const nonce = crypto.randomBytes(13); + + const cipher = crypto.createCipheriv('aes-128-ccm', key, nonce, { + authTagLength: 16, + }); + cipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); + cipher.update(new DataView(new ArrayBuffer(0))); + cipher.final(); + const tag = cipher.getAuthTag(); + assert.strictEqual(tag.length, 16); + + const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, { + authTagLength: 16, + }); + decipher.setAuthTag(tag); + decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); + decipher.update(new DataView(new ArrayBuffer(0))); + decipher.final(); +} From adac0774843839cce25d526942b448c28aa53777 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 29 Mar 2026 16:35:10 +0200 Subject: [PATCH 2/3] crypto: runtime-deprecate DEP0203 and DEP0204 PR-URL: https://github.com/nodejs/node/pull/62453 Refs: https://github.com/nodejs/node/issues/55293 Refs: https://github.com/nodejs/node/pull/62321 Reviewed-By: James M Snell Reviewed-By: Matteo Collina --- doc/api/crypto.md | 25 +++++++++++++++++- doc/api/deprecations.md | 10 ++++++-- lib/internal/crypto/keys.js | 38 +++++++++++++++++++++++----- test/parallel/test-crypto-dep0203.js | 23 +++++++++++++++++ test/parallel/test-crypto-dep0204.js | 23 +++++++++++++++++ 5 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-crypto-dep0203.js create mode 100644 test/parallel/test-crypto-dep0204.js diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 8f3f1fac1fae0b..245b29720c3b3f 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2255,12 +2255,20 @@ be listed in the `transferList` argument. * `key` {CryptoKey} * Returns: {KeyObject} -Example: Converting a `CryptoKey` instance to a `KeyObject`: +Returns the underlying {KeyObject} of a {CryptoKey}. The returned {KeyObject} +does not retain any of the restrictions imposed by the Web Crypto API on the +original {CryptoKey}, such as the allowed key usages, the algorithm or hash +algorithm bindings, and the extractability flag. In particular, the underlying +key material of the returned {KeyObject} can always be exported. ```mjs const { KeyObject } = await import('node:crypto'); @@ -3522,6 +3530,9 @@ operations. The specific constants currently defined are described in -Type: Documentation-only +Type: Runtime Passing a [`CryptoKey`][] to `node:crypto` functions is deprecated and will throw an error in a future version. This includes @@ -4507,12 +4510,15 @@ will throw an error in a future version. This includes -Type: Documentation-only +Type: Runtime Passing a non-extractable [`CryptoKey`][] to [`KeyObject.from()`][] is deprecated and will throw an error in a future version. diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 80c073a1dbfac1..ad532ed6885910 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -74,6 +74,7 @@ const { const { customInspectSymbol: kInspect, + getDeprecationWarningEmitter, kEnumerableProperty, lazyDOMException, } = require('internal/util'); @@ -89,6 +90,18 @@ const kKeyUsages = Symbol('kKeyUsages'); const kCachedAlgorithm = Symbol('kCachedAlgorithm'); const kCachedKeyUsages = Symbol('kCachedKeyUsages'); +const emitDEP0203 = getDeprecationWarningEmitter( + 'DEP0203', + 'Passing a CryptoKey to node:crypto functions is deprecated.', +); + +const maybeEmitDEP0204 = getDeprecationWarningEmitter( + 'DEP0204', + 'Passing a non-extractable CryptoKey to KeyObject.from() is deprecated.', + undefined, + false, + (key) => !key[kExtractable], +); // Key input contexts. const kConsumePublic = 0; @@ -140,6 +153,7 @@ const { static from(key) { if (!isCryptoKey(key)) throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); + maybeEmitDEP0204(key); return key[kKeyObject]; } @@ -796,21 +810,28 @@ function prepareAsymmetricKey(key, ctx) { if (isKeyObject(key)) { // Best case: A key object, as simple as that. return { data: getKeyObjectHandle(key, ctx) }; - } else if (isCryptoKey(key)) { + } + if (isCryptoKey(key)) { + emitDEP0203(); return { data: getKeyObjectHandle(key[kKeyObject], ctx) }; - } else if (isStringOrBuffer(key)) { + } + if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') }; - } else if (typeof key === 'object') { + } + if (typeof key === 'object') { const { key: data, encoding, format } = key; // The 'key' property can be a KeyObject as well to allow specifying // additional options such as padding along with the key. - if (isKeyObject(data)) + if (isKeyObject(data)) { return { data: getKeyObjectHandle(data, ctx) }; - else if (isCryptoKey(data)) + } + if (isCryptoKey(data)) { + emitDEP0203(); return { data: getKeyObjectHandle(data[kKeyObject], ctx) }; - else if (format === 'jwk') { + } + if (format === 'jwk') { validateObject(data, 'key.key'); return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' }; } else if (format === 'raw-public' || format === 'raw-private' || @@ -836,6 +857,7 @@ function prepareAsymmetricKey(key, ctx) { ...parseKeyEncoding(key, undefined, isPublic), }; } + throw new ERR_INVALID_ARG_TYPE( 'key', getKeyTypes(ctx !== kCreatePrivate), @@ -856,7 +878,9 @@ function prepareSecretKey(key, encoding, bufferOnly = false) { if (key.type !== 'secret') throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); return key[kHandle]; - } else if (isCryptoKey(key)) { + } + if (isCryptoKey(key)) { + emitDEP0203(); if (key[kKeyType] !== 'secret') throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key[kKeyType], 'secret'); return key[kKeyObject][kHandle]; diff --git a/test/parallel/test-crypto-dep0203.js b/test/parallel/test-crypto-dep0203.js new file mode 100644 index 00000000000000..a973301bcde1af --- /dev/null +++ b/test/parallel/test-crypto-dep0203.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const crypto = require('crypto'); + +common.expectWarning({ + DeprecationWarning: { + DEP0203: 'Passing a CryptoKey to node:crypto functions is deprecated.', + }, +}); + +(async () => { + const key = await globalThis.crypto.subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt'], + ); + + crypto.createCipheriv('aes-128-cbc', key, Buffer.alloc(16)); +})().then(common.mustCall()); diff --git a/test/parallel/test-crypto-dep0204.js b/test/parallel/test-crypto-dep0204.js new file mode 100644 index 00000000000000..29cd4d821fab94 --- /dev/null +++ b/test/parallel/test-crypto-dep0204.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { KeyObject } = require('crypto'); + +common.expectWarning({ + DeprecationWarning: { + DEP0204: 'Passing a non-extractable CryptoKey to KeyObject.from() is deprecated.', + }, +}); + +(async () => { + const key = await globalThis.crypto.subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + false, // non-extractable + ['encrypt'], + ); + + KeyObject.from(key); +})().then(common.mustCall()); From 7ffbb768fe4f0b90d61f3293a2ef09ba7337c578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sun, 29 Mar 2026 16:53:52 +0200 Subject: [PATCH 3/3] events: avoid cloning listeners array on every emit PR-URL: https://github.com/nodejs/node/pull/62261 Reviewed-By: Chemi Atlow Reviewed-By: Antoine du Hamel --- lib/events.js | 63 +++++++++++++------ .../test-event-emitter-modify-in-emit.js | 18 ++++++ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/lib/events.js b/lib/events.js index c065e43d6d2565..c6fc170d590aea 100644 --- a/lib/events.js +++ b/lib/events.js @@ -87,6 +87,7 @@ const { addAbortListener } = require('internal/events/abort_listener'); const kCapture = Symbol('kCapture'); const kErrorMonitor = Symbol('events.errorMonitor'); const kShapeMode = Symbol('shapeMode'); +const kEmitting = Symbol('events.emitting'); const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners'); const kMaxEventTargetListenersWarned = Symbol('events.maxEventTargetListenersWarned'); @@ -514,19 +515,22 @@ EventEmitter.prototype.emit = function emit(type, ...args) { addCatch(this, result, type, args); } } else { - const len = handler.length; - const listeners = arrayClone(handler); - for (let i = 0; i < len; ++i) { - const result = ReflectApply(listeners[i], this, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty. - // This code is duplicated because extracting it away - // would make it non-inlineable. - if (result !== undefined && result !== null) { - addCatch(this, result, type, args); + handler[kEmitting]++; + try { + for (let i = 0; i < handler.length; ++i) { + const result = ReflectApply(handler[i], this, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty. + // This code is duplicated because extracting it away + // would make it non-inlineable. + if (result !== undefined && result !== null) { + addCatch(this, result, type, args); + } } + } finally { + handler[kEmitting]--; } } @@ -565,13 +569,17 @@ function _addListener(target, type, listener, prepend) { } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. - existing = events[type] = - prepend ? [listener, existing] : [existing, listener]; + existing = prepend ? [listener, existing] : [existing, listener]; + existing[kEmitting] = 0; + events[type] = existing; // If we've already got an array, just append. - } else if (prepend) { - existing.unshift(listener); } else { - existing.push(listener); + existing = ensureMutableListenerArray(events, type, existing); + if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } } // Check for listener leak @@ -674,7 +682,7 @@ EventEmitter.prototype.removeListener = if (events === undefined) return this; - const list = events[type]; + let list = events[type]; if (list === undefined) return this; @@ -692,6 +700,7 @@ EventEmitter.prototype.removeListener = if (events.removeListener !== undefined) this.emit('removeListener', type, list.listener || listener); } else if (typeof list !== 'function') { + list = ensureMutableListenerArray(events, type, list); let position = -1; for (let i = list.length - 1; i >= 0; i--) { @@ -875,6 +884,24 @@ function arrayClone(arr) { return ArrayPrototypeSlice(arr); } +function cloneEventListenerArray(arr) { + const copy = arrayClone(arr); + copy[kEmitting] = 0; + if (arr.warned) { + copy.warned = true; + } + return copy; +} + +function ensureMutableListenerArray(events, type, handler) { + if (handler[kEmitting] > 0) { + const copy = cloneEventListenerArray(handler); + events[type] = copy; + return copy; + } + return handler; +} + function unwrapListeners(arr) { const ret = arrayClone(arr); for (let i = 0; i < ret.length; ++i) { diff --git a/test/parallel/test-event-emitter-modify-in-emit.js b/test/parallel/test-event-emitter-modify-in-emit.js index 995fa01d11a1a8..fecabca9def3e8 100644 --- a/test/parallel/test-event-emitter-modify-in-emit.js +++ b/test/parallel/test-event-emitter-modify-in-emit.js @@ -78,3 +78,21 @@ assert.strictEqual(e.listeners('foo').length, 2); e.emit('foo'); assert.deepStrictEqual(['callback2', 'callback3'], callbacks_called); assert.strictEqual(e.listeners('foo').length, 0); + +// Verify that removing all callbacks while in emit allows the current emit to +// propagate to all listeners. +callbacks_called = []; + +function callback4() { + callbacks_called.push('callback4'); + e.removeAllListeners('foo'); +} + +e.on('foo', callback4); +e.on('foo', callback2); +e.on('foo', callback3); +assert.strictEqual(e.listeners('foo').length, 3); +e.emit('foo'); +assert.deepStrictEqual(['callback4', 'callback2', 'callback3'], + callbacks_called); +assert.strictEqual(e.listeners('foo').length, 0);