From 040c51c98f4f3a65e36db64e2eb431294201fdf5 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 31 May 2026 00:59:42 -0700 Subject: [PATCH 1/8] stream: settle pending broadcast reads on return Resolve pending next() calls when broadcast consumers are returned, so the promise does not remain pending after iterator cleanup. Fixes: https://github.com/nodejs/node/issues/63519 Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: https://github.com/nodejs/node/pull/63603 Fixes: https://github.com/nodejs/node/issues/63519 Reviewed-By: Ethan Arrowood Reviewed-By: Matteo Collina --- lib/internal/streams/iter/broadcast.js | 1 + test/parallel/test-stream-iter-broadcast-basic.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/internal/streams/iter/broadcast.js b/lib/internal/streams/iter/broadcast.js index e6a404729d6e0b..aa83c9636598ea 100644 --- a/lib/internal/streams/iter/broadcast.js +++ b/lib/internal/streams/iter/broadcast.js @@ -165,6 +165,7 @@ class BroadcastImpl { function detach() { state.detached = true; + state.resolve?.({ __proto__: null, done: true, value: undefined }); state.resolve = null; state.reject = null; if (self.#deleteConsumer(state)) { diff --git a/test/parallel/test-stream-iter-broadcast-basic.js b/test/parallel/test-stream-iter-broadcast-basic.js index ab2c81304ec2ac..32c1750fb4cbfd 100644 --- a/test/parallel/test-stream-iter-broadcast-basic.js +++ b/test/parallel/test-stream-iter-broadcast-basic.js @@ -161,6 +161,18 @@ async function testCancelWithReason() { assert.strictEqual(result.message, 'cancelled'); } +async function testPendingNextSettlesAfterReturn() { + const { broadcast: bc } = broadcast(); + const iter = bc.push()[Symbol.asyncIterator](); + + const pendingNext = iter.next(); + await iter.return(); + + const result = await pendingNext; + assert.strictEqual(result.done, true); + assert.strictEqual(result.value, undefined); +} + // ============================================================================= // Writer fail detaches consumers // ============================================================================= @@ -254,6 +266,7 @@ Promise.all([ testCancelWithoutReason(), testCancelWithReason(), testCancelWithFalsyReason(), + testPendingNextSettlesAfterReturn(), testFailDetachesConsumers(), testWriterFailIdempotent(), testLateJoinerSeesBufferedData(), From 9b7e761ddf785267e134b5fca037cad871ab2e34 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 25 May 2026 09:54:33 +0200 Subject: [PATCH 2/8] crypto: coerce -0 to +0 before native calls Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63556 Reviewed-By: Jordan Harband Reviewed-By: Chemi Atlow --- lib/internal/crypto/cipher.js | 9 +- lib/internal/crypto/diffiehellman.js | 5 +- lib/internal/crypto/hash.js | 9 +- lib/internal/crypto/hkdf.js | 2 + lib/internal/crypto/keygen.js | 32 +++-- lib/internal/crypto/random.js | 8 +- test/parallel/test-crypto-negative-zero.js | 160 +++++++++++++++++++++ 7 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 test/parallel/test-crypto-negative-zero.js diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index 1d397e8a94819c..d622d35d85faf8 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -108,7 +108,8 @@ function getUIntOption(options, key) { if (options && (value = options[key]) != null) { if (value >>> 0 !== value) throw new ERR_INVALID_ARG_VALUE(`options.${key}`, value); - return value; + // Coerce -0 to +0. + return value + 0; } return -1; } @@ -256,12 +257,16 @@ const kMinNid = 1; const kMaxNid = 2_147_483_647; function getCipherInfo(nameOrNid, options = {}) { validateObject(options, 'options'); - const { keyLength, ivLength } = options; + let { keyLength, ivLength } = options; if (keyLength !== undefined) { validateUint32(keyLength, 'options.keyLength'); + // Coerce -0 to +0. + keyLength += 0; } if (ivLength !== undefined) { validateUint32(ivLength, 'options.ivLength'); + // Coerce -0 to +0. + ivLength += 0; } const type = typeof nameOrNid; diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index d17b06ef155f76..3bd4bd33156c94 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -92,8 +92,11 @@ function DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding) { // rejected with ERR_OSSL_BN_BITS_TOO_SMALL) by OpenSSL. The glue code // in node_crypto.cc accepts values that are IsInt32() for that reason // and that's why we do that here too. - if (typeof sizeOrKey === 'number') + if (typeof sizeOrKey === 'number') { validateInt32(sizeOrKey, 'sizeOrKey'); + // Coerce -0 to +0. + sizeOrKey += 0; + } if (keyEncoding && !Buffer.isEncoding(keyEncoding) && keyEncoding !== 'buffer') { diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 5aec1614cb92e9..d47f7518f06260 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -93,10 +93,13 @@ function Hash(algorithm, options) { const isCopy = algorithm instanceof _Hash; if (!isCopy) validateString(algorithm, 'algorithm'); - const xofLen = typeof options === 'object' && options !== null ? + let xofLen = typeof options === 'object' && options !== null ? options.outputLength : undefined; - if (xofLen !== undefined) + if (xofLen !== undefined) { validateUint32(xofLen, 'options.outputLength'); + // Coerce -0 to +0. + xofLen += 0; + } // Lookup the cached ID from JS land because it's faster than decoding // the string in C++ land. const algorithmId = isCopy ? -1 : getCachedHashId(algorithm); @@ -285,6 +288,8 @@ function hash(algorithm, input, options) { if (outputLength !== undefined) { validateUint32(outputLength, 'outputLength'); + // Coerce -0 to +0. + outputLength += 0; } if (outputLength === undefined) { diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 73b16da6923024..c9b868e23af4e2 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -58,6 +58,8 @@ const validateParameters = hideStackFrames((hash, key, salt, info, length) => { info = validateByteSource.withoutStackTrace(info, 'info'); validateInteger.withoutStackTrace(length, 'length', 0, kMaxLength); + // Coerce -0 to +0. + length += 0; if (info.byteLength > 1024) { throw new ERR_OUT_OF_RANGE.HideStackFramesError( diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index e6e787c39f512a..41971002441ef5 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -219,14 +219,18 @@ function createJob(mode, type, options) { case 'rsa-pss': { validateObject(options, 'options'); - const { modulusLength } = options; + let { modulusLength } = options; validateUint32(modulusLength, 'options.modulusLength'); + // Coerce -0 to +0. + modulusLength += 0; let { publicExponent } = options; if (publicExponent == null) { publicExponent = 0x10001; } else { validateUint32(publicExponent, 'options.publicExponent'); + // Coerce -0 to +0. + publicExponent += 0; } if (type === 'rsa') { @@ -238,12 +242,14 @@ function createJob(mode, type, options) { ...encoding); } - const { - hashAlgorithm, mgf1HashAlgorithm, saltLength, - } = options; + const { hashAlgorithm, mgf1HashAlgorithm } = options; + let { saltLength } = options; - if (saltLength !== undefined) + if (saltLength !== undefined) { validateInt32(saltLength, 'options.saltLength', 0); + // Coerce -0 to +0. + saltLength += 0; + } if (hashAlgorithm !== undefined) validateString(hashAlgorithm, 'options.hashAlgorithm'); if (mgf1HashAlgorithm !== undefined) @@ -284,14 +290,19 @@ function createJob(mode, type, options) { case 'dsa': { validateObject(options, 'options'); - const { modulusLength } = options; + let { modulusLength } = options; validateUint32(modulusLength, 'options.modulusLength'); + // Coerce -0 to +0. + modulusLength += 0; let { divisorLength } = options; if (divisorLength == null) { divisorLength = -1; - } else + } else { validateInt32(divisorLength, 'options.divisorLength', 0); + // Coerce -0 to +0. + divisorLength += 0; + } return new DsaKeyPairGenJob( mode, @@ -321,7 +332,8 @@ function createJob(mode, type, options) { case 'dh': { validateObject(options, 'options'); - const { group, primeLength, prime, generator } = options; + const { group, prime } = options; + let { primeLength, generator } = options; if (group != null) { if (prime != null) throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'prime'); @@ -342,6 +354,8 @@ function createJob(mode, type, options) { validateBuffer(prime, 'options.prime'); } else if (primeLength != null) { validateInt32(primeLength, 'options.primeLength', 0); + // Coerce -0 to +0. + primeLength += 0; } else { throw new ERR_MISSING_OPTION( 'At least one of the group, prime, or primeLength options'); @@ -349,6 +363,8 @@ function createJob(mode, type, options) { if (generator != null) { validateInt32(generator, 'options.generator', 0); + // Coerce -0 to +0. + generator += 0; } return new DhKeyPairGenJob( mode, diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index c324b2292b2fb8..a75c14fd2a1c4b 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -603,12 +603,14 @@ function checkPrime(candidate, options = kEmptyObject, callback) { } validateFunction(callback, 'callback'); validateObject(options, 'options'); - const { + let { checks = 0, } = options; // The checks option is unsigned but must fit into a signed C int for OpenSSL. validateInt32(checks, 'options.checks', 0); + // Coerce -0 to +0. + checks += 0; const job = new CheckPrimeJob(kCryptoJobAsync, candidate, checks); job.ondone = callback; @@ -632,12 +634,14 @@ function checkPrimeSync(candidate, options = kEmptyObject) { ); } validateObject(options, 'options'); - const { + let { checks = 0, } = options; // The checks option is unsigned but must fit into a signed C int for OpenSSL. validateInt32(checks, 'options.checks', 0); + // Coerce -0 to +0. + checks += 0; const job = new CheckPrimeJob(kCryptoJobSync, candidate, checks); const { 0: err, 1: result } = job.run(); diff --git a/test/parallel/test-crypto-negative-zero.js b/test/parallel/test-crypto-negative-zero.js new file mode 100644 index 00000000000000..0af9220569c374 --- /dev/null +++ b/test/parallel/test-crypto-negative-zero.js @@ -0,0 +1,160 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); + +function getOutcome(fn) { + try { + return { result: fn() }; + } catch (err) { + return { err }; + } +} + +function assertSameOutcome(actual, expected) { + if (expected.err !== undefined) { + assert(actual.err instanceof Error); + assert.strictEqual(actual.err.name, expected.err.name); + assert.strictEqual(actual.err.code, expected.err.code); + assert.strictEqual(actual.err.message, expected.err.message); + } else { + assert.deepStrictEqual(actual.result, expected.result); + } +} + +function assertSameErrorOrSuccess(actual, expected) { + if (expected.err !== undefined) { + assert(actual.err instanceof Error); + assert.strictEqual(actual.err.name, expected.err.name); + assert.strictEqual(actual.err.code, expected.err.code); + assert.strictEqual(actual.err.message, expected.err.message); + } else { + assert.strictEqual(actual.err, undefined); + } +} + +{ + const expected = getOutcome(() => + crypto.hkdfSync('sha256', 'key', 'salt', 'info', 0), + ); + assertSameOutcome( + getOutcome(() => crypto.hkdfSync('sha256', 'key', 'salt', 'info', -0)), + expected, + ); + crypto.hkdf('sha256', 'key', 'salt', 'info', -0, + common.mustCall((err, result) => { + assertSameOutcome({ err, result }, expected); + })); +} + +{ + assert.strictEqual( + crypto.checkPrimeSync(Buffer.from([3]), { checks: -0 }), + true, + ); + crypto.checkPrime(Buffer.from([3]), { checks: -0 }, + common.mustSucceed((result) => { + assert.strictEqual(result, true); + })); +} + +{ + assert.throws(() => crypto.createDiffieHellman(-0, 2), { + name: 'Error', + }); +} + +{ + for (const [type, getOptions] of [ + ['rsa', (zero) => ({ modulusLength: zero })], + ['rsa', (zero) => ({ modulusLength: 512, publicExponent: zero })], + ['rsa-pss', (zero) => ({ + modulusLength: 512, + publicExponent: 65537, + saltLength: zero, + })], + ['dsa', (zero) => ({ modulusLength: zero })], + ['dh', (zero) => ({ primeLength: zero })], + ['dh', (zero) => ({ primeLength: 2, generator: zero })], + ]) { + assertSameErrorOrSuccess( + getOutcome(() => crypto.generateKeyPairSync(type, getOptions(-0))), + getOutcome(() => crypto.generateKeyPairSync(type, getOptions(0))), + ); + } + + if (!hasOpenSSL(3)) { + common.printSkipMessage( + 'Skipping DSA divisorLength 0 key generation on OpenSSL 1.1.1'); + } else { + assertSameErrorOrSuccess( + getOutcome(() => crypto.generateKeyPairSync('dsa', { + modulusLength: 512, + divisorLength: -0, + })), + getOutcome(() => crypto.generateKeyPairSync('dsa', { + modulusLength: 512, + divisorLength: 0, + })), + ); + } + + crypto.generateKeyPair('rsa', { modulusLength: -0 }, + common.mustCall((err) => { + assert(err instanceof Error); + })); +} + +if (!process.features.openssl_is_boringssl) { + assert.strictEqual( + crypto.createHash('shake128', { outputLength: -0 }).digest('hex'), + '', + ); + assert.strictEqual( + crypto.createHash('shake128', { outputLength: 5 }) + .copy({ outputLength: -0 }) + .digest('hex'), + '', + ); + assert.strictEqual( + crypto.hash('shake128', 'data', { outputLength: -0 }), + '', + ); +} + +{ + const key = Buffer.alloc(16); + const iv = Buffer.alloc(12); + + assertSameErrorOrSuccess( + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv, { authTagLength: -0 })), + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv, { authTagLength: 0 })), + ); + assertSameErrorOrSuccess( + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv).setAAD( + Buffer.alloc(0), + { plaintextLength: -0 }, + )), + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv).setAAD( + Buffer.alloc(0), + { plaintextLength: 0 }, + )), + ); + assert.strictEqual( + crypto.getCipherInfo('aes-128-cbc', { keyLength: -0 }), + undefined, + ); + assert.strictEqual( + crypto.getCipherInfo('aes-128-cbc', { ivLength: -0 }), + undefined, + ); +} From 9413368cf386125a76c9f0d9c8dd71e6747b64a1 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 25 May 2026 10:23:24 +0200 Subject: [PATCH 3/8] dns: coerce -0 to +0 in lookup and resolver inputs Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63556 Reviewed-By: Jordan Harband Reviewed-By: Chemi Atlow --- lib/dns.js | 9 ++++++--- lib/internal/dns/promises.js | 6 ++++-- lib/internal/dns/utils.js | 8 ++++++-- test/parallel/test-dns-negative-zero.js | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 test/parallel/test-dns-negative-zero.js diff --git a/lib/dns.js b/lib/dns.js index d651f5ea0a2685..1301cbd2ce9901 100644 --- a/lib/dns.js +++ b/lib/dns.js @@ -157,7 +157,8 @@ function lookup(hostname, options, callback) { validateFunction(callback, 'callback'); validateOneOf(options, 'family', validFamilies); - family = options; + // Coerce -0 to +0. + family = options + 0; } else if (options !== undefined && typeof options !== 'object') { validateFunction(arguments.length === 2 ? options : callback, 'callback'); throw new ERR_INVALID_ARG_TYPE('options', ['integer', 'object'], options); @@ -179,7 +180,8 @@ function lookup(hostname, options, callback) { break; default: validateOneOf(options.family, 'options.family', validFamilies); - family = options.family; + // Coerce -0 to +0. + family = options.family + 0; break; } } @@ -274,7 +276,8 @@ function lookupService(address, port, callback) { validateFunction(callback, 'callback'); - port = +port; + // Coerce -0 to +0. + port = +port + 0; const req = new GetNameInfoReqWrap(); req.callback = callback; diff --git a/lib/internal/dns/promises.js b/lib/internal/dns/promises.js index f7ee8fd25423ca..57943e1f658b78 100644 --- a/lib/internal/dns/promises.js +++ b/lib/internal/dns/promises.js @@ -205,7 +205,8 @@ function lookup(hostname, options) { if (typeof options === 'number') { validateOneOf(options, 'family', validFamilies); - family = options; + // Coerce -0 to +0. + family = options + 0; } else if (options !== undefined && typeof options !== 'object') { throw new ERR_INVALID_ARG_TYPE('options', ['integer', 'object'], options); } else { @@ -216,7 +217,8 @@ function lookup(hostname, options) { } if (options?.family != null) { validateOneOf(options.family, 'options.family', validFamilies); - family = options.family; + // Coerce -0 to +0. + family = options.family + 0; } if (options?.all != null) { validateBoolean(options.all, 'options.all'); diff --git a/lib/internal/dns/utils.js b/lib/internal/dns/utils.js index d036c4c7255eab..271731f87c43a2 100644 --- a/lib/internal/dns/utils.js +++ b/lib/internal/dns/utils.js @@ -45,14 +45,18 @@ const { } = require('internal/v8/startup_snapshot'); function validateTimeout(options) { - const { timeout = -1 } = { ...options }; + let { timeout = -1 } = { ...options }; validateInt32(timeout, 'options.timeout', -1); + // Coerce -0 to +0. + timeout += 0; return timeout; } function validateMaxTimeout(options) { - const { maxTimeout = 0 } = { ...options }; + let { maxTimeout = 0 } = { ...options }; validateUint32(maxTimeout, 'options.maxTimeout'); + // Coerce -0 to +0. + maxTimeout += 0; return maxTimeout; } diff --git a/test/parallel/test-dns-negative-zero.js b/test/parallel/test-dns-negative-zero.js new file mode 100644 index 00000000000000..2b07144bc06bf0 --- /dev/null +++ b/test/parallel/test-dns-negative-zero.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); + +const dns = require('dns'); + +dns.lookup('localhost', -0, common.mustCall()); +dns.lookup('localhost', { family: -0 }, common.mustCall()); +dns.lookupService('127.0.0.1', -0, common.mustCall()); + +new dns.Resolver({ timeout: -0 }); +new dns.Resolver({ maxTimeout: -0 }); + +dns.promises.lookup('localhost', { family: -0 }).then(common.mustCall()); From 1ee129df8f2976ff9ab09951a66caaa19eced106 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 25 May 2026 10:23:43 +0200 Subject: [PATCH 4/8] fs: coerce -0 to +0 in mode flags and watch intervals Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63556 Reviewed-By: Jordan Harband Reviewed-By: Chemi Atlow --- lib/internal/fs/utils.js | 3 ++- lib/internal/fs/watchers.js | 2 ++ lib/internal/validators.js | 3 ++- test/parallel/test-fs-negative-zero.js | 33 ++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-fs-negative-zero.js diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index f15b63dc20a367..b8305d8f2dbe05 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -724,7 +724,8 @@ function getStatFsFromBinding(stats) { function stringToFlags(flags, name = 'flags') { if (typeof flags === 'number') { validateInt32(flags, name); - return flags; + // Coerce -0 to +0. + return flags + 0; } if (flags == null) { diff --git a/lib/internal/fs/watchers.js b/lib/internal/fs/watchers.js index c540307d79a52d..2dad214a729c9c 100644 --- a/lib/internal/fs/watchers.js +++ b/lib/internal/fs/watchers.js @@ -176,6 +176,8 @@ StatWatcher.prototype[kFSStatWatcherStart] = function(filename, filename = getValidatedPath(filename, 'filename'); validateUint32(interval, 'interval'); + // Coerce -0 to +0. + interval += 0; const err = this._handle.start(toNamespacedPath(filename), interval); if (err) { const error = new UVException({ diff --git a/lib/internal/validators.js b/lib/internal/validators.js index d2add7faa30a9e..db36251e56f602 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -79,7 +79,8 @@ function parseFileMode(value, name, def) { } validateUint32(value, name); - return value; + // Coerce -0 to +0. + return value + 0; } /** diff --git a/test/parallel/test-fs-negative-zero.js b/test/parallel/test-fs-negative-zero.js new file mode 100644 index 00000000000000..538cea67faaa3c --- /dev/null +++ b/test/parallel/test-fs-negative-zero.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const missing = path.join( + os.tmpdir(), + `node-fs-negative-zero-${process.pid}`, + 'entry', +); + +function ignoreExpectedError(fn) { + try { + fn(); + } catch { + // Ignore expected file system errors from the missing path. + } +} + +const fd = fs.openSync(process.execPath, -0); +fs.closeSync(fd); + +ignoreExpectedError(() => fs.openSync(process.execPath, 'r', -0)); +ignoreExpectedError(() => fs.readFileSync(process.execPath, { flag: -0 })); +ignoreExpectedError(() => fs.mkdirSync(missing, { mode: -0 })); +ignoreExpectedError(() => fs.chmodSync(missing, -0)); +ignoreExpectedError(() => fs.writeFileSync(missing, '', { mode: -0 })); + +fs.watchFile(missing, { interval: -0 }, () => {}); +fs.unwatchFile(missing); From 8bab410245a3a170412a4b23a9eb7607c857a27c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 25 May 2026 10:23:58 +0200 Subject: [PATCH 5/8] net: coerce -0 to +0 in BlockList prefixes Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63556 Reviewed-By: Jordan Harband Reviewed-By: Chemi Atlow --- lib/internal/blocklist.js | 2 ++ test/parallel/test-net-negative-zero.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 test/parallel/test-net-negative-zero.js diff --git a/lib/internal/blocklist.js b/lib/internal/blocklist.js index 776b510dfd3bc6..fd0e7667377b6a 100644 --- a/lib/internal/blocklist.js +++ b/lib/internal/blocklist.js @@ -123,6 +123,8 @@ class BlockList { validateInt32(prefix, 'prefix', 0, 128); break; } + // Coerce -0 to +0. + prefix += 0; this[kHandle].addSubnet(network[kSocketAddressHandle], prefix); } diff --git a/test/parallel/test-net-negative-zero.js b/test/parallel/test-net-negative-zero.js new file mode 100644 index 00000000000000..fa64d1ef4151cc --- /dev/null +++ b/test/parallel/test-net-negative-zero.js @@ -0,0 +1,15 @@ +'use strict'; + +require('../common'); + +const net = require('net'); + +{ + const blockList = new net.BlockList(); + blockList.addSubnet('0.0.0.0', -0); +} + +{ + const blockList = new net.BlockList(); + blockList.addSubnet('::', -0, 'ipv6'); +} From 9e58d9d244126ebd6f086e0dc2dab63719ff1bc6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 25 May 2026 10:24:15 +0200 Subject: [PATCH 6/8] zlib: coerce -0 to +0 for crc32 seeds Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63556 Reviewed-By: Jordan Harband Reviewed-By: Chemi Atlow --- lib/zlib.js | 2 ++ test/parallel/test-zlib-negative-zero.js | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 test/parallel/test-zlib-negative-zero.js diff --git a/lib/zlib.js b/lib/zlib.js index 056b1a13a17392..d4f2446a5976cb 100644 --- a/lib/zlib.js +++ b/lib/zlib.js @@ -969,6 +969,8 @@ function crc32(data, value = 0) { throw new ERR_INVALID_ARG_TYPE('data', ['Buffer', 'TypedArray', 'DataView', 'string'], data); } validateUint32(value, 'value'); + // Coerce -0 to +0. + value += 0; return crc32Native(data, value); } diff --git a/test/parallel/test-zlib-negative-zero.js b/test/parallel/test-zlib-negative-zero.js new file mode 100644 index 00000000000000..dc9cc3175d23b5 --- /dev/null +++ b/test/parallel/test-zlib-negative-zero.js @@ -0,0 +1,8 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const zlib = require('zlib'); + +assert.strictEqual(zlib.crc32('', -0), zlib.crc32('', 0)); From 0fe48b693d78877fc85686f7f76ba99d39c3137f Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 24 May 2026 07:19:35 -0700 Subject: [PATCH 7/8] quic: add listEndpoints API Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/63536 Reviewed-By: Stephen Belanger Reviewed-By: Matteo Collina Reviewed-By: Trivikram Kamat --- doc/api/quic.md | 15 +++ lib/internal/quic/quic.js | 20 ++++ lib/quic.js | 58 +++++------- test/parallel/test-quic-exports.mjs | 6 -- test/parallel/test-quic-list-endpoints.mjs | 105 +++++++++++++++++++++ 5 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 test/parallel/test-quic-list-endpoints.mjs diff --git a/doc/api/quic.md b/doc/api/quic.md index 5ecac022a3c83b..9de625030fc180 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -529,6 +529,21 @@ with either a `QuicEndpoint` or `EndpointOptions` as the argument. At most, any single `QuicEndpoint` can only be configured to listen as a server once. +## `quic.listEndpoints([options])` + + + +* `options` {object} + * `active` {boolean} If `true` (the default), only returns endpoints that are + active (not destroyed, not closing, and not busy). If `false` returns all + endpoints. +* Returns: {quic.QuicEndpoint\[]} + +Returns the list of all `QuicEndpoint` instances. By default, only active +endpoints are returned. + ## `quic.constants`