From 071d46d3ddf5ae576e09be7fe1c18b2b0659e56a Mon Sep 17 00:00:00 2001 From: Y1D7NG Date: Tue, 18 Aug 2026 05:03:39 +0800 Subject: [PATCH 01/97] fs: fix close listener leak in FileHandle streams Fixes: https://github.com/nodejs/node/issues/64214 Signed-off-by: y1d7ng PR-URL: https://github.com/nodejs/node/pull/64227 Reviewed-By: Chemi Atlow Reviewed-By: Claudio Wunder --- lib/internal/fs/streams.js | 14 ++++++- .../test-fs-promises-file-handle-stream.js | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/internal/fs/streams.js b/lib/internal/fs/streams.js index 30617b3b5937..cbf247b3523c 100644 --- a/lib/internal/fs/streams.js +++ b/lib/internal/fs/streams.js @@ -159,7 +159,19 @@ function importFd(stream, options) { stream[kHandle] = options.fd; stream[kFs] = FileHandleOperations(stream[kHandle]); stream[kHandle][kRef](); - options.fd.on('close', FunctionPrototypeBind(stream.close, stream)); + + const onclose = FunctionPrototypeBind(stream.close, stream); + options.fd.on('close', onclose); + if (options.autoClose === false) { + function cleanup() { + options.fd.removeListener('close', onclose); + options.fd[kUnref](); + } + stream.once('end', cleanup); + stream.once('finish', cleanup); + stream.once('error', cleanup); + } + return options.fd.fd; } diff --git a/test/parallel/test-fs-promises-file-handle-stream.js b/test/parallel/test-fs-promises-file-handle-stream.js index 71f312b6f9d7..61d0b3ca2ec7 100644 --- a/test/parallel/test-fs-promises-file-handle-stream.js +++ b/test/parallel/test-fs-promises-file-handle-stream.js @@ -42,7 +42,46 @@ async function validateRead() { ); } +async function validateReusedCreateReadStream() { + const filePath = path.resolve(tmpDir, 'tmp-reused-stream.txt'); + fs.writeFileSync(filePath, Buffer.from('ab', 'utf8')); + + const fileHandle = await open(filePath, 'r'); + try { + await buffer(fileHandle.createReadStream({ + start: 0, + end: 0, + autoClose: false, + })); + assert.strictEqual(fileHandle.listenerCount('close'), 0); + + await buffer(fileHandle.createReadStream({ + start: 1, + end: 1, + autoClose: false, + })); + assert.strictEqual(fileHandle.listenerCount('close'), 0); + } finally { + await fileHandle.close(); + } +} + +async function validateReusedCreateWriteStream() { + const filePath = path.resolve(tmpDir, 'tmp-reused-write-stream.txt'); + const fileHandle = await open(filePath, 'w'); + try { + const stream = fileHandle.createWriteStream({ autoClose: false }); + stream.end('a'); + await finished(stream); + assert.strictEqual(fileHandle.listenerCount('close'), 0); + } finally { + await fileHandle.close(); + } +} + Promise.all([ validateWrite(), validateRead(), + validateReusedCreateReadStream(), + validateReusedCreateWriteStream(), ]).then(common.mustCall()); From a2a91dce841418e6a145378ac5ed91c108b71bba Mon Sep 17 00:00:00 2001 From: ulofiai Date: Tue, 18 Aug 2026 05:45:03 +0800 Subject: [PATCH 02/97] build: pass target architecture to small-icu genccode Signed-off-by: ulofiai PR-URL: https://github.com/nodejs/node/pull/65095 Reviewed-By: Aviv Keller Reviewed-By: Stefan Stojanovic --- tools/icu/icu-generic.gyp | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/icu/icu-generic.gyp b/tools/icu/icu-generic.gyp index f49b4ddba74a..c4e8c6fbb9f8 100644 --- a/tools/icu/icu-generic.gyp +++ b/tools/icu/icu-generic.gyp @@ -208,6 +208,7 @@ 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/icudt<(icu_ver_major)<(icu_endianness)_dat.<(icu_asm_ext)' ], 'action': [ '<(PRODUCT_DIR)/genccode<(EXECUTABLE_SUFFIX)', '<@(icu_asm_opts)', # -o + '-c', '<(target_arch)', '-d', '<(SHARED_INTERMEDIATE_DIR)/', '-n', 'icudata', '-e', 'icusmdt<(icu_ver_major)', From 8667c0077d5d243b92e59536941c57e413f69a62 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:54:05 -0700 Subject: [PATCH 03/97] test: avoid timer race in event loop delay test An expired timer can run before the first complete event loop iteration, disabling the histogram before it records any samples. Drive a known number of iterations with setImmediate before checking the histograms, and share the chain between resolution variants. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/64728 Refs: https://github.com/nodejs/reliability/issues?q=sort%3Aupdated-desc%20test-performance-eventloopdelay Reviewed-By: Filip Skokan --- .../test-performance-eventloopdelay.js | 85 +++++++++---------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/test/sequential/test-performance-eventloopdelay.js b/test/sequential/test-performance-eventloopdelay.js index ddd33372ec5e..ede729514937 100644 --- a/test/sequential/test-performance-eventloopdelay.js +++ b/test/sequential/test-performance-eventloopdelay.js @@ -9,6 +9,18 @@ const { } = require('perf_hooks'); const { sleep } = require('internal/util'); +function runEventLoopIterations(iterations, callback) { + let remaining = iterations; + function tick() { + if (--remaining > 0) { + setImmediate(tick); + } else { + callback(); + } + } + setImmediate(tick); +} + { const histogram = monitorEventLoopDelay(); assert(histogram); @@ -125,12 +137,16 @@ const { sleep } = require('internal/util'); } { + const iterations = 10; const histogram = monitorEventLoopDelay({ samplePerIteration: true }); histogram.enable(); - setTimeout(common.mustCall(() => { + runEventLoopIterations(iterations, common.mustCall(() => { histogram.disable(); - assert(histogram.count > 0, - `Expected samples to be recorded, got count=${histogram.count}`); + assert( + histogram.count >= iterations - 1, + `Expected at least ${iterations - 1} samples for ${iterations} iterations, ` + + `got ${histogram.count}` + ); assert(histogram.min > 0); assert(histogram.max > 0); assert(histogram.mean > 0); @@ -146,7 +162,7 @@ const { sleep } = require('internal/util'); assert(Number.isNaN(histogram.mean)); assert(Number.isNaN(histogram.stddev)); assert.strictEqual(histogram.percentiles.size, 1); - }), common.platformTimeout(20)); + })); } { @@ -158,65 +174,40 @@ const { sleep } = require('internal/util'); assert.strictEqual(histogram.disable(), false); // Already disabled, no-op // Re-enabling after disable should work assert.strictEqual(histogram.enable(), true); - setTimeout(common.mustCall(() => { + runEventLoopIterations(10, common.mustCall(() => { histogram.disable(); assert(histogram.count > 0, `Expected samples after re-enable, got count=${histogram.count}`); - }), common.platformTimeout(20)); + })); } { // Verify that samplePerIteration records exactly one sample per event loop iteration. - const N = 10; + // It should do so independently of the timer resolution used by the legacy + // monitorEventLoopDelay path. + const iterations = 10; const histogram = monitorEventLoopDelay({ samplePerIteration: true }); - histogram.enable(); - - let iterations = 0; - const verify = common.mustCall(() => { - histogram.disable(); - assert( - histogram.count >= N - 1, - `Expected at least ${N - 1} samples for ${N} iterations, got ${histogram.count}` - ); - }); - - function tick() { - if (++iterations < N) { - setImmediate(tick); - } else { - verify(); - } - } - setImmediate(tick); -} - -{ - // samplePerIteration should sample per event loop iteration, independent of - // the timer resolution used by the legacy monitorEventLoopDelay path. - const N = 10; - const histogram = monitorEventLoopDelay({ + const largeResolutionHistogram = monitorEventLoopDelay({ samplePerIteration: true, resolution: 60 * 1000, }); histogram.enable(); + largeResolutionHistogram.enable(); - let iterations = 0; - const verify = common.mustCall(() => { + runEventLoopIterations(iterations, common.mustCall(() => { histogram.disable(); + largeResolutionHistogram.disable(); assert( - histogram.count >= N - 1, - `Expected samples despite large resolution, got count=${histogram.count}` + histogram.count >= iterations - 1, + `Expected at least ${iterations - 1} samples for ${iterations} iterations, ` + + `got ${histogram.count}` ); - }); - - function tick() { - if (++iterations < N) { - setImmediate(tick); - } else { - verify(); - } - } - setImmediate(tick); + assert( + largeResolutionHistogram.count >= iterations - 1, + `Expected samples despite large resolution, ` + + `got count=${largeResolutionHistogram.count}` + ); + })); } // Make sure that the histogram instances can be garbage-collected without From 45e8e4ab62f90e16dc074853e36f459e0855da7d Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Mon, 17 Aug 2026 20:33:43 -0400 Subject: [PATCH 04/97] deps: update googletest to 49495eacfdbda3f4b6ba219923fedbb2e3f99376 PR-URL: https://github.com/nodejs/node/pull/65317 Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca Reviewed-By: Chemi Atlow --- .../googletest/include/gtest/gtest-printers.h | 11 +-- .../include/gtest/internal/gtest-port.h | 75 +++++++++++++++---- deps/googletest/src/gtest-printers.cc | 25 ++++++- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/deps/googletest/include/gtest/gtest-printers.h b/deps/googletest/include/gtest/gtest-printers.h index 315ce0a51016..69c9fec3ca95 100644 --- a/deps/googletest/include/gtest/gtest-printers.h +++ b/deps/googletest/include/gtest/gtest-printers.h @@ -1173,15 +1173,12 @@ class [[nodiscard]] UniversalTersePrinter { } } }; -#endif template <> -class [[nodiscard]] UniversalTersePrinter { - public: - static void Print(wchar_t* str, ::std::ostream* os) { - UniversalTersePrinter::Print(str, os); - } -}; +class [[nodiscard]] UniversalTersePrinter + : public UniversalTersePrinter {}; + +#endif // GTEST_HAS_STD_WSTRING template void UniversalTersePrint(const T& value, ::std::ostream* os) { diff --git a/deps/googletest/include/gtest/internal/gtest-port.h b/deps/googletest/include/gtest/internal/gtest-port.h index 92e6591d2cec..051228553449 100644 --- a/deps/googletest/include/gtest/internal/gtest-port.h +++ b/deps/googletest/include/gtest/internal/gtest-port.h @@ -500,22 +500,71 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION; #endif // defined(_MSC_VER) || defined(__BORLANDC__) #endif // GTEST_HAS_EXCEPTIONS -#ifndef GTEST_HAS_STD_WSTRING -// The user didn't tell us whether ::std::wstring is available, so we need -// to figure it out. +// 1. Calculate default GTEST_HAS_STD_WSTRING values based on STL capabilities. +#if defined(_MSVC_STL_VERSION) +// Microsoft's STL implementation always supports ::std::wstring. +#define GTEST_HAS_STD_WSTRING_DEFAULT 1 + +#elif defined(_LIBCPP_VERSION) +// Modern libc++ always defines _LIBCPP_HAS_WIDE_CHARACTERS; its value +// determines whether wide characters are supported. +// Older libc++ omits a definition for _LIBCPP_HAS_NO_WIDE_CHARACTERS when wide +// characters are supported. +#if (defined(_LIBCPP_HAS_WIDE_CHARACTERS) && !_LIBCPP_HAS_WIDE_CHARACTERS) || \ + defined(_LIBCPP_HAS_NO_WIDE_CHARACTERS) +#define GTEST_HAS_STD_WSTRING_DEFAULT 0 +#else +#define GTEST_HAS_STD_WSTRING_DEFAULT 1 +#endif + +#elif defined(__GLIBCXX__) +#if defined(_GLIBCXX_USE_WCHAR_T) && _GLIBCXX_USE_WCHAR_T +#define GTEST_HAS_STD_WSTRING_DEFAULT 1 +#else +#define GTEST_HAS_STD_WSTRING_DEFAULT 0 +#endif + +#else +// Unknown standard library implementation; fall back looking at the OS. +// +// Always let the user override the defaults in this case; they might have more +// information about what's supported than we do. +#if defined(GTEST_OS_LINUX_ANDROID) +// Android started supporting std::wstring with API Level 21 (Lollipop). +#define GTEST_HAS_STD_WSTRING_DEFAULT (__ANDROID_API__ >= 21) +// The following platforms are known not to support ::std::wstring; assume it's +// supported on all others. +// // Cygwin 1.7 and below doesn't support ::std::wstring. -// Solaris' libc++ doesn't support it either. Android has -// no support for it at least as recent as Froyo (2.2). -#if (!(defined(GTEST_OS_LINUX_ANDROID) || defined(GTEST_OS_CYGWIN) || \ - defined(GTEST_OS_SOLARIS) || defined(GTEST_OS_HAIKU) || \ - defined(GTEST_OS_ESP32) || defined(GTEST_OS_ESP8266) || \ - defined(GTEST_OS_XTENSA) || defined(GTEST_OS_QURT) || \ - defined(GTEST_OS_NXP_QN9090) || defined(GTEST_OS_NRF52))) -#define GTEST_HAS_STD_WSTRING 1 +// Solaris' libc++ doesn't support it either. +#elif defined(GTEST_OS_CYGWIN) || defined(GTEST_OS_SOLARIS) || \ + defined(GTEST_OS_HAIKU) || defined(GTEST_OS_ESP32) || \ + defined(GTEST_OS_ESP8266) || defined(GTEST_OS_XTENSA) || \ + defined(GTEST_OS_QURT) || defined(GTEST_OS_NXP_QN9090) || \ + defined(GTEST_OS_NRF52) +#define GTEST_HAS_STD_WSTRING_DEFAULT 0 #else -#define GTEST_HAS_STD_WSTRING 0 +#define GTEST_HAS_STD_WSTRING_DEFAULT 1 +#endif +#endif + +// 2. Validate explicit user overrides (if user passed -DGTEST_HAS_*=1) against +// what the standard library implementation tells us it supports. +#if defined(GTEST_HAS_STD_WSTRING) && GTEST_HAS_STD_WSTRING +#if defined(_LIBCPP_VERSION) && \ + ((defined(_LIBCPP_HAS_WIDE_CHARACTERS) && !_LIBCPP_HAS_WIDE_CHARACTERS) || \ + defined(_LIBCPP_HAS_NO_WIDE_CHARACTERS)) +#error Cannot explicitly enable GTEST_HAS_STD_WSTRING without libc++ wide character support. +#elif defined(__GLIBCXX__) && \ + !(defined(_GLIBCXX_USE_WCHAR_T) && _GLIBCXX_USE_WCHAR_T) +#error Cannot explicitly enable GTEST_HAS_STD_WSTRING without libstdc++ wide character support. +#endif +#endif + +// 3. Set final values if not explicitly overridden by user +#if !defined(GTEST_HAS_STD_WSTRING) +#define GTEST_HAS_STD_WSTRING GTEST_HAS_STD_WSTRING_DEFAULT #endif -#endif // GTEST_HAS_STD_WSTRING #ifndef GTEST_HAS_FILE_SYSTEM // Most platforms support a file system. diff --git a/deps/googletest/src/gtest-printers.cc b/deps/googletest/src/gtest-printers.cc index 7c0ecc6ad1c9..975ebb829876 100644 --- a/deps/googletest/src/gtest-printers.cc +++ b/deps/googletest/src/gtest-printers.cc @@ -50,6 +50,7 @@ #include #include #include // NOLINT +#include #include #include @@ -422,6 +423,28 @@ void UniversalPrintArray(const wchar_t* begin, size_t len, ostream* os) { namespace { +template +size_t GetLength(const Char* s) { + return std::char_traits::length(s); +} + +#if !GTEST_HAS_STD_WSTRING + +// If GTEST_HAS_STD_WSTRING is unset because the standard library has disabled +// wide character support, std::char_traits won't be defined, which +// will cause a compile error, even if user code never actually could print a +// wide cstring. In that case, instead use `wcslen` directly. +// +// If `libc` _also_ lacks wide character support, this (and a bunch of other +// calls to wc functions) will fail to link, but only if user code actually +// uses them. +template <> +size_t GetLength(const wchar_t* s) { + return wcslen(s); +} + +#endif // GTEST_HAS_STD_WSTRING + // Prints a null-terminated C-style string to the ostream. template void PrintCStringTo(const Char* s, ostream* os) { @@ -429,7 +452,7 @@ void PrintCStringTo(const Char* s, ostream* os) { *os << "NULL"; } else { *os << ImplicitCast_(s) << " pointing to "; - PrintCharsAsStringTo(s, std::char_traits::length(s), os); + PrintCharsAsStringTo(s, GetLength(s), os); } } From e9eacdf66f358f92e0fe3958322de271b6cc8807 Mon Sep 17 00:00:00 2001 From: Yuya Inoue <65857152+inoway46@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:04:36 +0900 Subject: [PATCH 05/97] doc: add missing return types in buffer.md Add return types for Blob methods and legacy Base64 helpers so doc-kit does not render them as `void`. Refs: nodejs/doc-kit#953 Signed-off-by: inoway46 PR-URL: https://github.com/nodejs/node/pull/65308 Refs: https://github.com/nodejs/doc-kit/issues/953 Reviewed-By: Luigi Pinca Reviewed-By: Trivikram Kamat --- doc/api/buffer.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 8aed7ddc8b59..e8337e73afef 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -536,6 +536,8 @@ added: - v20.16.0 --> +* Returns: {Promise} + The `blob.bytes()` method returns the byte of the `Blob` object as a `Promise`. ```js @@ -566,6 +568,7 @@ added: * `start` {number} The starting index. * `end` {number} The ending index. * `type` {string} The content-type for the new `Blob` +* Returns: {Blob} Creates and returns a new `Blob` containing a subset of this `Blob` objects data. The original `Blob` is not altered. @@ -5248,6 +5251,7 @@ added: > Stability: 3 - Legacy. Use `Buffer.from(data, 'base64')` instead. * `data` {any} The Base64-encoded input string. +* Returns: {string} Decodes a string of Base64-encoded data into bytes, and encodes those bytes into a string using Latin-1 (ISO-8859-1). @@ -5278,6 +5282,7 @@ added: > Stability: 3 - Legacy. Use `buf.toString('base64')` instead. * `data` {any} An ASCII (Latin1) string. +* Returns: {string} Decodes a string into bytes using Latin-1 (ISO-8859), and encodes those bytes into a string using Base64. From 0f4ab49c0ed5369aef803de02ce762bde59d6581 Mon Sep 17 00:00:00 2001 From: Chaseton Collins <43923165+chasetonco@users.noreply.github.com> Date: Tue, 18 Aug 2026 03:04:46 -0400 Subject: [PATCH 06/97] doc: add missing return types in fs.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three entries in the fs documentation described their return value only in prose, or not at all, so doc-kit could not parse a return type and fell back to `void`: * `filehandle[Symbol.asyncDispose]()` and `dir[Symbol.asyncDispose]()` both return a promise, matching the existing `Returns: {Promise}` annotations on other async dispose methods. * `new fs.Utf8Stream([options])` is a constructor and returns an instance of the class. Verified at runtime and by rendering the page locally with doc-kit. Refs: https://github.com/nodejs/doc-kit/issues/953 Signed-off-by: Chxxeton <43923165+Chxxeton@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/65307 Reviewed-By: James M Snell Reviewed-By: Luigi Pinca Reviewed-By: Aviv Keller Reviewed-By: Ulises Gascón --- doc/api/fs.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/api/fs.md b/doc/api/fs.md index ab1cc3dbe6aa..11a4e3ee7ef5 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1184,6 +1184,8 @@ changes: description: No longer experimental. --> +* Returns: {Promise} + Calls `filehandle.close()` and returns a promise that fulfills when the filehandle is closed. @@ -7317,6 +7319,8 @@ changes: description: No longer experimental. --> +* Returns: {Promise} + Calls `dir.close()` if the directory handle is open, and returns a promise that fulfills when disposal is complete. From 6384318086384153a0c110255f8d08d6e1b45f64 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Tue, 18 Aug 2026 03:05:05 -0400 Subject: [PATCH 07/97] doc: document setRawMode write access on Windows Fixes: https://github.com/nodejs/node/issues/63852 Signed-off-by: Erik Demaine PR-URL: https://github.com/nodejs/node/pull/63856 Reviewed-By: Stefan Stojanovic Reviewed-By: Trivikram Kamat --- doc/api/tty.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/api/tty.md b/doc/api/tty.md index 03f86cd66052..cbfb3cc78377 100644 --- a/doc/api/tty.md +++ b/doc/api/tty.md @@ -86,6 +86,11 @@ characters. Ctrl+C will no longer cause a `SIGINT` when in this mode. This mode does not affect terminal output processing, such as newline translation on Unix terminals. +On Windows, `setRawMode()` requires write permission to the console input +buffer. When opening `"\\\\.\\CONIN$"` with the [`fs.open()`][] family of APIs +(for passing into `new tty.ReadStream()`), be sure to use a read/write flag +such as `'r+'`. + ## Class: `tty.WriteStream` \n`; } const attrsString = ArrayPrototypeJoin( diff --git a/test/fixtures/test-runner/output/junit_empty_diagnostic.js b/test/fixtures/test-runner/output/junit_empty_diagnostic.js new file mode 100644 index 000000000000..491c2f230d98 --- /dev/null +++ b/test/fixtures/test-runner/output/junit_empty_diagnostic.js @@ -0,0 +1,8 @@ +// Flags: --test --test-reporter=junit +'use strict'; +const test = require('node:test'); + +test('failing', (t) => { + t.diagnostic(''); + throw new Error('error'); +}); diff --git a/test/fixtures/test-runner/output/junit_empty_diagnostic.snapshot b/test/fixtures/test-runner/output/junit_empty_diagnostic.snapshot new file mode 100644 index 000000000000..666c5c3523a7 --- /dev/null +++ b/test/fixtures/test-runner/output/junit_empty_diagnostic.snapshot @@ -0,0 +1,23 @@ + + + + +[Error [ERR_TEST_FAILURE]: error] { + code: 'ERR_TEST_FAILURE', + failureType: 'testCodeFailure', + cause: Error: error + at TestContext.<anonymous> (/test/fixtures/test-runner/output/junit_empty_diagnostic.js:7:9) + at +} + + + + + + + + + + + + diff --git a/test/test-runner/test-output-junit-empty-diagnostic.mjs b/test/test-runner/test-output-junit-empty-diagnostic.mjs new file mode 100644 index 000000000000..5c1cf4a2b382 --- /dev/null +++ b/test/test-runner/test-output-junit-empty-diagnostic.mjs @@ -0,0 +1,11 @@ +// Test that the output of test-runner/output/junit_empty_diagnostic.js matches +// test-runner/output/junit_empty_diagnostic.snapshot +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawnAndAssert, junitTransform, ensureCwdIsProjectRoot } from '../common/assertSnapshot.js'; + +ensureCwdIsProjectRoot(); +await spawnAndAssert( + fixtures.path('test-runner/output/junit_empty_diagnostic.js'), + junitTransform, +); From 0a033d41eacd1b54b9f58f1d658cab4afce0e961 Mon Sep 17 00:00:00 2001 From: Vedant Kulkarni Date: Wed, 19 Aug 2026 20:59:57 +0000 Subject: [PATCH 18/97] doc: clarify that ipv4 mapped to ipv6 are classified as ipv6 Signed-off-by: Vedant Kulkarni PR-URL: https://github.com/nodejs/node/pull/62117 Reviewed-By: Ethan Arrowood Reviewed-By: Trivikram Kamat --- doc/api/net.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/api/net.md b/doc/api/net.md index 74b34a9e847d..8348f0158818 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -2375,12 +2375,13 @@ added: v0.3.0 * `input` {string} * Returns: {integer} -Returns `6` if `input` is an IPv6 address. Returns `4` if `input` is an IPv4 -address in [dot-decimal notation][] with no leading zeroes. Otherwise, returns -`0`. +Returns `6` if `input` is an IPv6 address, including an IPv4-mapped IPv6 address. +Returns `4` if `input` is an IPv4 address in [dot-decimal notation][] with no +leading zeroes. Otherwise, returns `0`. ```js net.isIP('::1'); // returns 6 +net.isIP('::ffff:127.0.0.1'); // returns 6 net.isIP('127.0.0.1'); // returns 4 net.isIP('127.000.000.001'); // returns 0 net.isIP('127.0.0.1/24'); // returns 0 @@ -2415,10 +2416,12 @@ added: v0.3.0 * `input` {string} * Returns: {boolean} -Returns `true` if `input` is an IPv6 address. Otherwise, returns `false`. +Returns `true` if `input` is an IPv6 address, including an IPv4-mapped IPv6 address. +Otherwise, returns `false`. ```js net.isIPv6('::1'); // returns true +net.isIPv6('::ffff:127.0.0.1'); // returns true net.isIPv6('fhqwhgads'); // returns false ``` From cfd72090f21101393b5937f5df1a3c556640289f Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:03:04 -0700 Subject: [PATCH 19/97] diagnostics_channel: validate before channel activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validate the first subscription before calling markActive(). This prevents invalid callbacks from leaving channels active and notifying native consumers without an installed subscriber. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/65313 Fixes: https://github.com/nodejs/node/issues/65312 Reviewed-By: James M Snell Reviewed-By: Gerhard Stöbich --- lib/diagnostics_channel.js | 1 + .../parallel/test-diagnostics-channel-object-channel-pub-sub.js | 2 ++ test/parallel/test-diagnostics-channel-pub-sub.js | 2 ++ 3 files changed, 5 insertions(+) diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index d26a5103a46e..bd4c9b4a9267 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -205,6 +205,7 @@ class Channel { } subscribe(subscription) { + validateFunction(subscription, 'subscription'); markActive(this); this.subscribe(subscription); } diff --git a/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js b/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js index 9498419b806c..f02544936559 100644 --- a/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js +++ b/test/parallel/test-diagnostics-channel-object-channel-pub-sub.js @@ -44,3 +44,5 @@ assert.ok(!channel.unsubscribe(subscriber)); assert.throws(() => { channel.subscribe(null); }, { code: 'ERR_INVALID_ARG_TYPE' }); +assert.ok(!channel.hasSubscribers); +assert.ok(!dc.hasSubscribers('test')); diff --git a/test/parallel/test-diagnostics-channel-pub-sub.js b/test/parallel/test-diagnostics-channel-pub-sub.js index a7232ab58ce8..e3a868b7ce0c 100644 --- a/test/parallel/test-diagnostics-channel-pub-sub.js +++ b/test/parallel/test-diagnostics-channel-pub-sub.js @@ -42,6 +42,8 @@ assert.ok(!dc.unsubscribe(name, subscriber)); assert.throws(() => { dc.subscribe(name, null); }, { code: 'ERR_INVALID_ARG_TYPE' }); +assert.ok(!channel.hasSubscribers); +assert.ok(!dc.hasSubscribers(name)); // Reaching zero subscribers should not delete from the channels map as there // will be no more weakref to incRef if another subscribe happens while the From b80af365c0dcad2d36c56b39b4f3213a99a73958 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 19 Aug 2026 19:56:00 -0800 Subject: [PATCH 20/97] stream: decouple transform backpressure changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/65143 Reviewed-By: James M Snell Reviewed-By: Gürgün Dayıoğlu --- benchmark/webstreams/pipe-through.js | 38 +++++ lib/internal/webstreams/transformstream.js | 153 +++++++++++++++------ lib/internal/webstreams/util.js | 9 ++ 3 files changed, 156 insertions(+), 44 deletions(-) create mode 100644 benchmark/webstreams/pipe-through.js diff --git a/benchmark/webstreams/pipe-through.js b/benchmark/webstreams/pipe-through.js new file mode 100644 index 000000000000..8af088f4eed1 --- /dev/null +++ b/benchmark/webstreams/pipe-through.js @@ -0,0 +1,38 @@ +'use strict'; +const common = require('../common.js'); +const { + ReadableStream, + TransformStream, +} = require('node:stream/web'); + +const bench = common.createBenchmark(main, { + n: [5e5], + kind: ['default', 'transform'], +}); + +async function main({ n, kind }) { + const b = Buffer.alloc(64); + let i = 0; + const rs = new ReadableStream({ + pull(controller) { + if (i++ < n) { + controller.enqueue(b); + } else { + controller.close(); + } + }, + }); + const ts = kind === 'default' ? + new TransformStream() : + new TransformStream({ + transform(chunk, controller) { controller.enqueue(chunk); }, + }); + + const reader = rs.pipeThrough(ts).getReader(); + bench.start(); + for (;;) { + const { done } = await reader.read(); + if (done) break; + } + bench.end(n); +} diff --git a/lib/internal/webstreams/transformstream.js b/lib/internal/webstreams/transformstream.js index 535c783a3a31..30b7b1c8fac1 100644 --- a/lib/internal/webstreams/transformstream.js +++ b/lib/internal/webstreams/transformstream.js @@ -5,6 +5,8 @@ const { ObjectDefineProperties, ObjectSetPrototypeOf, PromisePrototypeThen, + PromiseReject, + PromiseResolve, PromiseWithResolvers, Symbol, SymbolToStringTag, @@ -44,12 +46,14 @@ const { const { createPromiseCallback1Param, - createPromiseCallback2Params, + createRawCallback2Params, customInspect, extractHighWaterMark, extractSizeAlgorithm, getNonWritablePropertyDescriptor, isBrandCheck, + kParkedAlgorithmResult, + kResolvedPromise, kState, kType, nonOpCancel, @@ -258,7 +262,10 @@ function InternalTransferredTransformStream() { readable: undefined, writable: undefined, backpressure: undefined, - backpressureChange: undefined, + pullPending: false, + pendingWrite: undefined, + pendingWriteChunk: undefined, + writeContinuation: undefined, controller: undefined, }; } @@ -348,7 +355,9 @@ const isTransformStream = const isTransformStreamDefaultController = isBrandCheck('TransformStreamDefaultController'); -async function defaultTransformAlgorithm(chunk, controller) { +// Raw callback (see createRawCallback*): invoked inside the try/catch of +// transformStreamDefaultControllerPerformTransform. +function defaultTransformAlgorithm(chunk, controller) { transformStreamDefaultControllerEnqueue(controller, chunk); } @@ -385,7 +394,12 @@ function initializeTransformStream( writable, controller: undefined, backpressure: undefined, - backpressureChange: undefined, + // Continuation slots replacing the spec's + // [[backpressureChangePromise]]; see transformStreamSetBackpressure. + pullPending: false, + pendingWrite: undefined, + pendingWriteChunk: undefined, + writeContinuation: undefined, }; transformStreamSetBackpressure(stream, true); @@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) { // The spec's [[backpressureChangePromise]] is only ever observed by the // source pull algorithm (settles when backpressure next becomes true) and // by a sink write arriving while backpressure is set (settles when -// backpressure next becomes false). Instead of allocating a fresh promise -// record on every flip, the record is materialized lazily on first -// observation and dropped once settled; flips nobody is waiting on -// allocate nothing. -function transformStreamBackpressureChangePromise(stream) { - const state = stream[kState]; - return (state.backpressureChange ??= PromiseWithResolvers()).promise; -} - +// backpressure next becomes false). Both observers are internal, so the +// promise record is replaced by continuation slots: a parked pull is +// completed by delivering the readable controller's pull-fulfilled step, +// and a parked write by the cached write continuation (see +// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the +// shared resolved promise at the exact microtask position the old +// record's reaction would have had. function transformStreamSetBackpressure(stream, backpressure) { const state = stream[kState]; assert(state.backpressure !== backpressure); - const backpressureChange = state.backpressureChange; - if (backpressureChange !== undefined) { - state.backpressureChange = undefined; - backpressureChange.resolve(); - } state.backpressure = backpressure; + if (backpressure) { + if (state.pullPending) { + state.pullPending = false; + // The pull-fulfilled step exists: a pull parked it (see + // transformStreamDefaultSourcePullAlgorithm), and the readable + // controller creates it before invoking the pull algorithm. + PromisePrototypeThen( + kResolvedPromise, + state.readable[kState].controller[kState].pullFulfilled); + } + } else if (state.pendingWrite !== undefined) { + PromisePrototypeThen(kResolvedPromise, state.writeContinuation); + } } function setupTransformStreamDefaultController( @@ -456,6 +476,7 @@ function setupTransformStreamDefaultController( transformAlgorithm, flushAlgorithm, cancelAlgorithm, + performTransformRejected: undefined, }; stream[kState].controller = controller; } @@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer( const flush = transformer?.flush; const cancel = transformer?.cancel; const transformAlgorithm = transform ? - createPromiseCallback2Params('transformer.transform', transform, transformer) : + createRawCallback2Params('transformer.transform', transform, transformer) : defaultTransformAlgorithm; const flushAlgorithm = flush ? createPromiseCallback1Param('transformer.flush', flush, transformer) : @@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) { transformStreamError(controller[kState].stream, error); } -async function transformStreamDefaultControllerPerformTransform(controller, chunk) { +// Mirrors the reference implementation's +// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`: +// the returned promise settles one microtask after the (coerced) result +// does, and a rejection errors the transform stream before propagating. +// The raw transform callback plus the shared resolved promise for +// non-thenable results replace the previous async wrapper's two implicit +// promises per chunk. +function transformStreamDefaultControllerPerformTransform(controller, chunk) { + const controllerState = controller[kState]; + const transformAlgorithm = controllerState.transformAlgorithm; + if (transformAlgorithm === undefined) { + // Algorithms were cleared by a concurrent cancel/abort/close. + return kResolvedPromise; + } + let result; try { - const transformAlgorithm = controller[kState].transformAlgorithm; - if (transformAlgorithm === undefined) { - // Algorithms were cleared by a concurrent cancel/abort/close. - return; - } - return await transformAlgorithm(chunk, controller); + result = transformAlgorithm(chunk, controller); } catch (error) { + result = PromiseReject(error); + } + if (result === null || + (typeof result !== 'object' && typeof result !== 'function')) { + result = kResolvedPromise; + } else { + result = PromiseResolve(result); + } + controllerState.performTransformRejected ??= (error) => { transformStreamError(controller[kState].stream, error); throw error; - } + }; + return PromisePrototypeThen( + result, + undefined, + controllerState.performTransformRejected); } function transformStreamDefaultControllerTerminate(controller) { @@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) { } function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) { + const state = stream[kState]; const { writable, controller, - } = stream[kState]; + } = state; assert(writable[kState].state === 'writable'); - if (stream[kState].backpressure) { - const backpressureChange = transformStreamBackpressureChangePromise(stream); - return PromisePrototypeThen( - backpressureChange, - () => { - const { - writable, - } = stream[kState]; - if (writable[kState].state === 'erroring') - throw writable[kState].storedError; - assert(writable[kState].state === 'writable'); - return transformStreamDefaultControllerPerformTransform( + if (state.backpressure) { + // Park the chunk and one promise record; the backpressure -> false + // flip delivers the cached continuation (see + // transformStreamSetBackpressure) at the same microtask position as + // the old [[backpressureChangePromise]] reaction. The continuation + // resolves the sink promise with the perform-transform promise, so + // adoption reproduces the old derived-chain settle depth exactly. + // The writable dispatches a single write at a time, so one pending + // slot suffices. + assert(state.pendingWrite === undefined); + const pendingWrite = PromiseWithResolvers(); + state.pendingWrite = pendingWrite; + state.pendingWriteChunk = chunk; + state.writeContinuation ??= () => { + const pending = state.pendingWrite; + const pendingChunk = state.pendingWriteChunk; + state.pendingWrite = undefined; + state.pendingWriteChunk = undefined; + const writableState = state.writable[kState]; + if (writableState.state === 'erroring') { + pending.reject(writableState.storedError); + return; + } + assert(writableState.state === 'writable'); + pending.resolve( + transformStreamDefaultControllerPerformTransform( controller, - chunk); - }); + pendingChunk)); + }; + return pendingWrite.promise; } return transformStreamDefaultControllerPerformTransform(controller, chunk); } @@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) { } function transformStreamDefaultSourcePullAlgorithm(stream) { - assert(stream[kState].backpressure); + const state = stream[kState]; + assert(state.backpressure); transformStreamSetBackpressure(stream, false); - return transformStreamBackpressureChangePromise(stream); + // Park the pull: the next backpressure -> true flip delivers the + // pull-fulfilled step (see transformStreamSetBackpressure). The old + // [[backpressureChangePromise]] this replaces was only ever resolved, + // so the parked pull needs no rejection delivery. + state.pullPending = true; + return kParkedAlgorithmResult; } function transformStreamDefaultSourceCancelAlgorithm(stream, reason) { diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js index 05439a25dcb5..9598796f35c8 100644 --- a/lib/internal/webstreams/util.js +++ b/lib/internal/webstreams/util.js @@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) { // the next microtask checkpoint without allocating a fresh promise. const kResolvedPromise = PromiseResolve(); +// Returned by an internal algorithm to signal that it parked the +// operation and takes responsibility for delivering the fulfilled (or +// rejected) continuation itself later, instead of settling a promise +// (see the transform stream source pull algorithm). +const kParkedAlgorithmResult = { __proto__: null }; + // Wires the (possibly non-thenable) result of an underlying algorithm // callback to its fulfilled/rejected continuations. A non-thenable result // means fulfillment is guaranteed and no then() lookup is observable, so @@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve(); // matches the spec's "a promise resolved with" conversion (identity for // native promises). function thenAlgorithmResult(result, onFulfilled, onRejected) { + if (result === kParkedAlgorithmResult) + return; if (result === null || (typeof result !== 'object' && typeof result !== 'function')) { PromisePrototypeThen(kResolvedPromise, onFulfilled); @@ -457,6 +465,7 @@ module.exports = { isBrandCheck, isPromisePending, kEmptyQueue, + kParkedAlgorithmResult, kResolvedPromise, kState, kType, From 28be1043280d6574cec8388da0b77db168e7115d Mon Sep 17 00:00:00 2001 From: trivenay Date: Thu, 20 Aug 2026 12:33:21 +0530 Subject: [PATCH 21/97] quic: do not destroy incoming streams that have a consumer An incoming stream was destroyed unless the session had an onstream callback, even when session-level stream callbacks (onheaders et al) were registered and the negotiated application (HTTP/3) would drive the stream through them. Users had to register stub onstream handlers just to keep their streams alive. Destroy an incoming stream only when the session has no consumer for it at all: no onstream callback, and no session-level stream callbacks runnable on the negotiated application (checked via the existing headersSupported session state, computed when the application is selected from ALPN). Sessions with no consumers keep the current destroy-and-warn behavior so unconsumed streams cannot accumulate and hold flow control credit. On HTTP/3 sessions only bidirectional request streams reach this path; control and QPACK streams are consumed internally by nghttp3 and are never exposed to JavaScript. Fixes: https://github.com/nodejs/node/issues/64192 Signed-off-by: Naman Trivedi PR-URL: https://github.com/nodejs/node/pull/65335 Fixes: https://github.com/nodejs/node/issues/64192 Reviewed-By: James M Snell Reviewed-By: Trivikram Kamat --- doc/api/quic.md | 24 +- lib/internal/quic/quic.js | 38 ++- lib/internal/quic/state.js | 15 ++ src/quic/application.h | 5 + src/quic/defs.h | 6 + src/quic/http3.cc | 2 + src/quic/session.cc | 5 + .../test-quic-h3-stream-without-onstream.mjs | 244 ++++++++++++++++++ 8 files changed, 331 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-quic-h3-stream-without-onstream.mjs diff --git a/doc/api/quic.md b/doc/api/quic.md index c243f347e863..75dba5028425 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -305,7 +305,11 @@ unidirectional (data flows in only one direction). The `quic` module provides separate APIs for creating each kind: [`session.createBidirectionalStream()`][] and [`session.createUnidirectionalStream()`][]. Streams initiated by a remote -peer are delivered via the [`session.onstream`][] callback. +peer are delivered via the [`session.onstream`][] callback. When the +negotiated application protocol supports the stream-level callbacks (e.g. +HTTP/3) and an `onheaders` callback is configured, incoming streams can +instead be consumed entirely through it and registering `onstream` is +optional. There are two ways to write data to a stream: @@ -409,7 +413,9 @@ A typical client session progresses through these stages: On the server side, call [`quic.listen()`][] with a callback. The callback fires for each incoming session after the TLS handshake begins. Incoming -streams arrive via the [`session.onstream`][] callback. +streams arrive via the [`session.onstream`][] callback, or, for HTTP/3 +sessions with an `onheaders` callback configured, directly through that +callback (see the [minimal HTTP/3 server][] example). [`session.destroy()`][] is available for immediate teardown — all open streams are destroyed and the session is closed without waiting for them to finish. @@ -1090,6 +1096,15 @@ added: v23.8.0 The callback to invoke when a new stream is initiated by a remote peer. Read/write. +If no `onstream` callback is set and the stream has no other consumer, an +incoming stream is destroyed on arrival and a warning is emitted. An +`onheaders` callback counts as a consumer when the negotiated application +protocol supports it (e.g. HTTP/3), because it is invoked for every incoming +request stream. Other stream-level callbacks (`ontrailers`, `oninfo`, +`onwanttrailers`) do not, since they are conditional or outbound-only and +would leave the stream unobservable. An HTTP/3 server that handles requests +entirely through `onheaders` does not need to set `onstream`. + ### `session.ondatagram` + + + + + + + + diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index cef5f0b52da1..5130996dd2fd 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -129,7 +129,7 @@ true !== false - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail at TestContext.<anonymous> (/test/fixtures/test-runner/output/output.js:125:11) @@ -152,15 +152,15 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail - - - - + + + + - + - + @@ -267,9 +267,9 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw - - - + + + @@ -289,7 +289,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first at TestContext.<anonymous> (/test/fixtures/test-runner/output/output.js:334:11) @@ -304,7 +304,7 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first } - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at second at TestContext.<anonymous> (/test/fixtures/test-runner/output/output.js:337:11) { diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js index 7fed79d45b48..a2f6316a84fe 100644 --- a/test/parallel/test-runner-reporters.js +++ b/test/parallel/test-runner-reporters.js @@ -207,7 +207,7 @@ describe('node:test reporters', { concurrency: true }, () => { assert.match(timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); assert.ok(!Number.isNaN(Date.parse(timestamp)), `expected a valid date, got ${timestamp}`); assert.match(fileContents, /\s*/); - assert.match(fileContents, //); + assert.match(fileContents, //); assert.match(fileContents, //); }); }); diff --git a/test/test-runner/test-output-junit-classname-hierarchy.mjs b/test/test-runner/test-output-junit-classname-hierarchy.mjs new file mode 100644 index 000000000000..737cefd89eca --- /dev/null +++ b/test/test-runner/test-output-junit-classname-hierarchy.mjs @@ -0,0 +1,12 @@ +// Test that the output of test-runner/output/junit_classname_hierarchy.js matches +// test-runner/output/junit_classname_hierarchy.snapshot +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawnAndAssert, junitTransform, ensureCwdIsProjectRoot } from '../common/assertSnapshot.js'; + +ensureCwdIsProjectRoot(); +await spawnAndAssert( + fixtures.path('test-runner/output/junit_classname_hierarchy.js'), + junitTransform, + { flags: ['--test-reporter=junit'] }, +); From 1c24cefeef36c9cfc1cf283dd30d6386c4f96a9c Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Thu, 20 Aug 2026 19:55:28 -0400 Subject: [PATCH 27/97] deps: update simdjson to 4.6.7 PR-URL: https://github.com/nodejs/node/pull/65318 Reviewed-By: Colin Ihrig Reviewed-By: Moshe Atlow --- deps/simdjson/simdjson.cpp | 29 ++++++++++++++++++++++++++++- deps/simdjson/simdjson.h | 6 +++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/deps/simdjson/simdjson.cpp b/deps/simdjson/simdjson.cpp index 0d19312880fd..7b8b0c44f057 100644 --- a/deps/simdjson/simdjson.cpp +++ b/deps/simdjson/simdjson.cpp @@ -1,4 +1,4 @@ -/* auto-generated on 2026-07-30 16:20:13 -0400. version 4.6.6 Do not edit! */ +/* auto-generated on 2026-08-14 12:14:27 -0400. version 4.6.7 Do not edit! */ /* including simdjson.cpp: */ /* begin file simdjson.cpp */ #define SIMDJSON_SRC_SIMDJSON_CPP @@ -15228,6 +15228,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -21620,6 +21623,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -28007,6 +28013,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -34665,6 +34674,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -41885,6 +41897,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -48136,6 +48151,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -54291,6 +54309,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -60865,6 +60886,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); @@ -64733,6 +64757,9 @@ simdjson_warn_unused simdjson_inline error_code tape_builder::visit_number(json_ const uint8_t *p = value; if (*p == '-') p++; while (numberparsing::is_digit(*p)) p++; + // The digit run must be terminated by a structural or whitespace character; otherwise the + // token is malformed (e.g. "123456789123456789123x"). + if (jsoncharutils::is_not_structural_or_whitespace(*p)) { return NUMBER_ERROR; } size_t len = size_t(p - value); tape.append(current_string_buf_loc - iter.dom_parser.doc->string_buf.get(), internal::tape_type::BIGINT); uint8_t *dst = current_string_buf_loc + sizeof(uint32_t); diff --git a/deps/simdjson/simdjson.h b/deps/simdjson/simdjson.h index 89175e6d6f70..afcca85ad6bf 100644 --- a/deps/simdjson/simdjson.h +++ b/deps/simdjson/simdjson.h @@ -1,4 +1,4 @@ -/* auto-generated on 2026-07-30 16:20:13 -0400. version 4.6.6 Do not edit! */ +/* auto-generated on 2026-08-14 12:14:27 -0400. version 4.6.7 Do not edit! */ /* including simdjson.h: */ /* begin file simdjson.h */ #ifndef SIMDJSON_H @@ -2538,7 +2538,7 @@ namespace std { #define SIMDJSON_SIMDJSON_VERSION_H /** The version of simdjson being used (major.minor.revision) */ -#define SIMDJSON_VERSION "4.6.6" +#define SIMDJSON_VERSION "4.6.7" namespace simdjson { enum { @@ -2553,7 +2553,7 @@ enum { /** * The revision (major.minor.REVISION) of simdjson being used. */ - SIMDJSON_VERSION_REVISION = 6 + SIMDJSON_VERSION_REVISION = 7 }; } // namespace simdjson From 0eac2faecf77dcb0bc21eb205697eaadfacd4d8e Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Thu, 20 Aug 2026 19:55:38 -0400 Subject: [PATCH 28/97] deps: update zlib to 1.3.2.1-motley-8002e91 PR-URL: https://github.com/nodejs/node/pull/65316 Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca --- deps/zlib/google/OWNERS | 2 +- deps/zlib/google/zip_reader_unittest.cc | 13 ++++--------- src/zlib_version.h | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/deps/zlib/google/OWNERS b/deps/zlib/google/OWNERS index 1bd83ac482ee..901c226205f8 100644 --- a/deps/zlib/google/OWNERS +++ b/deps/zlib/google/OWNERS @@ -1,4 +1,4 @@ -satorux@chromium.org +satorux@google.com # compression_utils* asvitkine@chromium.org diff --git a/deps/zlib/google/zip_reader_unittest.cc b/deps/zlib/google/zip_reader_unittest.cc index 578539ffbf76..6e58f7f72ee8 100644 --- a/deps/zlib/google/zip_reader_unittest.cc +++ b/deps/zlib/google/zip_reader_unittest.cc @@ -35,7 +35,6 @@ #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" -#include "third_party/icu/source/i18n/unicode/timezone.h" #include "third_party/zlib/google/zip_internal.h" using ::testing::_; @@ -367,10 +366,8 @@ TEST_F(ZipReaderTest, RegularFile) { EXPECT_EQ(target_path, entry->path); EXPECT_EQ(13527, entry->original_size); - EXPECT_EQ("2009-05-29 06:22:20.000", - base::UnlocalizedTimeFormatWithPattern(entry->last_modified, - "y-MM-dd HH:mm:ss.SSS", - icu::TimeZone::getGMT())); + EXPECT_EQ("2009-05-29T06:22:20.000Z", + base::TimeFormatAsIso8601(entry->last_modified)); EXPECT_FALSE(entry->is_unsafe); EXPECT_FALSE(entry->is_directory); } @@ -467,10 +464,8 @@ TEST_F(ZipReaderTest, Directory) { EXPECT_EQ(target_path, entry->path); // The directory size should be zero. EXPECT_EQ(0, entry->original_size); - EXPECT_EQ("2009-05-31 15:49:52.000", - base::UnlocalizedTimeFormatWithPattern(entry->last_modified, - "y-MM-dd HH:mm:ss.SSS", - icu::TimeZone::getGMT())); + EXPECT_EQ("2009-05-31T15:49:52.000Z", + base::TimeFormatAsIso8601(entry->last_modified)); EXPECT_FALSE(entry->is_unsafe); EXPECT_TRUE(entry->is_directory); } diff --git a/src/zlib_version.h b/src/zlib_version.h index 302282a2027e..861b339e2bfc 100644 --- a/src/zlib_version.h +++ b/src/zlib_version.h @@ -2,5 +2,5 @@ // Refer to tools/dep_updaters/update-zlib.sh #ifndef SRC_ZLIB_VERSION_H_ #define SRC_ZLIB_VERSION_H_ -#define ZLIB_VERSION "1.3.2.1-motley-42c2f19" +#define ZLIB_VERSION "1.3.2.1-motley-8002e91" #endif // SRC_ZLIB_VERSION_H_ From 8f26eb3c5e2e2d447ca27a6520c7a40de8b3f09f Mon Sep 17 00:00:00 2001 From: Paul Bouchon Date: Thu, 20 Aug 2026 19:56:07 -0400 Subject: [PATCH 29/97] module: report unreadable package.json A package.json that exists but cannot be read was treated the same as one that is not there: the read failure returned no config and resolution continued as if the package had none. Fields such as "exports" and "type" silently disappear, so a specifier can resolve to a different file than the package declares, while an unparsable package.json already throws ERR_INVALID_PACKAGE_CONFIG. Keep treating ENOENT and ENOTDIR as "no package config here", and report any other read failure with the underlying error. Fixes: https://github.com/nodejs/node/issues/65220 Signed-off-by: Paul Bouchon PR-URL: https://github.com/nodejs/node/pull/65223 Reviewed-By: Antoine du Hamel --- src/node_modules.cc | 18 +++++- .../test-module-unreadable-package-json.js | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-module-unreadable-package-json.js diff --git a/src/node_modules.cc b/src/node_modules.cc index 5000ab66381c..f45d7ba91d98 100644 --- a/src/node_modules.cc +++ b/src/node_modules.cc @@ -112,7 +112,23 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( PackageConfig package_config{}; package_config.file_path = path; // No need to exclude BOM since simdjson will skip it. - if (ReadFileSync(&package_config.raw_json, path.data()) < 0) { + int read_error = ReadFileSync(&package_config.raw_json, path.data()); + if (read_error < 0) { + // No file at this path, a path component that is not a directory, or a + // "package.json" that is itself a directory all mean there is no package + // config here. Any other failure means a package.json is present but + // could not be read. Treating that as absent silently drops fields such + // as "exports" and "type", which can resolve a specifier to a different + // file, so surface the read error instead of continuing. + if (read_error != UV_ENOENT && read_error != UV_ENOTDIR && + read_error != UV_EISDIR) { + THROW_ERR_INVALID_PACKAGE_CONFIG(realm->isolate(), + "Cannot read package config %s: %s.", + path.data(), + uv_strerror(read_error)); + return nullptr; + } + // Add `nullopt` to the package config cache so that we don't // need to open and attempt to read this path again binding_data->package_configs_.insert({std::string(path), std::nullopt}); diff --git a/test/parallel/test-module-unreadable-package-json.js b/test/parallel/test-module-unreadable-package-json.js new file mode 100644 index 000000000000..4261dad85580 --- /dev/null +++ b/test/parallel/test-module-unreadable-package-json.js @@ -0,0 +1,57 @@ +'use strict'; + +// A package.json that exists but cannot be read must not be treated as +// absent. Doing so silently drops fields such as "exports", which can resolve +// a specifier to a different file than the one the package declares. +// Refs: https://github.com/nodejs/node/issues/65220 + +const common = require('../common'); + +if (common.isWindows) { + common.skip('chmod does not restrict reads on Windows'); +} +if (process.getuid?.() === 0) { + common.skip('cannot make a file unreadable as root'); +} + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const depDir = tmpdir.resolve('node_modules/dep'); +fs.mkdirSync(path.join(depDir, 'lib'), { recursive: true }); +const depPackageJson = path.join(depDir, 'package.json'); +fs.writeFileSync( + depPackageJson, + '{"name":"dep","exports":{".":"./lib/real.js"}}', +); +fs.writeFileSync(path.join(depDir, 'lib', 'real.js'), 'export const which = "real";'); +// If the package config is ignored, resolution falls back to this file. +fs.writeFileSync(path.join(depDir, 'index.js'), 'export const which = "decoy";'); + +fs.writeFileSync(tmpdir.resolve('package.json'), '{"type":"module"}'); +const entry = tmpdir.resolve('main.mjs'); +fs.writeFileSync(entry, 'import { which } from "dep"; console.log(which);'); + +// Sanity check: the export resolves while the package config is readable. +{ + const child = spawnSync(process.execPath, [entry], { encoding: 'utf8' }); + assert.strictEqual(child.stdout.trim(), 'real'); + assert.strictEqual(child.status, 0, child.stderr); +} + +fs.chmodSync(depPackageJson, 0o000); + +try { + const child = spawnSync(process.execPath, [entry], { encoding: 'utf8' }); + // The read failure must be reported rather than resolving to index.js. + assert.doesNotMatch(child.stdout, /decoy/); + assert.match(child.stderr, /Cannot read package config/); + assert.notStrictEqual(child.status, 0); +} finally { + fs.chmodSync(depPackageJson, 0o644); +} From 3151bf57e62a3726ace7f75e746c349ed350d2a2 Mon Sep 17 00:00:00 2001 From: Nora Dossche <7771979+ndossche@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:56:17 +0200 Subject: [PATCH 30/97] crypto: fix missing error checks on ASN1_STRING_to_UTF8() This function returns a negative error code on error. When it does so, the `value_str` pointer will remain uninitialized and cause a crash later on when it is freed by OPENSSL_free(). Even if it wouldn't crash there, it still fails to signal the error and an empty string may be propagated to the callers. The check also mirrors the other one in the same file. Signed-off-by: ndossche PR-URL: https://github.com/nodejs/node/pull/65200 Reviewed-By: Yagiz Nizipli Reviewed-By: Filip Skokan Reviewed-By: Daeyeon Jeong --- deps/ncrypto/ncrypto.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 2f59ab119ead..7959bc604e91 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -6904,6 +6904,9 @@ std::pair X509Name::Iterator::operator*() const { unsigned char* value_str; int value_str_size = ASN1_STRING_to_UTF8(&value_str, value); + if (value_str_size < 0) [[unlikely]] { + return {{}, {}}; + } std::string out(reinterpret_cast(value_str), value_str_size); OPENSSL_free(value_str); // free after copy From d67925f71117a45def57de8a30ad03b52588b4a5 Mon Sep 17 00:00:00 2001 From: Hierax_Umbra <72476163+frandle331-yh@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:56:28 +0900 Subject: [PATCH 31/97] fs: fix out-of-bounds write in mkdtemp for long prefixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mkdtemp() allocated the template buffer as `length + strlen("XXXXXX")`, leaving no room for the terminating NUL byte. For a single-byte prefix long enough to force the heap allocation path (length + 6 > the stack-buffer threshold), the terminating NUL was written one byte past the end of the buffer -- a 1-byte heap-buffer-overflow flagged by AddressSanitizer. Allocate room for the terminating NUL, copy the suffix, and use SetLengthAndZeroTerminate to set the correct length and write the terminator, following the MaybeStackBuffer paradigm used elsewhere in this file. Signed-off-by: frandle331-yh PR-URL: https://github.com/nodejs/node/pull/64770 Reviewed-By: René --- src/node_file.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/node_file.cc b/src/node_file.cc index 2d8f5a9306a6..638b17717d09 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -3363,10 +3363,11 @@ static void Mkdtemp(const FunctionCallbackInfo& args) { CHECK_GE(argc, 2); BufferValue tmpl(isolate, args[0]); - static constexpr const char* const suffix = "XXXXXX"; - const auto length = tmpl.length(); - tmpl.AllocateSufficientStorage(length + strlen(suffix)); - snprintf(tmpl.out() + length, tmpl.length(), "%s", suffix); + const auto prefix_length = tmpl.length(); + static constexpr std::string_view suffix = "XXXXXX"; + tmpl.AllocateSufficientStorage(prefix_length + suffix.size() + 1); + memcpy(tmpl.out() + prefix_length, suffix.data(), suffix.size()); + tmpl.SetLengthAndZeroTerminate(prefix_length + suffix.size()); CHECK_NOT_NULL(*tmpl); From 93ead3a0891d8b6b9b9a7b8083574d4f76eb2dbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:49:54 +0000 Subject: [PATCH 32/97] tools: bump brace-expansion in `/tools/clang-format` Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.13 to 1.1.18. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.13...v1.1.18) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.18 dependency-type: indirect ... PR-URL: https://github.com/nodejs/node/pull/64984 Reviewed-By: Antoine du Hamel Reviewed-By: Colin Ihrig --- tools/clang-format/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/clang-format/package-lock.json b/tools/clang-format/package-lock.json index 5d03eb31fde1..10ee47a76269 100644 --- a/tools/clang-format/package-lock.json +++ b/tools/clang-format/package-lock.json @@ -23,9 +23,9 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -193,9 +193,9 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" From 3d6041cbdcd68fcc70d3664e46052b18db9e660e Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Fri, 21 Aug 2026 05:50:05 +0500 Subject: [PATCH 33/97] dns: validate address type in lookupService Signed-off-by: Lazizbek Ergashev PR-URL: https://github.com/nodejs/node/pull/64878 Fixes: https://github.com/nodejs/node/issues/64877 Reviewed-By: theanarkh Reviewed-By: Tim Perry --- lib/dns.js | 3 +++ lib/internal/dns/promises.js | 2 ++ test/parallel/test-dns.js | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/lib/dns.js b/lib/dns.js index 715351d8d29f..5452e7321419 100644 --- a/lib/dns.js +++ b/lib/dns.js @@ -84,6 +84,7 @@ const { validateNumber, validateOneOf, validatePort, + validateString, validateStringWithoutNullBytes, } = require('internal/validators'); @@ -275,6 +276,8 @@ function lookupService(address, port, callback) { if (arguments.length !== 3) throw new ERR_MISSING_ARGS('address', 'port', 'callback'); + validateString(address, 'address'); + if (isIP(address) === 0) throw new ERR_INVALID_ARG_VALUE('address', address); diff --git a/lib/internal/dns/promises.js b/lib/internal/dns/promises.js index 899e8c6672dd..0a67e2e196f4 100644 --- a/lib/internal/dns/promises.js +++ b/lib/internal/dns/promises.js @@ -283,6 +283,8 @@ function lookupService(address, port) { if (arguments.length !== 2) throw new ERR_MISSING_ARGS('address', 'port'); + validateString(address, 'address'); + if (isIP(address) === 0) throw new ERR_INVALID_ARG_VALUE('address', address); diff --git a/test/parallel/test-dns.js b/test/parallel/test-dns.js index c69a04b385b8..d8182a8ccf58 100644 --- a/test/parallel/test-dns.js +++ b/test/parallel/test-dns.js @@ -344,6 +344,24 @@ dns.lookup('', { }, err); } +{ + const invalidAddress = Buffer.from('127.0.0.1'); + const err = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "address" argument must be of type string. ' + + 'Received an instance of Buffer' + }; + + assert.throws(() => { + dnsPromises.lookupService(invalidAddress, 0); + }, err); + + assert.throws(() => { + dns.lookupService(invalidAddress, 0, common.mustNotCall()); + }, err); +} + [null, undefined, 65538, 'test', NaN, Infinity, Symbol(), 0n, true, false, '', () => {}, {}].forEach((port) => { const err = { code: 'ERR_SOCKET_BAD_PORT', From 26ad6b09802c9673058acc54d3b98b319b2d7dd7 Mon Sep 17 00:00:00 2001 From: Kirill Saied Date: Fri, 21 Aug 2026 02:50:15 +0200 Subject: [PATCH 34/97] child_process: keep SIGWINCH from killing on Win Fixes: https://github.com/nodejs/node/issues/64324 Signed-off-by: PickBas PR-URL: https://github.com/nodejs/node/pull/64510 Reviewed-By: Stefan Stojanovic --- doc/api/child_process.md | 12 ++++--- src/process_wrap.cc | 2 +- .../test-child-process-kill-sigwinch.js | 34 +++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-child-process-kill-sigwinch.js diff --git a/doc/api/child_process.md b/doc/api/child_process.md index 4cb444dbd7d9..77db426c668b 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -1714,10 +1714,14 @@ may not actually terminate the process. See kill(2) for reference. -On Windows, where POSIX signals do not exist, the `signal` argument will be -ignored except for `'SIGKILL'`, `'SIGTERM'`, `'SIGINT'` and `'SIGQUIT'`, and the -process will always be killed forcefully and abruptly (similar to `'SIGKILL'`). -See [Signal Events][] for more details. +On Windows, where POSIX signals do not exist, signals are handled as follows. +`'SIGKILL'`, `'SIGTERM'`, `'SIGINT'` and `'SIGQUIT'` terminate the process +forcefully and abruptly (similar to `'SIGKILL'`); any other signal whose name is +known on Windows (such as `'SIGHUP'`) does the same. `'SIGWINCH'` is not +terminal and is not coerced: `subprocess.kill()` throws an `ENOSYS` error and +the child keeps running. A signal name that does not exist on Windows (such as +`'SIGSTOP'`) throws an `ERR_UNKNOWN_SIGNAL` error. See [Signal Events][] for more +details. On Linux, child processes of child processes will not be terminated when attempting to kill their parent. This is likely to happen when running a diff --git a/src/process_wrap.cc b/src/process_wrap.cc index 21ccb2a9989b..4d9420757122 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -351,7 +351,7 @@ class ProcessWrap : public HandleWrap { } #ifdef _WIN32 if (signal != SIGKILL && signal != SIGTERM && signal != SIGINT && - signal != SIGQUIT && signal != 0) { + signal != SIGQUIT && signal != 0 && signal != SIGWINCH) { signal = SIGKILL; } #endif diff --git a/test/parallel/test-child-process-kill-sigwinch.js b/test/parallel/test-child-process-kill-sigwinch.js new file mode 100644 index 000000000000..e474eefa40b4 --- /dev/null +++ b/test/parallel/test-child-process-kill-sigwinch.js @@ -0,0 +1,34 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +// SIGWINCH is a non-terminal signal: sending it must not terminate the target +// process. On Windows, kill() must surface ENOSYS instead of coercing +// SIGWINCH into a SIGKILL (which would wrongly terminate the process). +// Refs: https://github.com/nodejs/node/issues/64324 + +const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)']); + +// The process must survive the SIGWINCH; it is only ever torn down by the +// explicit SIGKILL in the cleanup below (after the listener is removed). +child.on('exit', common.mustNotCall('child must survive SIGWINCH')); + +child.on('spawn', common.mustCall(() => { + if (common.isWindows) { + assert.throws(() => child.kill('SIGWINCH'), { code: 'ENOSYS' }); + } else { + assert.strictEqual(child.kill('SIGWINCH'), true); + } + + assert.strictEqual(child.signalCode, null); + assert.strictEqual(child.exitCode, null); + + setTimeout(common.mustCall(() => { + assert.strictEqual(child.signalCode, null); + assert.strictEqual(child.exitCode, null); + + child.removeAllListeners('exit'); + child.kill('SIGKILL'); + }), common.platformTimeout(500)); +})); From 2114baa7dcc6edda60540d3f978d78aef6f2f651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=9A=B0?= <162293672+guhyunwoo@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:21:47 +0900 Subject: [PATCH 35/97] test: fix Linux debug skip in SEA test guard process.config.variables.is_debug is only populated by the GN build flow (tools/generate_config_gypi.py), so on the gyp builds used by CI the guard never fired and SEA tests ran on Linux debug builds against the original intent. Switch to process.config.target_defaults.default_configuration === 'Debug', matching the pattern used in test/common/index.js. Fixes: https://github.com/nodejs/node/issues/63749 Refs: https://github.com/nodejs/node/issues/61483 Signed-off-by: Hyunwoo Gu PR-URL: https://github.com/nodejs/node/pull/63751 Reviewed-By: Joyee Cheung Reviewed-By: Richard Lau Reviewed-By: James M Snell --- test/common/sea.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/common/sea.js b/test/common/sea.js index 5f3741e0117c..f0dfc8479888 100644 --- a/test/common/sea.js +++ b/test/common/sea.js @@ -20,7 +20,7 @@ function skipIfSingleExecutableIsNotSupported() { if (!['darwin', 'win32', 'linux'].includes(process.platform)) common.skip(`Unsupported platform ${process.platform}.`); - if (process.platform === 'linux' && process.config.variables.is_debug === 1) + if (process.platform === 'linux' && common.isDebug) common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.'); if (process.config.variables.node_shared) From c2e533b8f6aab18cbb6173aad263a2475a55e15e Mon Sep 17 00:00:00 2001 From: Donghoon Kang Date: Fri, 21 Aug 2026 14:53:42 +0900 Subject: [PATCH 36/97] doc: update outdated nodejs.org guide links Update outdated `nodejs.org/en/docs/guides/...` links to their current `nodejs.org/learn/...` destinations. Signed-off-by: HoonDongKang PR-URL: https://github.com/nodejs/node/pull/65394 Reviewed-By: Mike McCready <66998419+MikeMcC399@users.noreply.github.com> Reviewed-By: Luigi Pinca Reviewed-By: James M Snell --- doc/api/cli.md | 2 +- doc/api/n-api.md | 2 +- doc/api/timers.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 4c791ef6709e..165fac354a14 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -4306,7 +4306,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [conditional exports]: packages.md#conditional-exports [context-aware]: addons.md#context-aware-addons [debugger]: debugger.md -[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications +[debugging security implications]: https://nodejs.org/learn/getting-started/debugging#security-implications [deprecation warnings]: deprecations.md#list-of-deprecated-apis [emit_warning]: process.md#processemitwarningwarning-options [environment_variables]: #environment-variables_1 diff --git a/doc/api/n-api.md b/doc/api/n-api.md index f51a5ad9427e..87f1561cbd90 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -6944,7 +6944,7 @@ node_api_get_module_file_name(node_api_basic_env env, const char** result); `result` may be an empty string if the add-on loading process fails to establish the add-on's file name during loading. -[ABI Stability]: https://nodejs.org/en/docs/guides/abi-stability/ +[ABI Stability]: https://nodejs.org/learn/modules/abi-stability [AppVeyor]: https://www.appveyor.com [C++ Addons]: addons.md [CMake]: https://cmake.org diff --git a/doc/api/timers.md b/doc/api/timers.md index 7c91543c4573..cdbed03ba7ac 100644 --- a/doc/api/timers.md +++ b/doc/api/timers.md @@ -600,7 +600,7 @@ being developed as a standard Web Platform API. Calling `timersPromises.scheduler.yield()` is equivalent to calling `timersPromises.setImmediate()` with no arguments. -[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout +[Event Loop]: https://nodejs.org/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout [Scheduling APIs]: https://github.com/WICG/scheduling-apis [`AbortController`]: globals.md#class-abortcontroller [`TypeError`]: errors.md#class-typeerror From 35007d1b25d681e62261c2c08d4cbace29ca39b7 Mon Sep 17 00:00:00 2001 From: Dayun Date: Fri, 21 Aug 2026 22:39:55 +0900 Subject: [PATCH 37/97] doc: clarify socket destroyed behavior Signed-off-by: Dayun PR-URL: https://github.com/nodejs/node/pull/65395 Fixes: https://github.com/nodejs/node/issues/57367 Reviewed-By: James M Snell Reviewed-By: Daeyeon Jeong --- doc/api/net.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/net.md b/doc/api/net.md index 8348f0158818..b79e43821a2b 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -1404,15 +1404,15 @@ added: v0.1.90 * `error` {Object} * Returns: {net.Socket} -Ensures that no more I/O activity happens on this socket. +Ensures that no more I/O activity happens on the current connection. Destroys the stream and closes the connection. See [`writable.destroy()`][] for further details. ### `socket.destroyed` -* Type: {boolean} Indicates if the connection is destroyed or not. Once a - connection is destroyed no further data can be transferred using it. +* Type: {boolean} Indicates if the connection is destroyed or not. No further + data can be transferred using a destroyed connection. See [`writable.destroyed`][] for further details. From 114df48f781a2012ae0d584aec75ca5dcca6b678 Mon Sep 17 00:00:00 2001 From: Junsoo Ha <35479251+ganjanggejang@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:40:07 +0900 Subject: [PATCH 38/97] test: use common/child_process spawnSync helpers Replace manual spawnSync() result assertions with test/common/child_process helper functions. Signed-off-by: Junsoo Ha PR-URL: https://github.com/nodejs/node/pull/65377 Reviewed-By: Chemi Atlow Reviewed-By: Luigi Pinca --- test/parallel/test-crypto-key-store-pkcs11.js | 4 +-- ...test-experimental-shared-value-conveyor.js | 12 ++++----- test/parallel/test-heap-prof-basic.js | 9 ++----- test/parallel/test-heap-prof-exec-argv.js | 8 ++---- test/parallel/test-heap-prof-loop-drained.js | 9 ++----- test/parallel/test-inspect-address-in-use.js | 27 +++++++++---------- test/parallel/test-os-homedir-no-envvar.js | 6 ++--- test/parallel/test-process-execpath.js | 10 +++---- .../test-runner-mock-timers-with-timeout.js | 6 ++--- test/parallel/test-v8-stop-coverage.js | 7 +++-- test/parallel/test-v8-take-coverage-noop.js | 7 +++-- test/parallel/test-v8-take-coverage.js | 7 +++-- 12 files changed, 45 insertions(+), 67 deletions(-) diff --git a/test/parallel/test-crypto-key-store-pkcs11.js b/test/parallel/test-crypto-key-store-pkcs11.js index db34eaf18957..8c344686a426 100644 --- a/test/parallel/test-crypto-key-store-pkcs11.js +++ b/test/parallel/test-crypto-key-store-pkcs11.js @@ -32,6 +32,7 @@ const { verify, } = require('crypto'); const tmpdir = require('../common/tmpdir'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); const { subtle } = globalThis.crypto; const kData = Buffer.from( @@ -61,7 +62,7 @@ function softhsmOptions() { function runInChild() { const { cwd, env } = softhsmOptions(); - const child = spawnSync(process.execPath, [ + spawnSyncAndExitWithoutError(process.execPath, [ `--openssl-config=${kOpenSSLConfig}`, __filename, ], { @@ -69,7 +70,6 @@ function runInChild() { env: { ...process.env, ...env, NODE_TEST_PKCS11_CHILD: '1' }, stdio: 'inherit', }); - assert.strictEqual(child.status, 0); } function privateKeyUrl(label) { diff --git a/test/parallel/test-experimental-shared-value-conveyor.js b/test/parallel/test-experimental-shared-value-conveyor.js index 17eb32c66b11..123e212bd1da 100644 --- a/test/parallel/test-experimental-shared-value-conveyor.js +++ b/test/parallel/test-experimental-shared-value-conveyor.js @@ -1,8 +1,8 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const { spawnSync } = require('child_process'); const { Worker, parentPort } = require('worker_threads'); +const { spawnSyncAndAssert } = require('../common/child_process'); if (process.env.TEST_CHILD_PROCESS === '1') { // Do not use isMainThread so that this test itself can be run inside a Worker. @@ -29,10 +29,10 @@ if (process.env.TEST_CHILD_PROCESS === '1') { const args = ['--harmony-struct', __filename]; const options = { env: { TEST_CHILD_PROCESS: '1', ...process.env } }; - const child = spawnSync(process.execPath, args, options); - assert.strictEqual(child.stderr.toString().trim(), ''); - assert.strictEqual(child.stdout.toString().trim(), ''); - assert.strictEqual(child.status, 0); - assert.strictEqual(child.signal, null); + spawnSyncAndAssert(process.execPath, args, options, { + stdout: '', + stderr: '', + trim: true + }); } diff --git a/test/parallel/test-heap-prof-basic.js b/test/parallel/test-heap-prof-basic.js index 34d8af9a7840..4ddc313ff048 100644 --- a/test/parallel/test-heap-prof-basic.js +++ b/test/parallel/test-heap-prof-basic.js @@ -9,7 +9,7 @@ const fixtures = require('../common/fixtures'); common.skipIfInspectorDisabled(); const assert = require('assert'); -const { spawnSync } = require('child_process'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); const tmpdir = require('../common/tmpdir'); @@ -20,18 +20,13 @@ const { { tmpdir.refresh(); - const output = spawnSync(process.execPath, [ + spawnSyncAndExitWithoutError(process.execPath, [ '--heap-prof', fixtures.path('workload', 'allocation.js'), ], { cwd: tmpdir.path, env }); - if (output.status !== 0) { - console.log(output.stderr.toString()); - console.log(output); - } - assert.strictEqual(output.status, 0); const profiles = getHeapProfiles(tmpdir.path); assert.strictEqual(profiles.length, 1); } diff --git a/test/parallel/test-heap-prof-exec-argv.js b/test/parallel/test-heap-prof-exec-argv.js index 02ad4430dba7..186b4d5d631b 100644 --- a/test/parallel/test-heap-prof-exec-argv.js +++ b/test/parallel/test-heap-prof-exec-argv.js @@ -9,7 +9,7 @@ const fixtures = require('../common/fixtures'); common.skipIfInspectorDisabled(); const assert = require('assert'); -const { spawnSync } = require('child_process'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); const tmpdir = require('../common/tmpdir'); @@ -20,7 +20,7 @@ const { { tmpdir.refresh(); - const output = spawnSync(process.execPath, [ + const { child: output } = spawnSyncAndExitWithoutError(process.execPath, [ fixtures.path('workload', 'allocation-worker-argv.js'), ], { cwd: tmpdir.path, @@ -29,10 +29,6 @@ const { HEAP_PROF_INTERVAL: '128' } }); - if (output.status !== 0) { - console.log(output.stderr.toString()); - } - assert.strictEqual(output.status, 0); const profiles = getHeapProfiles(tmpdir.path); assert.strictEqual(profiles.length, 1); verifyFrames(output, profiles[0], 'runAllocation'); diff --git a/test/parallel/test-heap-prof-loop-drained.js b/test/parallel/test-heap-prof-loop-drained.js index d0fc4c987849..d8e07b33cb46 100644 --- a/test/parallel/test-heap-prof-loop-drained.js +++ b/test/parallel/test-heap-prof-loop-drained.js @@ -8,7 +8,7 @@ const fixtures = require('../common/fixtures'); common.skipIfInspectorDisabled(); const assert = require('assert'); -const { spawnSync } = require('child_process'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); const tmpdir = require('../common/tmpdir'); @@ -21,7 +21,7 @@ const { { tmpdir.refresh(); - const output = spawnSync(process.execPath, [ + const { child: output } = spawnSyncAndExitWithoutError(process.execPath, [ '--heap-prof', '--heap-prof-interval', kHeapProfInterval, @@ -30,11 +30,6 @@ const { cwd: tmpdir.path, env }); - if (output.status !== 0) { - console.log(output.stderr.toString()); - console.log(output); - } - assert.strictEqual(output.status, 0); const profiles = getHeapProfiles(tmpdir.path); assert.strictEqual(profiles.length, 1); verifyFrames(output, profiles[0], 'runAllocation'); diff --git a/test/parallel/test-inspect-address-in-use.js b/test/parallel/test-inspect-address-in-use.js index d900fdfb6795..bd954e4a7bc8 100644 --- a/test/parallel/test-inspect-address-in-use.js +++ b/test/parallel/test-inspect-address-in-use.js @@ -2,7 +2,7 @@ const common = require('../common'); common.skipIfInspectorDisabled(); -const { spawnSync } = require('child_process'); +const { spawnSyncAndExit } = require('../common/child_process'); const { createServer } = require('http'); const assert = require('assert'); const tmpdir = require('../common/tmpdir'); @@ -25,19 +25,18 @@ function testOnServerListen(fn) { function testChildProcess(getArgs, exitCode, options) { testOnServerListen(common.mustCall((server) => { const { port } = server.address(); - const child = spawnSync(process.execPath, getArgs(port), options); - const stderr = child.stderr.toString().trim(); - const stdout = child.stdout.toString().trim(); - console.log('[STDERR]'); - console.log(stderr); - console.log('[STDOUT]'); - console.log(stdout); - const match = stderr.match( - /Starting inspector on 127\.0\.0\.1:(\d+) failed: address already in use/ - ); - assert.notStrictEqual(match, null); - assert.strictEqual(match[1], port + ''); - assert.strictEqual(child.status, exitCode); + spawnSyncAndExit(process.execPath, getArgs(port), options, { + status: exitCode, + signal: null, + trim: true, + stderr: function(str) { + const match = str.match( + /Starting inspector on 127\.0\.0\.1:(\d+) failed: address already in use/ + ); + assert.notStrictEqual(match, null); + assert.strictEqual(match[1], port + ''); + }, + }); })); } diff --git a/test/parallel/test-os-homedir-no-envvar.js b/test/parallel/test-os-homedir-no-envvar.js index 2f9b1b47a704..3a47d6d72c4d 100644 --- a/test/parallel/test-os-homedir-no-envvar.js +++ b/test/parallel/test-os-homedir-no-envvar.js @@ -1,9 +1,9 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const cp = require('child_process'); const os = require('os'); const path = require('path'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); if (process.argv[2] === 'child') { @@ -22,9 +22,7 @@ if (process.argv[2] === 'child') { else delete process.env.HOME; - const child = cp.spawnSync(process.execPath, [__filename, 'child'], { + spawnSyncAndExitWithoutError(process.execPath, [__filename, 'child'], { env: process.env }); - - assert.strictEqual(child.status, 0); } diff --git a/test/parallel/test-process-execpath.js b/test/parallel/test-process-execpath.js index 0fce35e2645e..53d8f39fbf7e 100644 --- a/test/parallel/test-process-execpath.js +++ b/test/parallel/test-process-execpath.js @@ -4,7 +4,7 @@ if (common.isWindows) common.skip('symlinks are weird on windows'); const assert = require('assert'); -const child_process = require('child_process'); +const { spawnSyncAndAssert } = require('../common/child_process'); const fs = require('fs'); assert.strictEqual(process.execPath, fs.realpathSync(process.execPath)); @@ -19,8 +19,8 @@ if (process.argv[2] === 'child') { const symlinkedNode = tmpdir.resolve('symlinked-node'); fs.symlinkSync(process.execPath, symlinkedNode); - const proc = child_process.spawnSync(symlinkedNode, [__filename, 'child']); - assert.strictEqual(proc.stderr.toString(), ''); - assert.strictEqual(proc.stdout.toString(), `${process.execPath}\n`); - assert.strictEqual(proc.status, 0); + spawnSyncAndAssert(symlinkedNode, [__filename, 'child'], { + stdout: `${process.execPath}\n`, + stderr: '' + }); } diff --git a/test/parallel/test-runner-mock-timers-with-timeout.js b/test/parallel/test-runner-mock-timers-with-timeout.js index 67f266851fe1..6d98e6e9479a 100644 --- a/test/parallel/test-runner-mock-timers-with-timeout.js +++ b/test/parallel/test-runner-mock-timers-with-timeout.js @@ -1,14 +1,12 @@ 'use strict'; require('../common'); const fixtures = require('../common/fixtures'); -const assert = require('node:assert'); -const { spawnSync } = require('node:child_process'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); const { test } = require('node:test'); test('mock timers do not break test timeout cleanup', async () => { const fixture = fixtures.path('test-runner', 'mock-timers-with-timeout.js'); - const cp = spawnSync(process.execPath, ['--test', fixture], { + spawnSyncAndExitWithoutError(process.execPath, ['--test', fixture], { timeout: 30_000, }); - assert.strictEqual(cp.status, 0, `Test failed:\nstdout: ${cp.stdout}\nstderr: ${cp.stderr}`); }); diff --git a/test/parallel/test-v8-stop-coverage.js b/test/parallel/test-v8-stop-coverage.js index e9764d60477b..b37f8320abb4 100644 --- a/test/parallel/test-v8-stop-coverage.js +++ b/test/parallel/test-v8-stop-coverage.js @@ -5,7 +5,7 @@ const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const assert = require('assert'); const fs = require('fs'); -const { spawnSync } = require('child_process'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); common.skipIfInspectorDisabled(); @@ -13,7 +13,7 @@ tmpdir.refresh(); const intervals = 20; { - const output = spawnSync(process.execPath, [ + const { child } = spawnSyncAndExitWithoutError(process.execPath, [ '-r', fixtures.path('v8-coverage', 'stop-coverage'), '-r', @@ -27,8 +27,7 @@ const intervals = 20; TEST_INTERVALS: intervals }, }); - console.log(output.stderr.toString()); - assert.strictEqual(output.status, 0); + console.log(child.stderr.toString()); const coverageFiles = fs.readdirSync(tmpdir.path); assert.strictEqual(coverageFiles.length, 0); } diff --git a/test/parallel/test-v8-take-coverage-noop.js b/test/parallel/test-v8-take-coverage-noop.js index 8d49b0f23296..14bb8a2c3a91 100644 --- a/test/parallel/test-v8-take-coverage-noop.js +++ b/test/parallel/test-v8-take-coverage-noop.js @@ -5,7 +5,7 @@ const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const assert = require('assert'); const fs = require('fs'); -const { spawnSync } = require('child_process'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); common.skipIfInspectorDisabled(); @@ -14,7 +14,7 @@ tmpdir.refresh(); // v8.takeCoverage() should be a noop if NODE_V8_COVERAGE is not set. const intervals = 40; { - const output = spawnSync(process.execPath, [ + const { child } = spawnSyncAndExitWithoutError(process.execPath, [ '-r', fixtures.path('v8-coverage', 'take-coverage'), fixtures.path('v8-coverage', 'interval'), @@ -25,8 +25,7 @@ const intervals = 40; TEST_INTERVALS: intervals }, }); - console.log(output.stderr.toString()); - assert.strictEqual(output.status, 0); + console.log(child.stderr.toString()); const coverageFiles = fs.readdirSync(tmpdir.path); assert.strictEqual(coverageFiles.length, 0); } diff --git a/test/parallel/test-v8-take-coverage.js b/test/parallel/test-v8-take-coverage.js index 6b1fe149e992..2119a30d6ad9 100644 --- a/test/parallel/test-v8-take-coverage.js +++ b/test/parallel/test-v8-take-coverage.js @@ -5,7 +5,7 @@ const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const assert = require('assert'); const fs = require('fs'); -const { spawnSync } = require('child_process'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); common.skipIfInspectorDisabled(); @@ -13,7 +13,7 @@ tmpdir.refresh(); const intervals = 40; // Outputs coverage when v8.takeCoverage() is invoked. { - const output = spawnSync(process.execPath, [ + const { child } = spawnSyncAndExitWithoutError(process.execPath, [ '-r', fixtures.path('v8-coverage', 'take-coverage'), fixtures.path('v8-coverage', 'interval'), @@ -25,8 +25,7 @@ const intervals = 40; TEST_INTERVALS: intervals }, }); - console.log(output.stderr.toString()); - assert.strictEqual(output.status, 0); + console.log(child.stderr.toString()); const coverageFiles = fs.readdirSync(tmpdir.path); let coverages = []; From 5bb18e1b67f6a007698bd4a4588e406458405809 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 21 Aug 2026 17:01:39 +0200 Subject: [PATCH 39/97] tools: fix max body length handler in `create-release-proposal.sh` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/65455 Reviewed-By: Juan José Arboleda Reviewed-By: Richard Lau --- tools/actions/create-release-proposal.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/actions/create-release-proposal.sh b/tools/actions/create-release-proposal.sh index f4878f1cc940..9240fa2ad7f7 100755 --- a/tools/actions/create-release-proposal.sh +++ b/tools/actions/create-release-proposal.sh @@ -35,9 +35,14 @@ HEAD_SHA="$(git rev-parse HEAD^)" TITLE="$(git log -1 --format=%s)" -TEMP_BODY="$(awk -v MAX_BODY_LENGTH="65536" \ - "/^## ${RELEASE_DATE}/,/^ MAX_BODY_LENGTH) {exit 1;} print }" \ - "doc/changelogs/CHANGELOG_V${RELEASE_LINE}.md" || echo "…")" +# GH rest API has an undocumented limit of 65536 char for the body, setting it +# to 65534 to account for the ellipsis and the final EOL. +TEMP_BODY="$(awk -v MAX_BODY_LENGTH="65534" \ + "/^## ${RELEASE_DATE}/,/^ MAX_BODY_LENGTH) {exit 1;} + print + }" "doc/changelogs/CHANGELOG_V${RELEASE_LINE}.md" || echo "…")" # Create the proposal branch gh api \ From 8612cc077ec413baeda9984ec583cc08bd943631 Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:01:49 +0200 Subject: [PATCH 40/97] doc: fix broken TLS security level example The example under "Setting security levels" does not run. The client sets `maxVersion: 'TLSv1'` while its `minVersion` stays at the `tls.DEFAULT_MIN_VERSION` default of `'TLSv1.2'`, so no version overlaps and the connection fails with ERR_SSL_NO_PROTOCOLS_AVAILABLE. Setting the client's `minVersion` is not enough on its own: the handshake then fails with an alert 40, because `createServer` is given no key or certificate and the server has no shared cipher. Set `minVersion` on the client, add the key and certificate placeholders and the openssl recipe that the other sections using them already carry, pass the server certificate as the client's `ca`, and use port 8000 like the rest of the file. The section now runs from an empty directory and prints "Client connected with protocol: TLSv1". Signed-off-by: Julian Soreavis PR-URL: https://github.com/nodejs/node/pull/65391 Fixes: https://github.com/nodejs/node/issues/60569 Refs: https://github.com/nodejs/node/pull/60571 Reviewed-By: Tim Perry Reviewed-By: James M Snell --- doc/api/tls.md | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/doc/api/tls.md b/doc/api/tls.md index ed8588179117..4ff31989bf3a 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -468,35 +468,64 @@ to set the security level to 0 while using the default OpenSSL cipher list, you ```mjs import { createServer, connect } from 'node:tls'; -const port = 443; +import { readFileSync } from 'node:fs'; +const port = 8000; -createServer({ ciphers: 'DEFAULT@SECLEVEL=0', minVersion: 'TLSv1' }, function(socket) { +createServer({ + key: readFileSync('server-key.pem'), + cert: readFileSync('server-cert.pem'), + ciphers: 'DEFAULT@SECLEVEL=0', + minVersion: 'TLSv1', +}, function(socket) { console.log('Client connected with protocol:', socket.getProtocol()); socket.end(); this.close(); }) .listen(port, () => { - connect(port, { ciphers: 'DEFAULT@SECLEVEL=0', maxVersion: 'TLSv1' }); + connect(port, { + ciphers: 'DEFAULT@SECLEVEL=0', + minVersion: 'TLSv1', + maxVersion: 'TLSv1', + ca: [ readFileSync('server-cert.pem') ], + }); }); ``` ```cjs const { createServer, connect } = require('node:tls'); -const port = 443; +const { readFileSync } = require('node:fs'); +const port = 8000; -createServer({ ciphers: 'DEFAULT@SECLEVEL=0', minVersion: 'TLSv1' }, function(socket) { +createServer({ + key: readFileSync('server-key.pem'), + cert: readFileSync('server-cert.pem'), + ciphers: 'DEFAULT@SECLEVEL=0', + minVersion: 'TLSv1', +}, function(socket) { console.log('Client connected with protocol:', socket.getProtocol()); socket.end(); this.close(); }) .listen(port, () => { - connect(port, { ciphers: 'DEFAULT@SECLEVEL=0', maxVersion: 'TLSv1' }); + connect(port, { + ciphers: 'DEFAULT@SECLEVEL=0', + minVersion: 'TLSv1', + maxVersion: 'TLSv1', + ca: [ readFileSync('server-cert.pem') ], + }); }); ``` This approach sets the security level to 0, allowing the use of legacy features while still leveraging the default OpenSSL ciphers. +To generate the certificate and key for this example, run: + +```bash +openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \ + -keyout server-key.pem -out server-cert.pem +``` + ### Using [`--tls-cipher-list`][] You can also set the security level and ciphers from the command line using the From 08944680041f5cc583eed6f8cc1a795c9e8ca93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Fri, 21 Aug 2026 18:00:21 +0200 Subject: [PATCH 41/97] meta: move targos to emeritus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-Off-By: Michaël Zasso PR-URL: https://github.com/nodejs/node/pull/65393 Reviewed-By: Paolo Insogna Reviewed-By: Moshe Atlow Reviewed-By: Yagiz Nizipli Reviewed-By: Colin Ihrig Reviewed-By: Marco Ippolito Reviewed-By: Rafael Gonzaga Reviewed-By: Chengzhong Wu Reviewed-By: Trivikram Kamat Reviewed-By: Joyee Cheung Reviewed-By: Gerhard Stöbich Reviewed-By: Tobias Nießen Reviewed-By: Ulises Gascón --- README.md | 12 ++++++------ doc/contributing/strategic-initiatives.md | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e27d95f26f1f..3827556a44fb 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,6 @@ For information about the governance of the Node.js project, see **Ruy Adorno** <> (he/him) * [ShogunPanda](https://github.com/ShogunPanda) - **Paolo Insogna** <> (he/him) -* [targos](https://github.com/targos) - - **Michaël Zasso** <> (he/him) * [tniessen](https://github.com/tniessen) - **Tobias Nießen** <> (he/him) @@ -260,6 +258,8 @@ For information about the governance of the Node.js project, see **Sam Roberts** <> * [shigeki](https://github.com/shigeki) - **Shigeki Ohtsu** <> (he/him) +* [targos](https://github.com/targos) - + **Michaël Zasso** <> (he/him) * [thefourtheye](https://github.com/thefourtheye) - **Sakthipriyan Vairamani** <> (he/him) * [TimothyGu](https://github.com/TimothyGu) - @@ -429,8 +429,6 @@ For information about the governance of the Node.js project, see **Stefan Stojanovic** <> (he/him) * [sxa](https://github.com/sxa) - **Stewart X Addison** <> (he/him) -* [targos](https://github.com/targos) - - **Michaël Zasso** <> (he/him) * [theanarkh](https://github.com/theanarkh) - **theanarkh** <> (he/him) * [tniessen](https://github.com/tniessen) - @@ -699,6 +697,8 @@ For information about the governance of the Node.js project, see **Weijia Wang** <> * [stefanmb](https://github.com/stefanmb) - **Stefan Budeanu** <> +* [targos](https://github.com/targos) - + **Michaël Zasso** <> (he/him) * [tellnes](https://github.com/tellnes) - **Christian Tellnes** <> * [thefourtheye](https://github.com/thefourtheye) - @@ -779,8 +779,6 @@ Primary GPG keys for Node.js Releasers (some Releasers sign with subkeys): `DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7` * **Marco Ippolito** <> `CC68F5A3106FF448322E48ED27F5E38D5B0A215F` -* **Michaël Zasso** <> - `8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600` * **Rafael Gonzaga** <> `890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4` * **Richard Lau** <> @@ -846,6 +844,8 @@ verify a downloaded file. `61FC681DFB92A079F1685E77973F295594EC4689` * **Julien Gilli** <> `114F43EE0176B71C7BC219DD50A3051F888C628D` +* **Michaël Zasso** <> + `8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600` * **Myles Borins** <> `C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8` * **Rod Vagg** <> diff --git a/doc/contributing/strategic-initiatives.md b/doc/contributing/strategic-initiatives.md index ac154c0e2bb7..a7ee78f42aae 100644 --- a/doc/contributing/strategic-initiatives.md +++ b/doc/contributing/strategic-initiatives.md @@ -11,7 +11,6 @@ agenda to ensure they are active and have the support they need. | QUIC / HTTP3 | [James M Snell][jasnell] | | | Unified HTTP API | [James M Snell][jasnell] | | | Shadow Realm | [Chengzhong Wu][legendecas] | | -| V8 Currency | [Michaël Zasso][targos] | | | Next-10 | [Jacob Smith][JakobJingleheimer] | | | Single executable apps | [Darshan Sen][RaisinTen] | | | Performance | [Rafael Gonzaga][RafaelGSS] | | @@ -47,4 +46,3 @@ agenda to ensure they are active and have the support they need. [jasnell]: https://github.com/jasnell [joyeecheung]: https://github.com/joyeecheung [legendecas]: https://github.com/legendecas -[targos]: https://github.com/targos From 5a0edb09e933953a4012da7a760dcbac05644d85 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Fri, 21 Aug 2026 20:05:31 +0300 Subject: [PATCH 42/97] doc: document that an empty OPENSSL_CONF skips config loading A default OpenSSL configuration file that exists but cannot be read is fatal at startup: CONF_MFLAGS_IGNORE_MISSING_FILE only covers ENOENT and ENOTDIR, so a container that mounts /etc/ssl inaccessible to the user Node.js runs as cannot start at all. OpenSSL skips config loading entirely when OPENSSL_CONF is set to an empty value, which gets past this, but that was undocumented. Say so, including that no configuration is applied, FIPS setup included. Refs: https://github.com/nodejs/node/issues/62230 Co-Authored-By: Claude Opus 5 Signed-off-by: Orgad Shaneh PR-URL: https://github.com/nodejs/node/pull/64949 Fixes: https://github.com/nodejs/node/issues/62230 Reviewed-By: Filip Skokan --- doc/api/cli.md | 9 ++++- doc/node.1 | 8 +++- .../test-openssl-unreadable-config.js | 40 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-openssl-unreadable-config.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 165fac354a14..f8373cb82d4a 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -4022,8 +4022,15 @@ added: v6.11.0 Load an OpenSSL configuration file on startup. The file can be used as part of a [FIPS mode][] configuration. +If the variable is set to an empty value, Node.js starts without loading any +OpenSSL configuration file. This is a way past a default configuration file +that exists but cannot be read, for example when `/etc/ssl` is not accessible +to the user Node.js runs as, which is otherwise fatal at startup. No +configuration is applied in that case, including any [FIPS mode][] setup the +file would have performed. + If the [`--openssl-config`][] command-line option is used, the environment -variable is ignored. +variable is ignored, and an empty value has no effect. ### `SSL_CERT_DIR=dir` diff --git a/doc/node.1 b/doc/node.1 index 15e3ccfed092..39909107b56c 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -1001,8 +1001,14 @@ When set, Node.js writes JavaScript code coverage information to .It Ev OPENSSL_CONF Ar file Load an OpenSSL configuration file on startup. The file can be used as part of a FIPS mode configuration. +If the variable is set to an empty value, Node.js starts without loading any +OpenSSL configuration file. This is a way past a default configuration file +that exists but cannot be read, for example when \fB/etc/ssl\fR is not accessible +to the user Node.js runs as, which is otherwise fatal at startup. No +configuration is applied in that case, including any FIPS mode setup the +file would have performed. If the \fB--openssl-config\fR command-line option is used, the environment -variable is ignored. +variable is ignored, and an empty value has no effect. . .It Ev SSL_CERT_DIR Ar dir If diff --git a/test/parallel/test-openssl-unreadable-config.js b/test/parallel/test-openssl-unreadable-config.js new file mode 100644 index 000000000000..99681e0ecba0 --- /dev/null +++ b/test/parallel/test-openssl-unreadable-config.js @@ -0,0 +1,40 @@ +'use strict'; + +// A default OpenSSL configuration file that cannot be read is fatal, and an +// empty OPENSSL_CONF is the documented way past it. +// Refs: https://github.com/nodejs/node/issues/62230 + +const common = require('../common'); +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); + +if (!common.hasCrypto) + common.skip('missing crypto'); +if (!common.isLinux) + common.skip('linux only'); +if (process.config.variables.node_shared_openssl) + common.skip('shared openssl may read a different configuration file'); + +// Replace /etc/ssl with an empty tmpfs in a private mount namespace, where +// openssl.cnf is a symlink loop: opening it then fails with ELOOP instead of +// ENOENT, which OpenSSL ignores on its own. The namespace goes away with the +// process, so the host /etc/ssl is left alone. +const setup = 'mount -t tmpfs tmpfs /etc/ssl && ln -s openssl.cnf /etc/ssl/openssl.cnf'; + +if (spawnSync('unshare', ['-Urm', 'sh', '-c', setup]).status !== 0) + common.skip('cannot set up an unprivileged user and mount namespace'); + +function run(env) { + return spawnSync( + 'unshare', + ['-Urm', 'sh', '-c', `${setup} && exec "$0" -p 42`, process.execPath], + { encoding: 'utf8', env: { ...process.env, ...env } }); +} + +const failed = run({}); +assert.notStrictEqual(failed.status, 0); +assert.match(failed.stderr, /OpenSSL configuration error/); + +const skipped = run({ OPENSSL_CONF: '' }); +assert.strictEqual(skipped.status, 0); +assert.strictEqual(skipped.stdout.trim(), '42'); From a59febb84c8adedc65fe64d73f2de54d324e77f3 Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Sat, 25 Apr 2026 12:49:32 +0530 Subject: [PATCH 43/97] test_runner: mock dual-package with conditional exports When `mock.module()` targets a package whose `exports` field maps `import` and `require` to different files, the ESM resolver and the CJS resolver disagree on the resolved path. Only the ESM path was registered in `mockMap`, so `require()` of the mocked specifier bypassed the mock and loaded the real CJS module. Resolve the specifier through `Module._resolveFilename` from the caller's directory in addition to the existing ESM resolution. When the two paths differ, register the CJS path as a second key in `mockMap` and invalidate `Module._cache[cjsPath]`, restoring it on `restore()`. Single-resolution packages keep their existing behavior. Fixes: https://github.com/nodejs/node/issues/58231 Signed-off-by: Maruthan G PR-URL: https://github.com/nodejs/node/pull/62943 Reviewed-By: Aviv Keller Reviewed-By: Moshe Atlow Reviewed-By: Jacob Smith --- lib/internal/test_runner/mock/mock.js | 86 +++++++++++++++++++ test/fixtures/test-runner/mock-nm-dual-pkg.js | 32 +++++++ .../dual-pkg-with-exports/index.cjs | 5 ++ .../dual-pkg-with-exports/index.js | 2 + .../dual-pkg-with-exports/package.json | 12 +++ .../parallel/test-runner-mock-dual-package.js | 36 ++++++++ 6 files changed, 173 insertions(+) create mode 100644 test/fixtures/test-runner/mock-nm-dual-pkg.js create mode 100644 test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.cjs create mode 100644 test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.js create mode 100644 test/fixtures/test-runner/node_modules/dual-pkg-with-exports/package.json create mode 100644 test/parallel/test-runner-mock-dual-package.js diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js index 970356bcae3a..4ac1e0a7e0b6 100644 --- a/lib/internal/test_runner/mock/mock.js +++ b/lib/internal/test_runner/mock/mock.js @@ -20,6 +20,7 @@ const { ReflectConstruct, ReflectGet, SafeMap, + StringPrototypeIncludes, StringPrototypeSlice, StringPrototypeStartsWith, } = primordials; @@ -207,6 +208,7 @@ class MockModuleContext { baseURL, cache, caller, + cjsPath, format, fullPath, moduleExports, @@ -222,12 +224,25 @@ class MockModuleContext { sharedState.mockMap.set(baseURL, config); sharedState.mockMap.set(fullPath, config); + // For dual packages (e.g., a package with a "exports" field that exposes + // both ESM and CJS entry points), the file selected by the ESM resolver + // (used to compute fullPath) may differ from the one selected by CJS + // require(). Register the CJS-resolved path so that require() also picks + // up the mock. See https://github.com/nodejs/node/issues/58231. + if (cjsPath !== null && cjsPath !== fullPath) { + sharedState.mockMap.set(cjsPath, config); + } this.#sharedState = sharedState; this.#restore = { __proto__: null, baseURL, cached: fullPath in Module._cache, + cjsPath, + cjsCached: cjsPath !== null && cjsPath !== fullPath && + cjsPath in Module._cache, + cjsValue: cjsPath !== null && cjsPath !== fullPath ? + Module._cache[cjsPath] : undefined, format, fullPath, value: Module._cache[fullPath], @@ -257,6 +272,9 @@ class MockModuleContext { } delete Module._cache[fullPath]; + if (cjsPath !== null && cjsPath !== fullPath) { + delete Module._cache[cjsPath]; + } sharedState.mockExports.set(baseURL, { __proto__: null, moduleExports, @@ -276,6 +294,14 @@ class MockModuleContext { Module._cache[this.#restore.fullPath] = this.#restore.value; } + if (this.#restore.cjsPath !== null && + this.#restore.cjsPath !== this.#restore.fullPath) { + delete Module._cache[this.#restore.cjsPath]; + if (this.#restore.cjsCached) { + Module._cache[this.#restore.cjsPath] = this.#restore.cjsValue; + } + } + const mock = mocks.get(this.#restore.baseURL); if (mock !== undefined) { @@ -285,6 +311,10 @@ class MockModuleContext { this.#sharedState.mockMap.delete(this.#restore.baseURL); this.#sharedState.mockMap.delete(this.#restore.fullPath); + if (this.#restore.cjsPath !== null && + this.#restore.cjsPath !== this.#restore.fullPath) { + this.#sharedState.mockMap.delete(this.#restore.cjsPath); + } this.#restore = undefined; } } @@ -680,11 +710,19 @@ class MockTracker { const fullPath = StringPrototypeStartsWith(url, 'file://') ? fileURLToPath(url) : null; + // For dual packages, the ESM resolver may return a different file than + // CJS require() would for the same specifier (e.g., when a package's + // "exports" field points to different files for the "import" and + // "require" conditions). Compute the CJS-resolved path so that + // require() of a mocked module also picks up the mock. + // See https://github.com/nodejs/node/issues/58231. + const cjsPath = resolveAsCJS(mockSpecifier, caller, fullPath); const ctx = new MockModuleContext({ __proto__: null, baseURL: baseURL.href, cache, caller, + cjsPath, format, fullPath, moduleExports, @@ -987,6 +1025,54 @@ function cjsMockModuleLoad(request, parent, isMain) { return modExports; } +// Resolve `specifier` using CJS resolution rules so that mocks for dual +// packages (e.g., a package whose "exports" field points to different files +// for the "import" and "require" conditions) also intercept require(). +// Returns an absolute file path on success, or null when the specifier cannot +// be resolved as CJS (for example, when the package is ESM-only or when it is +// a non-file URL such as data: or node:). +function resolveAsCJS(specifier, callerURL, esmFullPath) { + if (isBuiltin(specifier) || + StringPrototypeStartsWith(specifier, 'node:') || + StringPrototypeStartsWith(specifier, 'data:')) { + return null; + } + + let parentPath; + if (StringPrototypeStartsWith(callerURL, 'file://')) { + try { + parentPath = fileURLToPath(callerURL); + } catch { + return null; + } + } else { + return null; + } + + try { + const tmpModule = new Module(parentPath, null); + tmpModule.paths = _nodeModulePaths(parentPath); + const resolved = _resolveFilename(specifier, tmpModule, false); + if (typeof resolved !== 'string') { + return null; + } + // If the resolution matches what the ESM resolver picked, there is + // nothing additional to register. + if (resolved === esmFullPath) { + return esmFullPath; + } + // If the resolution returned something that is not a filesystem path + // (e.g., a builtin id without a slash or backslash), ignore it. + if (!StringPrototypeIncludes(resolved, '/') && + !StringPrototypeIncludes(resolved, '\\')) { + return null; + } + return resolved; + } catch { + return null; + } +} + function validateStringOrSymbol(value, name) { if (typeof value !== 'string' && typeof value !== 'symbol') { throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value); diff --git a/test/fixtures/test-runner/mock-nm-dual-pkg.js b/test/fixtures/test-runner/mock-nm-dual-pkg.js new file mode 100644 index 000000000000..3686373c6032 --- /dev/null +++ b/test/fixtures/test-runner/mock-nm-dual-pkg.js @@ -0,0 +1,32 @@ +'use strict'; +const assert = require('node:assert'); +const { test } = require('node:test'); +const fixture = 'dual-pkg-with-exports'; + +test('mock node_modules dual package with conditional exports', async (t) => { + const mock = t.mock.module(fixture, { + namedExports: { add(x, y) { return 1 + x + y; }, flavor: 'mocked' }, + }); + + // CJS require should pick up the mock even though the package's "exports" + // field maps the "require" condition to a different file than "import". + const cjsImpl = require(fixture); + assert.strictEqual(cjsImpl.add(4, 5), 10); + assert.strictEqual(cjsImpl.flavor, 'mocked'); + + // ESM dynamic import should also pick up the mock. + const esmImpl = await import(fixture); + assert.strictEqual(esmImpl.add(4, 5), 10); + assert.strictEqual(esmImpl.flavor, 'mocked'); + + mock.restore(); + + // After restore, both module systems should see the original exports. + const restoredCjs = require(fixture); + assert.strictEqual(restoredCjs.add(4, 5), 9); + assert.strictEqual(restoredCjs.flavor, 'cjs'); + + const restoredEsm = await import(fixture); + assert.strictEqual(restoredEsm.add(4, 5), 9); + assert.strictEqual(restoredEsm.flavor, 'esm'); +}); diff --git a/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.cjs b/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.cjs new file mode 100644 index 000000000000..a8085de75d60 --- /dev/null +++ b/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.cjs @@ -0,0 +1,5 @@ +'use strict'; +const add = (x, y) => x + y; +const flavor = 'cjs'; + +module.exports = { add, flavor }; diff --git a/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.js b/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.js new file mode 100644 index 000000000000..f9e72d7f62fb --- /dev/null +++ b/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/index.js @@ -0,0 +1,2 @@ +export const add = (x, y) => x + y; +export const flavor = 'esm'; diff --git a/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/package.json b/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/package.json new file mode 100644 index 000000000000..e225302a770e --- /dev/null +++ b/test/fixtures/test-runner/node_modules/dual-pkg-with-exports/package.json @@ -0,0 +1,12 @@ +{ + "name": "dual-pkg-with-exports", + "type": "module", + "main": "index.js", + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs" + } + }, + "private": true +} diff --git a/test/parallel/test-runner-mock-dual-package.js b/test/parallel/test-runner-mock-dual-package.js new file mode 100644 index 000000000000..f96837d2cebf --- /dev/null +++ b/test/parallel/test-runner-mock-dual-package.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('registering customization hooks in Workers does not work'); +} + +const fixtures = require('../common/fixtures'); +const assert = require('node:assert'); +const { test } = require('node:test'); + +// Regression test for https://github.com/nodejs/node/issues/58231 +// When a dual package exposes both ESM and CJS entry points via the +// "exports" field with "import"/"require" conditions, the ESM resolver +// picks one file (e.g. index.js) and CJS require() picks another +// (e.g. index.cjs). mock.module() must intercept both so that require() +// of the mocked module does not return the original CJS file. +test('mock.module intercepts dual package require with conditional exports', + async () => { + const cwd = fixtures.path('test-runner'); + const fixture = fixtures.path('test-runner', 'mock-nm-dual-pkg.js'); + const args = ['--experimental-test-module-mocks', fixture]; + const { + code, + stdout, + signal, + } = await common.spawnPromisified(process.execPath, args, { cwd }); + + assert.strictEqual(signal, null); + assert.strictEqual(code, 0, + 'child process exited with non-zero status\n' + + `stdout:\n${stdout}`); + assert.match(stdout, /pass 1/); + assert.match(stdout, /fail 0/); + }); From 4baafe3c6ca1d9b0de40d51de0e7ffc54135cbd7 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Sun, 15 Mar 2026 23:51:03 +0100 Subject: [PATCH 44/97] test_runner: use run options with isolation="none" When using run() programatically with isolation="none", testNamePatterns, testSkipPattersn, and only were ignored. This combination of options only worked when set via CLI flags, because parseCommandLine() is still used to seed globalOptions. Fixes: https://github.com/nodejs/node/issues/57399 Signed-off-by: Sylvester Keil PR-URL: https://github.com/nodejs/node/pull/62269 Reviewed-By: Ethan Arrowood Reviewed-By: Chemi Atlow Reviewed-By: Pietro Marchini --- lib/internal/test_runner/runner.js | 12 ++++++ .../test-runner-isolation-none.mjs | 32 ++++++++++++++ test/parallel/test-runner-run.mjs | 42 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 test/fixtures/test-runner/test-runner-isolation-none.mjs diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 561254fb5624..8bfdca00bf5c 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -941,6 +941,18 @@ function run(options = kEmptyObject) { testTagFilters, }; + if (isolation === 'none') { + if (testNamePatterns != null) { + globalOptions.testNamePatterns = testNamePatterns; + } + if (testSkipPatterns != null) { + globalOptions.testSkipPatterns = testSkipPatterns; + } + if (only != null) { + globalOptions.only = only; + } + } + const root = createTestTree(rootTestOptions, globalOptions); let testFiles = files ?? createTestFileList(globPatterns, cwd); const { isTestRunner } = globalOptions; diff --git a/test/fixtures/test-runner/test-runner-isolation-none.mjs b/test/fixtures/test-runner/test-runner-isolation-none.mjs new file mode 100644 index 000000000000..03a32fd6ad57 --- /dev/null +++ b/test/fixtures/test-runner/test-runner-isolation-none.mjs @@ -0,0 +1,32 @@ +import { run } from 'node:test'; +import { tap } from 'node:test/reporters'; +import { parseArgs } from 'node:util'; + +const { + values, +} = parseArgs({ + args: process.argv.slice(2), + options: { + file: { type: 'string' }, + only: { type: 'boolean' }, + 'name-pattern': { type: 'string' }, + 'skip-pattern': { type: 'string' }, + }, +}); + +const opts = { + isolation: 'none', + files: [values.file], +}; + +if (values.only) { + opts.only = true; +} +if (values['name-pattern']) { + opts.testNamePatterns = [new RegExp(values['name-pattern'])]; +} +if (values['skip-pattern']) { + opts.testSkipPatterns = [new RegExp(values['skip-pattern'])]; +} + +run(opts).compose(tap).pipe(process.stdout); \ No newline at end of file diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index defacb4c3c30..947da4a77d8b 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -872,6 +872,48 @@ describe('forceExit', () => { }); }); +describe('with isolation="none"', () => { + const isolationNoneFixture = fixtures.path('test-runner', 'test-runner-isolation-none.mjs'); + + it('should pass only to children', async () => { + const child = await common.spawnPromisified(process.execPath, [ + isolationNoneFixture, + '--file', join(testFixtures, 'test_only.js'), + '--only', + ]); + + assert.strictEqual(child.stderr, ''); + assert.strictEqual(child.code, 0); + assert.match(child.stdout, /ok 1 - this should be executed/); + assert.match(child.stdout, /# tests 1/); + }); + + it('should skip tests not matching testNamePatterns - RegExp', async () => { + const child = await common.spawnPromisified(process.execPath, [ + isolationNoneFixture, + '--file', join(testFixtures, 'default-behavior/test/skip_by_name.cjs'), + '--name-pattern', 'executed', + ]); + + assert.strictEqual(child.stderr, ''); + assert.strictEqual(child.code, 0); + assert.match(child.stdout, /ok 1 - this should be executed/); + assert.match(child.stdout, /# tests 1/); + }); + + it('should skip tests matching testSkipPatterns - RegExp', async () => { + const child = await common.spawnPromisified(process.execPath, [ + isolationNoneFixture, + '--file', join(testFixtures, 'default-behavior/test/skip_by_name.cjs'), + '--skip-pattern', 'skipped', + ]); + + assert.strictEqual(child.stderr, ''); + assert.strictEqual(child.code, 0); + assert.match(child.stdout, /ok 1 - this should be executed/); + assert.match(child.stdout, /# tests 1/); + }); +}); // exitHandler doesn't run until after the tests / after hooks finish. process.on('exit', () => { From 9ad61b85411b949b72a8072eca772e6215a3ac44 Mon Sep 17 00:00:00 2001 From: Shivay-98 Date: Mon, 20 Jul 2026 21:39:29 -0700 Subject: [PATCH 45/97] net: handle undefined parent in _unrefTimer and _destroy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix and approach are from #64491 by Shivay-98; this reopens it to get it landed, since the original stalled awaiting requested changes. `Socket.prototype._unrefTimer` and `Socket.prototype._destroy` both walk the `_parent` chain with a strict `!== null` check. During connection teardown a socket's `_parent` can be left `undefined` (for example a TLS socket layered over another stream), so the loop steps onto `undefined` and reads a property off it, throwing a TypeError: Cannot read properties of undefined (reading 'Symbol(timeout)') from an uncaught I/O callback and crashing the process. Using a nullish (`!= null`) check terminates the walk on both `null` and `undefined`. [petter@hightouch.io: apply the same fix to the identical loop in `_destroy`, which the original regression test already exercised via `destroy()`; add direct unit coverage for both paths.] Fixes: https://github.com/nodejs/node/issues/64490 Refs: https://github.com/nodejs/node/pull/64491 Signed-off-by: Petter Häggholm PR-URL: https://github.com/nodejs/node/pull/64644 Fixes: https://github.com/nodejs/node/issues/64490 Refs: https://github.com/nodejs/node/pull/64491 Reviewed-By: Tim Perry Reviewed-By: Matteo Collina Reviewed-By: Ethan Arrowood Reviewed-By: Luigi Pinca --- lib/net.js | 8 +++- ...est-net-socket-unref-timer-parent-chain.js | 25 ++++++++++++ .../test-net-unref-timer-parent-undefined.js | 39 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-net-socket-unref-timer-parent-chain.js create mode 100644 test/parallel/test-net-unref-timer-parent-undefined.js diff --git a/lib/net.js b/lib/net.js index 55fc6ea843ce..36e1531a780c 100644 --- a/lib/net.js +++ b/lib/net.js @@ -736,7 +736,9 @@ ObjectSetPrototypeOf(Socket, stream.Duplex); // Refresh existing timeouts. Socket.prototype._unrefTimer = function _unrefTimer() { - for (let s = this; s !== null; s = s._parent) { + // `_parent` may be null; we use a loose `!= null` check in case external + // code sets it to undefined. + for (let s = this; s != null; s = s._parent) { if (s[kTimeout]) s[kTimeout].refresh(); } @@ -1097,7 +1099,9 @@ Socket.prototype._destroy = function(exception, cb) { this.connecting = false; - for (let s = this; s !== null; s = s._parent) { + // `_parent` may be null; we use a loose `!= null` check in case external + // code sets it to undefined. + for (let s = this; s != null; s = s._parent) { clearTimeout(s[kTimeout]); } diff --git a/test/parallel/test-net-socket-unref-timer-parent-chain.js b/test/parallel/test-net-socket-unref-timer-parent-chain.js new file mode 100644 index 000000000000..36a88f1c158e --- /dev/null +++ b/test/parallel/test-net-socket-unref-timer-parent-chain.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); + +// Walking the `_parent` chain must stop on a nullish link, not only strict +// `null`. During connection teardown a socket's `_parent` can be left +// `undefined`, which previously caused `_unrefTimer()` and `_destroy()` to read +// a property off `undefined` and throw. +// Refs: https://github.com/nodejs/node/issues/64490 + +const assert = require('assert'); +const net = require('net'); + +{ + const socket = new net.Socket(); + socket._parent = undefined; + socket._unrefTimer(); +} + +{ + const socket = new net.Socket(); + socket._parent = undefined; + socket.on('error', common.mustNotCall()); + socket.destroy(); + assert.strictEqual(socket.destroyed, true); +} diff --git a/test/parallel/test-net-unref-timer-parent-undefined.js b/test/parallel/test-net-unref-timer-parent-undefined.js new file mode 100644 index 000000000000..01e45e82f908 --- /dev/null +++ b/test/parallel/test-net-unref-timer-parent-undefined.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +// A TLS socket whose `_parent` is left `undefined` during teardown must not +// crash when reads land on it (`onStreamRead` -> `_unrefTimer`) or when it is +// destroyed (`_destroy`). Both walk the `_parent` chain. +// Refs: https://github.com/nodejs/node/issues/64490 + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), +}; + +const server = tls.createServer(options, common.mustCall((conn) => { + setTimeout(() => conn.write('x'), 50); +})); + +server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + }, common.mustCall(() => { + client._parent = undefined; + })); + + client.on('data', common.mustCall(() => { + server.close(); + client.destroy(); + })); + + client.on('error', common.mustNotCall()); +})); From f143c70506af6f23ce8f53adcf62793ba50e2650 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 22 Aug 2026 11:46:41 +0900 Subject: [PATCH 46/97] test: deflake test-net-listen-ipv6only The test verified ipv6Only by connecting to the IPv4 side of an ephemeral port and expecting ECONNREFUSED, but it never reserved that IPv4 port. Under parallel execution another test could occupy it, making the connection succeed instead of being refused. Run the test sequentially with a fixed common.PORT so it no longer competes with other tests for the same port, matching the fix already applied to the sibling cluster variants. Fixes: https://github.com/nodejs/node/issues/64172 Signed-off-by: sangwook PR-URL: https://github.com/nodejs/node/pull/64173 Reviewed-By: Luigi Pinca --- test/{parallel => sequential}/test-net-listen-ipv6only.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename test/{parallel => sequential}/test-net-listen-ipv6only.js (68%) diff --git a/test/parallel/test-net-listen-ipv6only.js b/test/sequential/test-net-listen-ipv6only.js similarity index 68% rename from test/parallel/test-net-listen-ipv6only.js rename to test/sequential/test-net-listen-ipv6only.js index a329011bcc8a..c85f813bc08b 100644 --- a/test/parallel/test-net-listen-ipv6only.js +++ b/test/sequential/test-net-listen-ipv6only.js @@ -11,9 +11,13 @@ const net = require('net'); const host = '::'; const server = net.createServer(); +// Use a fixed port and run this test sequentially. The assertion below relies +// on nothing else listening on the IPv4 side of the chosen port; with an +// ephemeral port under parallel execution another test can occupy that IPv4 +// port, making the connection succeed instead of being refused. server.listen({ host, - port: 0, + port: common.PORT, ipv6Only: true, }, common.mustCall(() => { const { port } = server.address(); From ed69a211bf7700add24cc7351ca7274a571439e4 Mon Sep 17 00:00:00 2001 From: Donghoon Kang Date: Sat, 22 Aug 2026 12:49:24 +0900 Subject: [PATCH 47/97] doc: fix broken GYP link in n-api.md Replace the unavailable GYP website with the GYP documentation preserved in the Chromium source repository. Signed-off-by: HoonDongKang PR-URL: https://github.com/nodejs/node/pull/65413 Reviewed-By: Luigi Pinca Reviewed-By: Aviv Keller Reviewed-By: Chengzhong Wu --- doc/api/n-api.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 87f1561cbd90..648d17a0f2d9 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -194,9 +194,8 @@ the native addon. #### node-gyp -[node-gyp][] is a build system based on the [gyp-next][] fork of -Google's [GYP][] tool and comes bundled with npm. GYP, and therefore node-gyp, -requires that Python be installed. +[node-gyp][] is a build system based on the [gyp-next][] tool and comes bundled with npm. +node-gyp requires that Python be installed. Historically, node-gyp has been the tool of choice for building native addons. It has widespread adoption and documentation. However, some @@ -6952,7 +6951,6 @@ the add-on's file name during loading. [ECMAScript Language Specification]: https://tc39.es/ecma262/ [Error handling]: #error-handling [GCC]: https://gcc.gnu.org -[GYP]: https://gyp.gsrc.io [GitHub releases]: https://help.github.com/en/github/administering-a-repository/about-releases [LLVM]: https://llvm.org [Native Abstractions for Node.js]: https://github.com/nodejs/nan From 8689612976e75f3c90f842ab97a68bfa1763d31c Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sat, 22 Aug 2026 01:07:59 -0400 Subject: [PATCH 48/97] util: fix formatting of functions returned from getters Fixes: https://github.com/nodejs/node/issues/64838 Signed-off-by: Richard Gibson PR-URL: https://github.com/nodejs/node/pull/64839 Reviewed-By: Aviv Keller --- lib/internal/util/inspect.js | 2 +- test/parallel/test-util-inspect.js | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 88110719e417..89b30b4304c4 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -2304,7 +2304,7 @@ function formatProperty(ctx, value, recurseTimes, key, type, desc, const tmp = FunctionPrototypeCall(desc.get, original); if (tmp === null) { str = `${s(`[${label}:`, sp)} ${s('null', 'null')}${s(']', sp)}`; - } else if (typeof tmp === 'object') { + } else if (typeof tmp === 'object' || typeof tmp === 'function') { str = `${s(`[${label}]`, sp)} ${formatValue(ctx, tmp, recurseTimes)}`; } else { const primitive = formatPrimitive(s, tmp, ctx); diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index c8b37f2a264b..d4dfe9acf006 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -2596,6 +2596,15 @@ assert.strictEqual( "'foobar', { x: 1 } },\n inc: [Getter: NaN]\n}"); } +// Getter returning a function. +// https://github.com/nodejs/node/issues/64838 +{ + const obj = { get foo() { return function bar() {}; } }; + assert.strictEqual( + inspect(obj, { getters: true }), + '{ foo: [Getter] [Function: bar] }'); +} + // Property getter throwing an error. { const error = new Error('Oops'); @@ -3524,16 +3533,14 @@ assert.strictEqual( '\x1B[2mdef: \x1B[33m5\x1B[39m\x1B[22m }' ); - assert.match( + assert.strictEqual( inspect(Object.getPrototypeOf(bar), { showHidden: true, getters: true }), - new RegExp('^' + RegExp.escape( - ' Foo [Map] {\n' + - ' [constructor]: [class Bar extends Foo] {\n' + + ' Foo [Map] {\n' + + ' [constructor]: [class Bar extends Foo] {\n' + ' [length]: 0,\n' + " [name]: 'Bar',\n" + - ' [prototype]: [Circular *1],\n' + - ' [Symbol(Symbol.species)]: [Getter: ]\n' + + ' [prototype]: [Circular *2],\n' + + ' [Symbol(Symbol.species)]: [Getter] [Circular *1]\n' + ' },\n' + " [xyz]: [Getter: 'YES!'],\n" + ' [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] {\n' + @@ -3543,7 +3550,6 @@ assert.strictEqual( ' [abc]: [Getter: true],\n' + ' [def]: [Getter/Setter: false]\n' + '}' - ) + '$', 's') ); assert.strictEqual( From 66b99025024e3c8db1660be48edf3f49d2bd957f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=98=9C=EB=AF=B8?= <103042868+hyemimi@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:59:46 +0900 Subject: [PATCH 49/97] doc: remove outdated WASI version fallback Signed-off-by: hyemimi PR-URL: https://github.com/nodejs/node/pull/65303 Refs: https://github.com/nodejs/node/pull/47391 Reviewed-By: Daeyeon Jeong --- doc/api/wasi.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/api/wasi.md b/doc/api/wasi.md index 7303e0eeabbe..6f477a765daf 100644 --- a/doc/api/wasi.md +++ b/doc/api/wasi.md @@ -197,8 +197,7 @@ If version `unstable` was passed into the constructor it will return: { wasi_unstable: wasi.wasiImport } ``` -If version `preview1` was passed into the constructor or no version was -specified it will return: +If version `preview1` was passed into the constructor it will return: ```json { wasi_snapshot_preview1: wasi.wasiImport } From 44defdc1da60d9c5cd43f84d4a938a9b4d934718 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 22 Aug 2026 16:24:44 +0200 Subject: [PATCH 50/97] tools: improve commit queue failure comment Commit Queue failure comments hid the actionable reason inside the collapsed landing transcript. Surface the reason and retry instructions before the full output. Add specific guidance for a missing multi-commit policy and explicit reasons for push and squash-merge failures. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65433 Reviewed-By: James M Snell Reviewed-By: Antoine du Hamel Reviewed-By: Yagiz Nizipli --- tools/actions/commit-queue.sh | 60 ++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/tools/actions/commit-queue.sh b/tools/actions/commit-queue.sh index deb2edf7cf2f..9fb74ed9cad8 100755 --- a/tools/actions/commit-queue.sh +++ b/tools/actions/commit-queue.sh @@ -10,12 +10,62 @@ COMMIT_QUEUE_FAILED_LABEL="commit-queue-failed" cqurl="${GITHUB_SERVER_URL:?}/${GITHUB_REPOSITORY:?}/actions/runs/${GITHUB_RUN_ID:?}" +escape_code_block_or_line() { + case $1 in + *" +"*|'') fence='```' sep=' +' ;; + *[![:space:]]*) fence='`' sep=' ' ;; + *) fence='`' sep='' ;; + esac + while case $1 in *"$fence"*) ;; *) false ;; esac; do + fence=$fence'`' + done + printf '%s%s%s%s%s\n' "$fence" "$sep" "$1" "$sep" "$fence" +} + commit_queue_failed() { pr=$1 + reported_failure=${2:-} gh -R "$GITHUB_REPOSITORY" pr edit "$pr" --add-label "${COMMIT_QUEUE_FAILED_LABEL}" --remove-label "${COMMIT_QUEUE_LABEL}" - body="
Commit Queue failed
$(sed -e 's/&/\&/g' -e 's//\>/g' output)
$cqurl
" + last_output_line=$(awk 'NF { line = $0 } END { sub(/^[[:space:]]*/, "", line); print line }' output) + # shellcheck disable=SC2016 + missing_policy_message='ℹ Add `commit-queue-squash` label to land the PR as one commit, or `commit-queue-rebase` to land as separate commits.' + if [ "$last_output_line" = "$missing_policy_message" ]; then + failure_body='This pull request has multiple commits, but no landing policy was selected. + +Add https://github.com/nodejs/node/labels/commit-queue-squash to land it as one commit, or https://github.com/nodejs/node/labels/commit-queue-rebase to land the commits separately.' + else + if [ -z "$reported_failure" ]; then + reported_failure=$(grep -e '✘' -e '⚠' output | tail -n 10) + fi + if [ -z "$reported_failure" ]; then + reported_failure=$(tail -n 10 output) + fi + if [ -z "$reported_failure" ]; then + reported_failure='No failure reason was reported.' + fi + failure_body=$(escape_code_block_or_line "$reported_failure") + fi + + raw_output=$(cat output) + + body="### Commit Queue failed + +$failure_body + +The pull request was removed from the Commit Queue and labeled https://github.com/nodejs/node/labels/commit-queue-failed. After resolving the failure, remove that label and add https://github.com/nodejs/node/labels/commit-queue to retry. + +
+Full Commit Queue output + +$(escape_code_block_or_line "$raw_output") + +
+ +[View workflow run]($cqurl)" echo "$body" gh -R "$GITHUB_REPOSITORY" pr comment "$pr" --body "$body" @@ -63,7 +113,8 @@ for pr in "$@"; do commits="${start_sha}...${end_sha}" if ! git push $UPSTREAM $DEFAULT_BRANCH >> output 2>&1; then - commit_queue_failed "$pr" + commit_queue_failed "$pr" \ + "Failed to push the landed commits to ${UPSTREAM}/${DEFAULT_BRANCH}." continue fi else @@ -80,9 +131,10 @@ for pr in "$@"; do --arg head "${commit_head}" \ '{merge_method:"squash",commit_title:$title,commit_message:$body,sha:$head}' |\ gh api -X PUT "repos/${GITHUB_REPOSITORY}/pulls/${pr}/merge" --input -\ - --jq 'if .merged then .sha else halt_error end' + --jq 'if .merged then .sha else halt_error end' 2>> output )"; then - commit_queue_failed "$pr" + commit_queue_failed "$pr" \ + 'GitHub failed to squash and merge this pull request.' continue fi fi From 41c81a234460995d47217734915906406515852a Mon Sep 17 00:00:00 2001 From: Luan Muniz Date: Sat, 22 Aug 2026 19:18:16 +0200 Subject: [PATCH 51/97] benchmark: add test-only and mock timers cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only benchmark covers creating selected and non-selected tests when running with --test-only. The mock timers benchmark covers enabling timer mocks, setTimeout, setInterval, setImmediate, scheduler.wait, AbortSignal.timeout, mocked Date.now(), setTime(), and runAll(). Refs: https://github.com/nodejs/node/issues/55723 Signed-off-by: Luan Muniz PR-URL: https://github.com/nodejs/node/pull/64097 Reviewed-By: Aviv Keller Reviewed-By: Rafael Gonzaga Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Trivikram Kamat --- benchmark/test_runner/mock-timers.js | 262 +++++++++++++++++++++++++++ benchmark/test_runner/test-only.js | 39 ++++ 2 files changed, 301 insertions(+) create mode 100644 benchmark/test_runner/mock-timers.js create mode 100644 benchmark/test_runner/test-only.js diff --git a/benchmark/test_runner/mock-timers.js b/benchmark/test_runner/mock-timers.js new file mode 100644 index 000000000000..4815c20ecd73 --- /dev/null +++ b/benchmark/test_runner/mock-timers.js @@ -0,0 +1,262 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { test } = require('node:test'); +const nodeTimersPromises = require('node:timers/promises'); + +const bench = common.createBenchmark(main, { + n: [1000], + mode: [ + 'enable-empty-apis', + 'enable-setTimeout', + 'enable-setInterval', + 'enable-setImmediate', + 'enable-Date', + 'enable-scheduler.wait', + 'enable-AbortSignal.timeout', + 'enable-all', + 'enable-default', + 'setTimeout', + 'setInterval', + 'setImmediate', + 'scheduler.wait', + 'AbortSignal.timeout', + 'Date', + 'setTime', + 'runAll', + ], +}, { + // We don't want to test the reporter here. + flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], +}); + +function benchmarkEnable(n, mode) { + const enableMode = mode.replace('enable-', ''); + let enableOptions = { apis: [enableMode] }; + + if (enableMode === 'all') { + enableOptions.apis = ['setTimeout', 'setInterval', 'setImmediate', 'Date', 'scheduler.wait', 'AbortSignal.timeout']; + } + + if (enableMode === 'empty-apis') { + enableOptions.apis = []; + } + + if (enableMode === 'default') { + enableOptions = undefined; + } + + test((t) => { + bench.start(); + + for (let i = 0; i < n; i++) { + t.mock.timers.enable(enableOptions); + t.mock.timers.reset(); + } + + bench.end(n); + }); +} + +function benchmarkSetTimeout(n) { + test((t) => { + let noDead; + + t.mock.timers.enable({ apis: ['setTimeout'] }); + bench.start(); + + for (let i = 0; i < n; i++) { + setTimeout(() => { + noDead = i; + }, i + 1); + } + + t.mock.timers.tick(n + 1); + bench.end(n); + + assert.strictEqual(noDead, n - 1); + }); +} + +function benchmarkSetInterval(n) { + test((t) => { + let noDead = 0; + + t.mock.timers.enable({ apis: ['setInterval'] }); + + setInterval(() => { + noDead++; + }, 1); + + bench.start(); + + t.mock.timers.tick(n); + + bench.end(n); + + assert.strictEqual(noDead, n); + }); +} + +function benchmarkSetImmediate(n) { + test((t) => { + let noDead; + + t.mock.timers.enable({ apis: ['setImmediate'] }); + + bench.start(); + + for (let i = 0; i < n; i++) { + setImmediate(() => { + noDead = i; + }); + } + + t.mock.timers.tick(0); + bench.end(n); + + assert.strictEqual(noDead, n - 1); + }); +} + +function benchmarkSchedulerWait(n) { + test(async (t) => { + const promises = []; + let noDead; + + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + + bench.start(); + + for (let i = 0; i < n; i++) { + promises.push(nodeTimersPromises.scheduler.wait(i + 1).then(() => { + noDead = i; + })); + } + + t.mock.timers.tick(n + 1); + await Promise.all(promises); + bench.end(n); + + assert.strictEqual(noDead, n - 1); + }); +} + +function benchmarkAbortSignalTimeout(n) { + test((t) => { + let noDead; + + t.mock.timers.enable({ apis: ['AbortSignal.timeout'] }); + + bench.start(); + + for (let i = 0; i < n; i++) { + noDead = AbortSignal.timeout(i + 1); + } + + t.mock.timers.tick(n + 1); + bench.end(n); + + assert.strictEqual(noDead.aborted, true); + }); +} + +function benchmarkDate(n) { + test((t) => { + let noDead; + + t.mock.timers.enable({ apis: ['Date'] }); + + bench.start(); + + for (let i = 0; i < n; i++) { + noDead = Date.now(); + } + + bench.end(n); + + assert.strictEqual(noDead, 0); + }); +} + +function benchmarkSetTime(n) { + test((t) => { + let noDead; + + t.mock.timers.enable({ apis: ['Date'] }); + + bench.start(); + + for (let i = 0; i < n; i++) { + t.mock.timers.setTime(i); + noDead = Date.now(); + } + + bench.end(n); + + assert.strictEqual(noDead, n - 1); + }); +} + +function benchmarkRunAll(n) { + test((t) => { + let noDead = 0; + + t.mock.timers.enable({ apis: ['setTimeout'] }); + + for (let i = 0; i < n; i++) { + setTimeout(() => { + noDead++; + }, i + 1); + } + + bench.start(); + + t.mock.timers.runAll(); + + bench.end(n); + + assert.strictEqual(noDead, n); + }); +} + +function main({ n, mode }) { + switch (mode) { + case 'enable-empty-apis': + case 'enable-setTimeout': + case 'enable-setInterval': + case 'enable-setImmediate': + case 'enable-Date': + case 'enable-scheduler.wait': + case 'enable-AbortSignal.timeout': + case 'enable-all': + case 'enable-default': + benchmarkEnable(n, mode); + break; + case 'setTimeout': + benchmarkSetTimeout(n); + break; + case 'setInterval': + benchmarkSetInterval(n); + break; + case 'setImmediate': + benchmarkSetImmediate(n); + break; + case 'scheduler.wait': + benchmarkSchedulerWait(n); + break; + case 'AbortSignal.timeout': + benchmarkAbortSignalTimeout(n); + break; + case 'Date': + benchmarkDate(n); + break; + case 'setTime': + benchmarkSetTime(n); + break; + case 'runAll': + benchmarkRunAll(n); + break; + } +} diff --git a/benchmark/test_runner/test-only.js b/benchmark/test_runner/test-only.js new file mode 100644 index 000000000000..fe79f10dfdd8 --- /dev/null +++ b/benchmark/test_runner/test-only.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +const { finished } = require('node:stream/promises'); +const reporter = require('../fixtures/empty-test-reporter'); +const { test } = require('node:test'); + +const bench = common.createBenchmark(main, { + n: [10000], + selected: [1], +}, { + // We don't want to test the reporter here. + flags: [ + '--test-reporter=./benchmark/fixtures/empty-test-reporter.js', + '--test-only', + ], +}); + +async function run({ n, selected }) { + for (let i = 0; i < selected; i++) { + test(`selected-${i}`, { only: true }, () => {}); + } + + for (let i = 0; i < n; i++) { + test(`not-selected-${i}`, () => { + throw new Error(`This test ${i} should not run.`); + }); + } + + return finished(reporter); +} + +function main(params) { + bench.start(); + + run(params).then(() => { + bench.end(params.n); + }); +} From e3ae11417da3aefd6d6b9550f7c34cafd70a71ea Mon Sep 17 00:00:00 2001 From: GetThatCookie Date: Sat, 22 Aug 2026 19:44:04 +0200 Subject: [PATCH 52/97] http: cache maxHeaderPairs per header section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: GetThatCookie PR-URL: https://github.com/nodejs/node/pull/64988 Reviewed-By: Robert Nagy Reviewed-By: Tim Perry Reviewed-By: Gürgün Dayıoğlu --- benchmark/http/bench-parser.js | 2 + src/node_http_parser.cc | 30 +++++--- ...test-http-parser-max-header-pairs-cache.js | 77 +++++++++++++++++++ 3 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 test/parallel/test-http-parser-max-header-pairs-cache.js diff --git a/benchmark/http/bench-parser.js b/benchmark/http/bench-parser.js index 0a1e8f7b5e8a..72cb2b6feb18 100644 --- a/benchmark/http/bench-parser.js +++ b/benchmark/http/bench-parser.js @@ -31,6 +31,8 @@ function main({ len, n }) { function newParser(type) { const parser = new HTTPParser(); parser.initialize(type, {}); + // Direct parsers bypass cleanParser(); use its production default. + parser.maxHeaderPairs = 2000; parser.headers = []; diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index f0f3795100e1..50e1fff07193 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -322,6 +322,7 @@ class Parser : public AsyncWrap, public StreamListener { allocator_.Reset(); url_.Reset(); status_message_.Reset(); + max_header_pairs_ = -1; if (connectionsList_ != nullptr) { connectionsList_->Push(this); @@ -464,6 +465,7 @@ class Parser : public AsyncWrap, public StreamListener { num_fields_ = 0; num_values_ = 0; header_pairs_ = 0; + max_header_pairs_ = -1; // METHOD if (parser_.type == HTTP_REQUEST) { @@ -1032,6 +1034,7 @@ class Parser : public AsyncWrap, public StreamListener { headers_completed_ = false; max_http_header_size_ = max_http_header_size; header_pairs_ = 0; + max_header_pairs_ = -1; } @@ -1051,21 +1054,23 @@ class Parser : public AsyncWrap, public StreamListener { header_pairs_ += 2; - Local max_header_pairs_v; - if (!object() - ->Get(env()->context(), - FIXED_ONE_BYTE_STRING(env()->isolate(), "maxHeaderPairs")) - .ToLocal(&max_header_pairs_v)) { - got_exception_ = true; - return -1; - } + if (max_header_pairs_ < 0) { + Local max_header_pairs_v; + if (!object() + ->Get(env()->context(), + FIXED_ONE_BYTE_STRING(env()->isolate(), "maxHeaderPairs")) + .ToLocal(&max_header_pairs_v)) { + got_exception_ = true; + return -1; + } - if (!max_header_pairs_v->IsNumber()) { - return 0; + const double value = max_header_pairs_v->IsNumber() + ? max_header_pairs_v.As()->Value() + : 0; + max_header_pairs_ = value > 0 ? value : 0; } - const double max_header_pairs = max_header_pairs_v.As()->Value(); - if (max_header_pairs > 0 && header_pairs_ > max_header_pairs) { + if (max_header_pairs_ > 0 && header_pairs_ > max_header_pairs_) { llhttp_set_error_reason(&parser_, "HPE_HEADER_OVERFLOW:Header overflow"); return HPE_USER; } @@ -1107,6 +1112,7 @@ class Parser : public AsyncWrap, public StreamListener { const char* current_buffer_data_; bool headers_completed_ = false; size_t header_pairs_ = 0; + double max_header_pairs_ = -1; bool pending_pause_ = false; uint64_t header_nread_ = 0; uint64_t chunk_extensions_nread_ = 0; diff --git a/test/parallel/test-http-parser-max-header-pairs-cache.js b/test/parallel/test-http-parser-max-header-pairs-cache.js new file mode 100644 index 000000000000..dc8a60f36b0f --- /dev/null +++ b/test/parallel/test-http-parser-max-header-pairs-cache.js @@ -0,0 +1,77 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { HTTPParser } = require('_http_common'); + +const { REQUEST } = HTTPParser; +const kOnHeaders = HTTPParser.kOnHeaders | 0; +const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; +const kOnBody = HTTPParser.kOnBody | 0; +const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; + +function createParser() { + const parser = new HTTPParser(); + parser.initialize(REQUEST, {}); + parser[kOnHeaders] = () => {}; + parser[kOnHeadersComplete] = () => {}; + parser[kOnBody] = common.mustNotCall(); + parser[kOnMessageComplete] = () => {}; + return parser; +} + +// maxHeaderPairs is cached once for each independent header section. Main +// headers, trailers, the next message, and a reinitialized parser must each +// observe a fresh value. +{ + const parser = createParser(); + const limits = [2, 4, 2, 2]; + + Object.defineProperty(parser, 'maxHeaderPairs', { + configurable: true, + get: common.mustCall(() => limits.shift(), limits.length), + }); + + parser[kOnHeadersComplete] = common.mustCall(undefined, 3); + parser[kOnMessageComplete] = common.mustCall(undefined, 3); + + const pipelined = Buffer.from( + 'POST /first HTTP/1.1\r\n' + + 'Transfer-Encoding: chunked\r\n' + + '\r\n' + + '0\r\n' + + 'X-A: a\r\n' + + 'X-B: b\r\n' + + '\r\n' + + 'GET /second HTTP/1.1\r\n' + + 'X-C: c\r\n' + + '\r\n' + ); + assert.strictEqual(parser.execute(pipelined, 0, pipelined.length), pipelined.length); + + parser.initialize(REQUEST, {}); + const reused = Buffer.from('GET /reused HTTP/1.1\r\nX-D: d\r\n\r\n'); + assert.strictEqual(parser.execute(reused, 0, reused.length), reused.length); + assert.deepStrictEqual(limits, []); +} + +// Preserve the existing exception behavior for the first property lookup. +{ + const parser = createParser(); + const expected = new Error('maxHeaderPairs getter'); + Object.defineProperty(parser, 'maxHeaderPairs', { + get: common.mustCall(() => { throw expected; }), + }); + const request = Buffer.from('GET / HTTP/1.1\r\nX-A: a\r\n\r\n'); + assert.throws(() => parser.execute(request, 0, request.length), expected); +} + +// Non-positive and non-number values continue to mean unlimited. +for (const maxHeaderPairs of [undefined, null, NaN, 0, -1, new Number(2)]) { + const parser = createParser(); + parser.maxHeaderPairs = maxHeaderPairs; + const request = Buffer.from( + 'GET / HTTP/1.1\r\nX-A: a\r\nX-B: b\r\nX-C: c\r\n\r\n' + ); + assert.strictEqual(parser.execute(request, 0, request.length), request.length); +} From 02664983d0b4361726b13aa8e566594f4f9a7267 Mon Sep 17 00:00:00 2001 From: nashit hayat Date: Sat, 22 Aug 2026 23:14:15 +0530 Subject: [PATCH 53/97] src: fix out-of-bounds write when transcoding odd-length ucs2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nashit-h PR-URL: https://github.com/nodejs/node/pull/64512 Reviewed-By: James M Snell Reviewed-By: René --- src/node_i18n.cc | 7 +++++-- test/parallel/test-icu-transcode.js | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/node_i18n.cc b/src/node_i18n.cc index 3c4f419aa294..259e6eeda3e4 100644 --- a/src/node_i18n.cc +++ b/src/node_i18n.cc @@ -123,10 +123,13 @@ MaybeLocal ToBufferEndian(Environment* env, MaybeStackBuffer* buf) { void CopySourceBuffer(MaybeStackBuffer* dest, const char* data, - const size_t length, const size_t length_in_chars) { dest->AllocateSufficientStorage(length_in_chars); char* dst = reinterpret_cast(**dest); + // The destination holds length_in_chars UChar units. Copy that many whole + // units and ignore a trailing odd byte; copying the raw byte length would + // write one byte past the buffer when the source length is not even. + const size_t length = length_in_chars * sizeof(UChar); memcpy(dst, data, length); if constexpr (IsBigEndian()) { CHECK(nbytes::SwapBytes16(dst, length)); @@ -199,7 +202,7 @@ MaybeLocal TranscodeFromUcs2(Environment* env, to.set_subst_chars(sub.c_str()); const size_t length_in_chars = source_length / sizeof(UChar); - CopySourceBuffer(&sourcebuf, source, source_length, length_in_chars); + CopySourceBuffer(&sourcebuf, source, length_in_chars); MaybeStackBuffer destbuf(length_in_chars); const uint32_t len = ucnv_fromUChars(to.conv(), *destbuf, length_in_chars, *sourcebuf, length_in_chars, status); diff --git a/test/parallel/test-icu-transcode.js b/test/parallel/test-icu-transcode.js index e9aced128eec..87b45e8649ce 100644 --- a/test/parallel/test-icu-transcode.js +++ b/test/parallel/test-icu-transcode.js @@ -88,3 +88,18 @@ assert.deepStrictEqual( { buffer.transcode(new buffer.SlowBuffer(1), 'utf16le', 'ucs2'); } + +// An odd-length ucs2 source must only convert whole 2-byte code units and +// leave the trailing byte untouched, without reading or writing past the +// conversion buffer. Lengths are chosen to exercise both the on-stack and the +// heap-allocated code paths. +for (const len of [2049, 4099]) { + const src = Buffer.alloc(len, 0x61); + const wholeUnits = src.subarray(0, len - 1); + for (const to of ['latin1', 'ascii']) { + assert.deepStrictEqual( + buffer.transcode(src, 'utf16le', to), + buffer.transcode(wholeUnits, 'utf16le', to), + `ucs2->${to} odd length ${len}`); + } +} From 893e16099abd0485178260c9ca34613312cc0fbb Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 22 Aug 2026 10:44:25 -0700 Subject: [PATCH 54/97] fs: allocate FSReqPromise stat arrays lazily Every promise-based fs operation eagerly allocated two AliasedBuffers (a stats array and a statfs array) at request creation, although only stat-family resolutions ever read the first and only statfs() reads the second. Each allocation is an ArrayBuffer, a TypedArray and a strong v8::Global. The callback path has no equivalent cost since it resolves through a shared global array. Construct the arrays lazily in ResolveStat()/ResolveStatFs() instead. Once created the lifetime is unchanged, so deferred continuations still read from request-owned memory. Improves fs/promises throughput under concurrency: writeFile +53%, stat +26%, readFile +22% at 64 in-flight operations on tmpfs, with callback paths unchanged. Signed-off-by: Sam Attard PR-URL: https://github.com/nodejs/node/pull/63886 Reviewed-By: Anna Henningsen Reviewed-By: Edy Silva --- src/node_file-inl.h | 34 +++++++++++++++---------- src/node_file.h | 7 +++-- test/pummel/test-heapdump-fs-promise.js | 6 ++--- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/node_file-inl.h b/src/node_file-inl.h index e0fc86bedc74..ebd97c1c8c52 100644 --- a/src/node_file-inl.h +++ b/src/node_file-inl.h @@ -209,13 +209,7 @@ FSReqPromise::FSReqPromise(BindingData* binding_data, v8::Local obj, bool use_bigint) : FSReqBase( - binding_data, obj, AsyncWrap::PROVIDER_FSREQPROMISE, use_bigint), - stats_field_array_( - env()->isolate(), - static_cast(FsStatsOffset::kFsStatsFieldsNumber)), - statfs_field_array_( - env()->isolate(), - static_cast(FsStatFsOffset::kFsStatFsFieldsNumber)) {} + binding_data, obj, AsyncWrap::PROVIDER_FSREQPROMISE, use_bigint) {} template void FSReqPromise::Reject(v8::Local reject) { @@ -253,14 +247,24 @@ void FSReqPromise::Resolve(v8::Local value) { template void FSReqPromise::ResolveStat(const uv_stat_t* stat) { - FillStatsArray(&stats_field_array_, stat); - Resolve(stats_field_array_.GetJSArray()); + if (!stats_field_array_.has_value()) { + stats_field_array_.emplace( + env()->isolate(), + static_cast(FsStatsOffset::kFsStatsFieldsNumber)); + } + FillStatsArray(&stats_field_array_.value(), stat); + Resolve(stats_field_array_->GetJSArray()); } template void FSReqPromise::ResolveStatFs(const uv_statfs_t* stat) { - FillStatFsArray(&statfs_field_array_, stat); - Resolve(statfs_field_array_.GetJSArray()); + if (!statfs_field_array_.has_value()) { + statfs_field_array_.emplace( + env()->isolate(), + static_cast(FsStatFsOffset::kFsStatFsFieldsNumber)); + } + FillStatFsArray(&statfs_field_array_.value(), stat); + Resolve(statfs_field_array_->GetJSArray()); } template @@ -280,8 +284,12 @@ void FSReqPromise::SetReturnValue( template void FSReqPromise::MemoryInfo(MemoryTracker* tracker) const { FSReqBase::MemoryInfo(tracker); - tracker->TrackField("stats_field_array", stats_field_array_); - tracker->TrackField("statfs_field_array", statfs_field_array_); + if (stats_field_array_.has_value()) { + tracker->TrackField("stats_field_array", stats_field_array_.value()); + } + if (statfs_field_array_.has_value()) { + tracker->TrackField("statfs_field_array", statfs_field_array_.value()); + } } FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo& args, diff --git a/src/node_file.h b/src/node_file.h index 17f3b4203c8e..fab01a4c17b8 100644 --- a/src/node_file.h +++ b/src/node_file.h @@ -266,8 +266,11 @@ class FSReqPromise final : public FSReqBase { bool use_bigint); bool finished_ = false; - AliasedBufferT stats_field_array_; - AliasedBufferT statfs_field_array_; + // Constructed lazily in ResolveStat()/ResolveStatFs(): most operations + // never resolve with stats, and eagerly allocating the backing stores + // for every request is a significant per-request cost. + std::optional stats_field_array_; + std::optional statfs_field_array_; }; class FSReqAfterScope final { diff --git a/test/pummel/test-heapdump-fs-promise.js b/test/pummel/test-heapdump-fs-promise.js index 429359e1a6be..5b259ea2dd32 100644 --- a/test/pummel/test-heapdump-fs-promise.js +++ b/test/pummel/test-heapdump-fs-promise.js @@ -20,7 +20,7 @@ fs.stat(__filename); validateByRetainingPathFromNodes(nodes, 'Node / FSReqPromise', [ { node_name: 'FSReqPromise', edge_name: 'native_to_javascript' }, ]); - validateByRetainingPathFromNodes(nodes, 'Node / FSReqPromise', [ - { node_name: 'Node / AliasedFloat64Array', edge_name: 'stats_field_array' }, - ]); + // The stats field array is allocated lazily when the request resolves + // with stats, so it is not retained by a request that is still pending + // and cannot be observed in a heap snapshot. } From adabc085e65e0cd0caa207153ef8a90afa4f9a84 Mon Sep 17 00:00:00 2001 From: Jerry Zhao <165626830+shulaoda@users.noreply.github.com> Date: Sun, 23 Aug 2026 01:44:37 +0800 Subject: [PATCH 55/97] fs: pass symlink type in cp when filter is provided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `fs.cp`/`fs.cpSync` is called with both `verbatimSymlinks: true` and a `filter` function, directory symlinks were incorrectly created as file symlinks on Windows. Without a `filter`, cp takes the C++ fast path (`cpSyncCopyDir`) which uses `std::filesystem::copy_symlink()` and preserves the symlink type automatically. With a `filter`, the JS fallback calls `symlinkSync`/ `symlink` without a `type` argument. On Windows, that causes the type to be auto-detected by stat-ing the resolved target at the destination, but during a recursive copy the target directory may not exist yet at the destination (e.g. `linked/` is copied before `packages/` in alphabetical order). The stat fails and `type` falls back to `'file'`, producing a file symlink in place of a directory symlink. Detect the symlink type from the source (which always exists) via `internalModuleStat(src)` and pass it explicitly to the `symlinkSync`/ `symlink` call sites. `onLink` already computed `srcIsDir` for subdirectory validation; hoist that computation above the early-return paths and thread the derived `symlinkType` through `copyLink` as well. Both the sync (`cp-sync.js`) and async (`cp.js`) implementations are fixed. Add two regression tests that copy a tree containing a relative directory symlink with `verbatimSymlinks: true` and a `filter` function, then assert the destination link still resolves as a directory. Fixes: https://github.com/nodejs/node/issues/62653 Signed-off-by: shulaoda <165626830+shulaoda@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/62654 Reviewed-By: Aviv Keller Reviewed-By: Juan José Arboleda Reviewed-By: Stefan Stojanovic Reviewed-By: Matteo Collina --- lib/internal/fs/cp/cp-sync.js | 13 +++--- lib/internal/fs/cp/cp.js | 14 +++--- ...sync-verbatim-dir-symlinks-with-filter.mjs | 46 +++++++++++++++++++ ...sync-verbatim-dir-symlinks-with-filter.mjs | 46 +++++++++++++++++++ 4 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 test/parallel/test-fs-cp-async-verbatim-dir-symlinks-with-filter.mjs create mode 100644 test/parallel/test-fs-cp-sync-verbatim-dir-symlinks-with-filter.mjs diff --git a/lib/internal/fs/cp/cp-sync.js b/lib/internal/fs/cp/cp-sync.js index 03fcae9b7cdb..f2b00f3f82bb 100644 --- a/lib/internal/fs/cp/cp-sync.js +++ b/lib/internal/fs/cp/cp-sync.js @@ -191,8 +191,10 @@ function onLink(destStat, src, dest, verbatimSymlinks) { if (!verbatimSymlinks && !isAbsolute(resolvedSrc)) { resolvedSrc = resolve(dirname(src), resolvedSrc); } + const srcIsDir = fsBinding.internalModuleStat(src) === 1; + const symlinkType = srcIsDir ? 'dir' : 'file'; if (!destStat) { - return symlinkSync(resolvedSrc, dest); + return symlinkSync(resolvedSrc, dest, symlinkType); } let resolvedDest; try { @@ -202,14 +204,13 @@ function onLink(destStat, src, dest, verbatimSymlinks) { // Windows may throw UNKNOWN error. If dest already exists, // fs throws error anyway, so no need to guard against it here. if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { - return symlinkSync(resolvedSrc, dest); + return symlinkSync(resolvedSrc, dest, symlinkType); } throw err; } if (!isAbsolute(resolvedDest)) { resolvedDest = resolve(dirname(dest), resolvedDest); } - const srcIsDir = fsBinding.internalModuleStat(src) === 1; if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) { throw new ERR_FS_CP_EINVAL({ @@ -233,12 +234,12 @@ function onLink(destStat, src, dest, verbatimSymlinks) { code: 'EINVAL', }); } - return copyLink(resolvedSrc, dest); + return copyLink(resolvedSrc, dest, symlinkType); } -function copyLink(resolvedSrc, dest) { +function copyLink(resolvedSrc, dest, symlinkType) { unlinkSync(dest); - return symlinkSync(resolvedSrc, dest); + return symlinkSync(resolvedSrc, dest, symlinkType); } module.exports = { cpSyncFn }; diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js index 10c52b114634..2eb4f0ffdd83 100644 --- a/lib/internal/fs/cp/cp.js +++ b/lib/internal/fs/cp/cp.js @@ -336,8 +336,10 @@ async function onLink(destStat, src, dest, opts) { if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) { resolvedSrc = resolve(dirname(src), resolvedSrc); } + const srcIsDir = fsBinding.internalModuleStat(src) === 1; + const symlinkType = srcIsDir ? 'dir' : 'file'; if (!destStat) { - return symlink(resolvedSrc, dest); + return symlink(resolvedSrc, dest, symlinkType); } let resolvedDest; try { @@ -347,7 +349,7 @@ async function onLink(destStat, src, dest, opts) { // Windows may throw UNKNOWN error. If dest already exists, // fs throws error anyway, so no need to guard against it here. if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { - return symlink(resolvedSrc, dest); + return symlink(resolvedSrc, dest, symlinkType); } throw err; } @@ -355,8 +357,6 @@ async function onLink(destStat, src, dest, opts) { resolvedDest = resolve(dirname(dest), resolvedDest); } - const srcIsDir = fsBinding.internalModuleStat(src) === 1; - if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) { throw new ERR_FS_CP_EINVAL({ message: `cannot copy ${resolvedSrc} to a subdirectory of self ` + @@ -380,12 +380,12 @@ async function onLink(destStat, src, dest, opts) { code: 'EINVAL', }); } - return copyLink(resolvedSrc, dest); + return copyLink(resolvedSrc, dest, symlinkType); } -async function copyLink(resolvedSrc, dest) { +async function copyLink(resolvedSrc, dest, symlinkType) { await unlink(dest); - return symlink(resolvedSrc, dest); + return symlink(resolvedSrc, dest, symlinkType); } module.exports = { diff --git a/test/parallel/test-fs-cp-async-verbatim-dir-symlinks-with-filter.mjs b/test/parallel/test-fs-cp-async-verbatim-dir-symlinks-with-filter.mjs new file mode 100644 index 000000000000..8e773bc6065c --- /dev/null +++ b/test/parallel/test-fs-cp-async-verbatim-dir-symlinks-with-filter.mjs @@ -0,0 +1,46 @@ +// This tests that cp with verbatimSymlinks and filter preserves +// the directory symlink type on Windows (does not create a file symlink). +import { mustNotMutateObjectDeep, isWindows } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { + mkdirSync, + writeFileSync, + symlinkSync, + readlinkSync, + readdirSync, + statSync, +} from 'node:fs'; +import { cp } from 'node:fs/promises'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +// Setup source with a relative directory symlink +const src = nextdir(); +mkdirSync(join(src, 'packages', 'my-lib'), mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'packages', 'my-lib', 'index.js'), 'module.exports = "hello"'); +mkdirSync(join(src, 'linked'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(join('..', 'packages', 'my-lib'), join(src, 'linked', 'my-lib'), 'dir'); + +// Copy with verbatimSymlinks: true AND a filter function +const dest = nextdir(); +await cp(src, dest, mustNotMutateObjectDeep({ + recursive: true, + verbatimSymlinks: true, + filter: () => true, +})); + +// Verify the symlink target is preserved verbatim +const link = readlinkSync(join(dest, 'linked', 'my-lib')); +if (isWindows) { + assert.strictEqual(link.toLowerCase(), join('..', 'packages', 'my-lib').toLowerCase()); +} else { + assert.strictEqual(link, join('..', 'packages', 'my-lib')); +} + +// Verify the symlink works as a directory (not a file symlink) +const destSymlink = join(dest, 'linked', 'my-lib'); +assert.ok(statSync(destSymlink).isDirectory(), 'symlink target should be accessible as a directory'); +assert.deepStrictEqual(readdirSync(destSymlink), ['index.js']); diff --git a/test/parallel/test-fs-cp-sync-verbatim-dir-symlinks-with-filter.mjs b/test/parallel/test-fs-cp-sync-verbatim-dir-symlinks-with-filter.mjs new file mode 100644 index 000000000000..7aa0d90ff2a4 --- /dev/null +++ b/test/parallel/test-fs-cp-sync-verbatim-dir-symlinks-with-filter.mjs @@ -0,0 +1,46 @@ +// This tests that cpSync with verbatimSymlinks and filter preserves +// the directory symlink type on Windows (does not create a file symlink). +import { mustNotMutateObjectDeep, isWindows } from '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { + cpSync, + mkdirSync, + writeFileSync, + symlinkSync, + readlinkSync, + readdirSync, + statSync, +} from 'node:fs'; +import { join } from 'node:path'; + +import tmpdir from '../common/tmpdir.js'; +tmpdir.refresh(); + +// Setup source with a relative directory symlink +const src = nextdir(); +mkdirSync(join(src, 'packages', 'my-lib'), mustNotMutateObjectDeep({ recursive: true })); +writeFileSync(join(src, 'packages', 'my-lib', 'index.js'), 'module.exports = "hello"'); +mkdirSync(join(src, 'linked'), mustNotMutateObjectDeep({ recursive: true })); +symlinkSync(join('..', 'packages', 'my-lib'), join(src, 'linked', 'my-lib'), 'dir'); + +// Copy with verbatimSymlinks: true AND a filter function +const dest = nextdir(); +cpSync(src, dest, mustNotMutateObjectDeep({ + recursive: true, + verbatimSymlinks: true, + filter: () => true, +})); + +// Verify the symlink target is preserved verbatim +const link = readlinkSync(join(dest, 'linked', 'my-lib')); +if (isWindows) { + assert.strictEqual(link.toLowerCase(), join('..', 'packages', 'my-lib').toLowerCase()); +} else { + assert.strictEqual(link, join('..', 'packages', 'my-lib')); +} + +// Verify the symlink works as a directory (not a file symlink) +const destSymlink = join(dest, 'linked', 'my-lib'); +assert.ok(statSync(destSymlink).isDirectory(), 'symlink target should be accessible as a directory'); +assert.deepStrictEqual(readdirSync(destSymlink), ['index.js']); From c3834495afe6b3b64b1e1f8ee46b08d8f98a2851 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Sat, 22 Aug 2026 14:48:28 -0400 Subject: [PATCH 56/97] url: speed up URLSearchParams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse query strings with indexOf instead of a per-character state machine, skip ToString when values are already strings, cache toString() until the list mutates, and join serialized pairs. Signed-off-by: Yagiz Nizipli Assisted-by: Cursor PR-URL: https://github.com/nodejs/node/pull/65363 Reviewed-By: Matteo Collina Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Aviv Keller --- lib/internal/url.js | 258 +++++++++--------- .../test-whatwg-url-searchparams-fast-path.js | 116 ++++++++ 2 files changed, 241 insertions(+), 133 deletions(-) create mode 100644 test/parallel/test-whatwg-url-searchparams-fast-path.js diff --git a/lib/internal/url.js b/lib/internal/url.js index 850ea473e5e0..91316b5701b3 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -74,14 +74,10 @@ const { }, } = require('internal/errors'); const { - CHAR_AMPERSAND, CHAR_BACKWARD_SLASH, - CHAR_EQUAL, CHAR_FORWARD_SLASH, CHAR_LOWERCASE_A, CHAR_LOWERCASE_Z, - CHAR_PERCENT, - CHAR_PLUS, CHAR_COLON, } = require('internal/constants'); const path = require('path'); @@ -331,12 +327,17 @@ class URLSearchParams { // "associated url object" #context; + // Cached application/x-www-form-urlencoded serialization. Cleared on + // mutation so repeated toString()/URL.href reads stay cheap. + #serialized; + static { setURLSearchParamsContext = (obj, ctx) => { obj.#context = ctx; }; getURLSearchParamsList = (obj) => obj.#searchParams; setURLSearchParams = (obj, query) => { + obj.#serialized = undefined; if (query === undefined) { obj.#searchParams = []; } else { @@ -345,6 +346,13 @@ class URLSearchParams { }; } + #markUpdated() { + this.#serialized = undefined; + if (this.#context) { + setURLSearchParamsModified(this.#context); + } + } + // URL Standard says the default value is '', but as undefined and '' have // the same result, undefined is used to prevent unnecessary parsing. // Default parameter is necessary to keep URLSearchParams.length === 0 in @@ -361,6 +369,7 @@ class URLSearchParams { // shortcut to avoid having to go through the costly generic iterator. const childParams = init.#searchParams; this.#searchParams = childParams.slice(); + this.#serialized = init.#serialized; } else if (method != null) { // Sequence> if (typeof method !== 'function') { @@ -388,8 +397,8 @@ class URLSearchParams { // Append (innerSequence[0], innerSequence[1]) to query's list. ArrayPrototypePush( this.#searchParams, - StringPrototypeToWellFormed(`${pair[0]}`), - StringPrototypeToWellFormed(`${pair[1]}`), + toUSVString(pair[0]), + toUSVString(pair[1]), ); } else { if (((typeof pair !== 'object' && typeof pair !== 'function') || @@ -401,7 +410,7 @@ class URLSearchParams { for (const element of pair) { length++; - ArrayPrototypePush(this.#searchParams, StringPrototypeToWellFormed(`${element}`)); + ArrayPrototypePush(this.#searchParams, toUSVString(element)); } // If innerSequence's size is not 2, then throw a TypeError. @@ -419,8 +428,8 @@ class URLSearchParams { const key = keys[i]; const desc = ReflectGetOwnPropertyDescriptor(init, key); if (desc !== undefined && desc.enumerable) { - const typedKey = StringPrototypeToWellFormed(key); - const typedValue = StringPrototypeToWellFormed(`${init[key]}`); + const typedKey = toUSVString(key); + const typedValue = toUSVString(init[key]); // Two different keys may become the same USVString after normalization. // In that case, we retain the later one. Refer to WPT. @@ -437,7 +446,7 @@ class URLSearchParams { } } else { // https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams - init = StringPrototypeToWellFormed(`${init}`); + init = toUSVString(init); this.#searchParams = init ? parseParams(init) : []; } } @@ -491,13 +500,10 @@ class URLSearchParams { throw new ERR_MISSING_ARGS('name', 'value'); } - name = StringPrototypeToWellFormed(`${name}`); - value = StringPrototypeToWellFormed(`${value}`); + name = toUSVString(name); + value = toUSVString(value); ArrayPrototypePush(this.#searchParams, name, value); - - if (this.#context) { - setURLSearchParamsModified(this.#context); - } + this.#markUpdated(); } delete(name, value = undefined) { @@ -509,12 +515,12 @@ class URLSearchParams { } const list = this.#searchParams; - name = StringPrototypeToWellFormed(`${name}`); + name = toUSVString(name); const { length } = list; let write = 0; if (value !== undefined) { - value = StringPrototypeToWellFormed(`${value}`); + value = toUSVString(value); for (let i = 0; i < length; i += 2) { if (list[i] === name && list[i + 1] === value) { continue; @@ -538,8 +544,10 @@ class URLSearchParams { } } - if (write !== length) + if (write !== length) { list.length = write; + this.#serialized = undefined; + } if (this.#context) { setURLSearchParamsModified(this.#context); @@ -555,8 +563,9 @@ class URLSearchParams { } const list = this.#searchParams; - name = StringPrototypeToWellFormed(`${name}`); - for (let i = 0; i < list.length; i += 2) { + name = toUSVString(name); + const { length } = list; + for (let i = 0; i < length; i += 2) { if (list[i] === name) { return list[i + 1]; } @@ -574,10 +583,11 @@ class URLSearchParams { const list = this.#searchParams; const values = []; - name = StringPrototypeToWellFormed(`${name}`); - for (let i = 0; i < list.length; i += 2) { + name = toUSVString(name); + const { length } = list; + for (let i = 0; i < length; i += 2) { if (list[i] === name) { - values.push(list[i + 1]); + ArrayPrototypePush(values, list[i + 1]); } } return values; @@ -592,13 +602,14 @@ class URLSearchParams { } const list = this.#searchParams; - name = StringPrototypeToWellFormed(`${name}`); + name = toUSVString(name); if (value !== undefined) { - value = StringPrototypeToWellFormed(`${value}`); + value = toUSVString(value); } - for (let i = 0; i < list.length; i += 2) { + const { length } = list; + for (let i = 0; i < length; i += 2) { if (list[i] === name) { if (value === undefined || list[i + 1] === value) { return true; @@ -618,8 +629,8 @@ class URLSearchParams { } const list = this.#searchParams; - name = StringPrototypeToWellFormed(`${name}`); - value = StringPrototypeToWellFormed(`${value}`); + name = toUSVString(name); + value = toUSVString(value); const { length } = list; // If there are any name-value pairs whose name is `name`, in `list`, set @@ -656,9 +667,7 @@ class URLSearchParams { ArrayPrototypePush(list, name, value); } - if (this.#context) { - setURLSearchParamsModified(this.#context); - } + this.#markUpdated(); } sort() { @@ -705,9 +714,7 @@ class URLSearchParams { } } - if (this.#context) { - setURLSearchParamsModified(this.#context); - } + this.#markUpdated(); } // https://heycam.github.io/webidl/#es-iterators @@ -760,7 +767,12 @@ class URLSearchParams { if (typeof this !== 'object' || this === null || !(#searchParams in this)) throw new ERR_INVALID_THIS('URLSearchParams'); - return serializeParams(this.#searchParams); + if (this.#serialized !== undefined) { + return this.#serialized; + } + const serialized = serializeParams(this.#searchParams); + this.#serialized = serialized; + return serialized; } } @@ -1276,102 +1288,82 @@ function installObjectURLMethods() { }); } +function toUSVString(value) { + return typeof value === 'string' ? + StringPrototypeToWellFormed(value) : + StringPrototypeToWellFormed(`${value}`); +} + +function unescapeFormComponent(s) { + try { + return decodeURIComponent(s); + } catch { + return querystring.unescapeBuffer(s).toString(); + } +} + +function hasPercentHex(s) { + const end = s.length - 2; + for (let i = 0; i < end; i++) { + if (StringPrototypeCharCodeAt(s, i) === 37 && // '%' + isHexTable[StringPrototypeCharCodeAt(s, i + 1)] === 1 && + isHexTable[StringPrototypeCharCodeAt(s, i + 2)] === 1) { + return true; + } + } + return false; +} + +function decodeFormComponent(qs, start, end) { + if (start >= end) { + return ''; + } + const s = qs.slice(start, end); + const plus = s.indexOf('+'); + const pct = s.indexOf('%'); + if (plus === -1 && pct === -1) { + return s; + } + const replaced = plus === -1 ? s : s.replaceAll('+', ' '); + // Only percent-decode when a complete %HH sequence exists. A lone '%' or + // a '%' followed by a non-hex character must be left intact so later + // serialization can encode the raw bytes. + if (pct === -1 || !hasPercentHex(replaced)) { + return replaced; + } + return unescapeFormComponent(replaced); +} + // application/x-www-form-urlencoded parser // Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser function parseParams(qs) { - const out = []; - let seenSep = false; - let buf = ''; - let encoded = false; - let encodeCheck = 0; + const len = qs.length; let i = qs[0] === '?' ? 1 : 0; - let pairStart = i; - let lastPos = i; - for (; i < qs.length; ++i) { - const code = StringPrototypeCharCodeAt(qs, i); - - // Try matching key/value pair separator - if (code === CHAR_AMPERSAND) { - if (pairStart === i) { - // We saw an empty substring between pair separators - lastPos = pairStart = i + 1; - continue; - } + if (i >= len) { + return []; + } - if (lastPos < i) - buf += qs.slice(lastPos, i); - if (encoded) - buf = querystring.unescape(buf); - out.push(buf); - - // If `buf` is the key, add an empty value. - if (!seenSep) - out.push(''); - - seenSep = false; - buf = ''; - encoded = false; - encodeCheck = 0; - lastPos = pairStart = i + 1; - continue; - } - - // Try matching key/value separator (e.g. '=') if we haven't already - if (!seenSep && code === CHAR_EQUAL) { - // Key/value separator match! - if (lastPos < i) - buf += qs.slice(lastPos, i); - if (encoded) - buf = querystring.unescape(buf); - out.push(buf); - - seenSep = true; - buf = ''; - encoded = false; - encodeCheck = 0; - lastPos = i + 1; - continue; - } - - // Handle + and percent decoding. - if (code === CHAR_PLUS) { - if (lastPos < i) - buf += StringPrototypeSlice(qs, lastPos, i); - buf += ' '; - lastPos = i + 1; - } else if (!encoded) { - // Try to match an (valid) encoded byte (once) to minimize unnecessary - // calls to string decoding functions - if (code === CHAR_PERCENT) { - encodeCheck = 1; - } else if (encodeCheck > 0) { - if (isHexTable[code] === 1) { - if (++encodeCheck === 3) { - encoded = true; - } - } else { - encodeCheck = 0; - } + const out = []; + // Native indexOf/slice/push outperform primordials on this tight loop. + const encoded = qs.indexOf('+', i) !== -1 || qs.indexOf('%', i) !== -1; + while (i < len) { + let amp = qs.indexOf('&', i); + if (amp === -1) { + amp = len; + } + if (amp !== i) { + const eq = qs.indexOf('=', i); + if (eq === -1 || eq > amp) { + out.push(encoded ? decodeFormComponent(qs, i, amp) : qs.slice(i, amp), ''); + } else { + out.push( + encoded ? decodeFormComponent(qs, i, eq) : qs.slice(i, eq), + encoded ? decodeFormComponent(qs, eq + 1, amp) : qs.slice(eq + 1, amp), + ); } } + i = amp + 1; } - - // Deal with any leftover key or value data - - // There is a trailing &. No more processing is needed. - if (pairStart === i) - return out; - - if (lastPos < i) - buf += StringPrototypeSlice(qs, lastPos, i); - if (encoded) - buf = querystring.unescape(buf); - ArrayPrototypePush(out, buf); - - // If `buf` is the key, add an empty value. - if (!seenSep) - ArrayPrototypePush(out, ''); - return out; } @@ -1402,17 +1394,17 @@ function serializeParams(array) { if (len === 0) return ''; - const firstEncodedParam = encodeStr(array[0], noEscape, paramHexTable); - const firstEncodedValue = encodeStr(array[1], noEscape, paramHexTable); - let output = `${firstEncodedParam}=${firstEncodedValue}`; - - for (let i = 2; i < len; i += 2) { - const encodedParam = encodeStr(array[i], noEscape, paramHexTable); - const encodedValue = encodeStr(array[i + 1], noEscape, paramHexTable); - output += `&${encodedParam}=${encodedValue}`; + if (len === 2) { + return encodeStr(array[0], noEscape, paramHexTable) + '=' + + encodeStr(array[1], noEscape, paramHexTable); } - return output; + const pairs = new Array(len / 2); + for (let i = 0, j = 0; i < len; i += 2, ++j) { + pairs[j] = encodeStr(array[i], noEscape, paramHexTable) + '=' + + encodeStr(array[i + 1], noEscape, paramHexTable); + } + return ArrayPrototypeJoin(pairs, '&'); } // for merge sort diff --git a/test/parallel/test-whatwg-url-searchparams-fast-path.js b/test/parallel/test-whatwg-url-searchparams-fast-path.js new file mode 100644 index 000000000000..2dbb38f88aa5 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-fast-path.js @@ -0,0 +1,116 @@ +'use strict'; + +// Tests for the URLSearchParams parse / serialize / toUSVString fast paths. + +require('../common'); +const assert = require('assert'); + +{ + const params = new URLSearchParams('?a=b'); + assert.strictEqual(params.toString(), 'a=b'); + assert.strictEqual(params.get('a'), 'b'); +} + +{ + const params = new URLSearchParams('a=b&c'); + assert.deepStrictEqual([...params], [['a', 'b'], ['c', '']]); +} + +{ + const params = new URLSearchParams('&a&&& &&&&&a+b=& c&m%c3%b8%c3%b8'); + assert.ok(params.has('a')); + assert.ok(params.has('a b')); + assert.ok(params.has(' ')); + assert.ok(params.has(' c')); + assert.ok(params.has('møø')); + assert.strictEqual(params.get('a+b'), null); +} + +{ + const params = new URLSearchParams('id=0&value=%'); + assert.strictEqual(params.get('id'), '0'); + assert.strictEqual(params.get('value'), '%'); +} + +{ + const params = new URLSearchParams('b=%2sf%2a'); + assert.strictEqual(params.get('b'), '%2sf*'); +} + +{ + const params = new URLSearchParams('a=b=c&d='); + assert.strictEqual(params.get('a'), 'b=c'); + assert.strictEqual(params.get('d'), ''); +} + +{ + const params = new URLSearchParams('foo=bar&baz=quux'); + assert.strictEqual(params.toString(), 'foo=bar&baz=quux'); + assert.strictEqual(params.toString(), 'foo=bar&baz=quux'); + params.append('xyzzy', 'thud'); + assert.strictEqual(params.toString(), 'foo=bar&baz=quux&xyzzy=thud'); + params.set('baz', 'updated'); + assert.strictEqual(params.toString(), 'foo=bar&baz=updated&xyzzy=thud'); + params.delete('foo'); + assert.strictEqual(params.toString(), 'baz=updated&xyzzy=thud'); + params.sort(); + assert.strictEqual(params.toString(), 'baz=updated&xyzzy=thud'); +} + +{ + const original = new URLSearchParams('a=1&b=2'); + assert.strictEqual(original.toString(), 'a=1&b=2'); + const copy = new URLSearchParams(original); + assert.strictEqual(copy.toString(), 'a=1&b=2'); + original.append('c', '3'); + assert.strictEqual(original.toString(), 'a=1&b=2&c=3'); + assert.strictEqual(copy.toString(), 'a=1&b=2'); +} + +{ + const params = new URLSearchParams({ foo: 'bar', baz: 1, xyzzy: false }); + assert.strictEqual(params.get('foo'), 'bar'); + assert.strictEqual(params.get('baz'), '1'); + assert.strictEqual(params.get('xyzzy'), 'false'); + assert.strictEqual(params.toString(), 'foo=bar&baz=1&xyzzy=false'); +} + +{ + const params = new URLSearchParams([['foo', 'bar'], ['baz', 'quux']]); + assert.strictEqual(params.toString(), 'foo=bar&baz=quux'); + assert.ok(params.has('foo', 'bar')); + assert.deepStrictEqual(params.getAll('foo'), ['bar']); +} + +{ + const params = new URLSearchParams('\uD83D'); + assert.strictEqual(params.keys().next().value, '\uFFFD'); + assert.strictEqual(params.toString(), '%EF%BF%BD='); +} + +{ + const params = new URLSearchParams('a=b+c&d=%20'); + assert.strictEqual(params.get('a'), 'b c'); + assert.strictEqual(params.get('d'), ' '); + assert.strictEqual(params.toString(), 'a=b+c&d=+'); +} + +{ + // Fake percent-encoding must not be UTF-8-decoded into U+FFFD. + const params = new URLSearchParams('foo=%©ar&baz=%A©uux&xyzzy=%©ud'); + assert.deepStrictEqual([...params], [ + ['foo', '%©ar'], + ['baz', '%A©uux'], + ['xyzzy', '%©ud'], + ]); + assert.strictEqual(params.toString(), 'foo=%25%C2%A9ar&baz=%25A%C2%A9uux&xyzzy=%25%C2%A9ud'); +} + +{ + const url = new URL('https://example.org/?foo=bar'); + const params = url.searchParams; + assert.strictEqual(params.toString(), 'foo=bar'); + params.append('baz', 'quux'); + assert.strictEqual(url.search, '?foo=bar&baz=quux'); + assert.strictEqual(params.toString(), 'foo=bar&baz=quux'); +} From 26329295d1898f6a75f984d78c2d5be4f46851ca Mon Sep 17 00:00:00 2001 From: mag123c Date: Sun, 18 Jan 2026 14:48:44 +0900 Subject: [PATCH 57/97] test_runner: print coverage and diagnostic info with dot reporter When using the dot reporter with coverage enabled, coverage threshold failures and coverage reports were not printed, only an exit code was returned. This made it impossible to know why the test run failed. This change adds handling for test:diagnostic and test:coverage events to the dot reporter, matching the behavior of the spec reporter. Fixes: https://github.com/nodejs/node/issues/60884 Signed-off-by: mag123c PR-URL: https://github.com/nodejs/node/pull/61423 Reviewed-By: Chemi Atlow Reviewed-By: Xuguang Mei Reviewed-By: Aviv Keller --- lib/internal/test_runner/reporter/dot.js | 25 ++++++++++++++++++- .../output/dot_reporter_coverage_threshold.js | 17 +++++++++++++ .../dot_reporter_coverage_threshold.snapshot | 18 +++++++++++++ .../test-runner-coverage-thresholds.js | 21 ++++++++++++++++ ...output-dot-reporter-coverage-threshold.mjs | 15 +++++++++++ 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/test-runner/output/dot_reporter_coverage_threshold.js create mode 100644 test/fixtures/test-runner/output/dot_reporter_coverage_threshold.snapshot create mode 100644 test/test-runner/test-output-dot-reporter-coverage-threshold.mjs diff --git a/lib/internal/test_runner/reporter/dot.js b/lib/internal/test_runner/reporter/dot.js index 45ff047bc4e5..1d73523f3c3e 100644 --- a/lib/internal/test_runner/reporter/dot.js +++ b/lib/internal/test_runner/reporter/dot.js @@ -4,12 +4,19 @@ const { MathMax, } = primordials; const colors = require('internal/util/colors'); -const { formatTestReport } = require('internal/test_runner/reporter/utils'); +const { getCoverageReport } = require('internal/test_runner/utils'); +const { + formatTestReport, + reporterColorMap, + reporterUnicodeSymbolMap, +} = require('internal/test_runner/reporter/utils'); module.exports = async function* dot(source) { let count = 0; let columns = getLineLength(); const failedTests = []; + const diagnostics = []; + let coverage; for await (const { type, data } of source) { if (type === 'test:pass') { yield `${colors.green}.${colors.reset}`; @@ -25,8 +32,24 @@ module.exports = async function* dot(source) { columns = getLineLength(); count = 0; } + if (type === 'test:diagnostic' && data.level === 'error') { + ArrayPrototypePush(diagnostics, data); + } + if (type === 'test:coverage') { + coverage = data; + } } yield '\n'; + if (diagnostics.length > 0) { + for (const diagnostic of diagnostics) { + const color = reporterColorMap[diagnostic.level] || reporterColorMap['test:diagnostic']; + yield `${color}${reporterUnicodeSymbolMap['test:diagnostic']}${diagnostic.message}${colors.white}\n`; + } + if (coverage) { + yield getCoverageReport('', coverage.summary, + reporterUnicodeSymbolMap['test:coverage'], colors.blue, true); + } + } if (failedTests.length > 0) { yield `\n${colors.red}Failed tests:${colors.white}\n\n`; for (const test of failedTests) { diff --git a/test/fixtures/test-runner/output/dot_reporter_coverage_threshold.js b/test/fixtures/test-runner/output/dot_reporter_coverage_threshold.js new file mode 100644 index 000000000000..b49dd1488151 --- /dev/null +++ b/test/fixtures/test-runner/output/dot_reporter_coverage_threshold.js @@ -0,0 +1,17 @@ +'use strict'; +require('../../../common'); +const fixtures = require('../../../common/fixtures'); +const spawn = require('node:child_process').spawn; + +spawn( + process.execPath, + [ + '--no-warnings', + '--experimental-test-coverage', + '--test-coverage-exclude=!test/**', + '--test-coverage-lines=99', + '--test-reporter', 'dot', + fixtures.path('test-runner/coverage.js'), + ], + { stdio: 'inherit' }, +); diff --git a/test/fixtures/test-runner/output/dot_reporter_coverage_threshold.snapshot b/test/fixtures/test-runner/output/dot_reporter_coverage_threshold.snapshot new file mode 100644 index 000000000000..0645329f1a84 --- /dev/null +++ b/test/fixtures/test-runner/output/dot_reporter_coverage_threshold.snapshot @@ -0,0 +1,18 @@ +invalid tap output +. +ℹ Error: 78.35% line coverage does not meet threshold of 99%. +ℹ start of coverage report +ℹ -------------------------------------------------------------------------------------------- +ℹ file | line % | branch % | funcs % | uncovered lines +ℹ -------------------------------------------------------------------------------------------- +ℹ test | | | | +ℹ fixtures | | | | +ℹ test-runner | | | | +ℹ coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72 +ℹ invalid-tap.js | 100.00 | 100.00 | 100.00 | +ℹ v8-coverage | | | | +ℹ throw.js | 71.43 | 50.00 | 100.00 | 5-6 +ℹ -------------------------------------------------------------------------------------------- +ℹ all files | 78.35 | 43.75 | 60.00 | +ℹ -------------------------------------------------------------------------------------------- +ℹ end of coverage report diff --git a/test/parallel/test-runner-coverage-thresholds.js b/test/parallel/test-runner-coverage-thresholds.js index e45e1191299c..2742464adf64 100644 --- a/test/parallel/test-runner-coverage-thresholds.js +++ b/test/parallel/test-runner-coverage-thresholds.js @@ -170,4 +170,25 @@ for (const coverage of coverages) { assert.strictEqual(result.status, 1); assert(!findCoverageFileForPid(result.pid)); }); + + test(`test failing ${coverage.flag} with dot reporter`, () => { + const result = spawnSync(process.execPath, [ + '--test', + '--experimental-test-coverage', + '--test-coverage-exclude=!test/**', + `${coverage.flag}=99`, + '--test-reporter', 'dot', + fixture, + ]); + + const stdout = result.stdout.toString(); + assert.match( + stdout, + RegExp(`Error: ${coverage.actual.toFixed(2)}% ${coverage.name} coverage does not meet threshold of 99%`) + ); + assert.match(stdout, /start of coverage report/); + assert.match(stdout, /end of coverage report/); + assert.strictEqual(result.status, 1); + assert(!findCoverageFileForPid(result.pid)); + }); } diff --git a/test/test-runner/test-output-dot-reporter-coverage-threshold.mjs b/test/test-runner/test-output-dot-reporter-coverage-threshold.mjs new file mode 100644 index 000000000000..e764863a6b0b --- /dev/null +++ b/test/test-runner/test-output-dot-reporter-coverage-threshold.mjs @@ -0,0 +1,15 @@ +// Test that the output of test-runner/output/dot_reporter_coverage_threshold.js matches +// test-runner/output/dot_reporter_coverage_threshold.snapshot +import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawnAndAssert, specTransform, ensureCwdIsProjectRoot } from '../common/assertSnapshot.js'; + +if (!process.features.inspector) { + common.skip('inspector support required'); +} + +ensureCwdIsProjectRoot(); +await spawnAndAssert( + fixtures.path('test-runner/output/dot_reporter_coverage_threshold.js'), + specTransform, +); From e3b658256ab82f4be871a3d9731e0dab4d85b005 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Das <151006536+skdas20@users.noreply.github.com> Date: Sun, 23 Aug 2026 02:07:46 +0530 Subject: [PATCH 58/97] esm: avoid super-linear data URL MIME regex The MIME regex used for data: URLs could backtrack super-linearly on a malformed URL lacking a ',' separator. Make the optional parameter group anchored on ';' so it cannot overlap with the media-type group. Adds a benchmark (esm/get-data-protocol-format) parameterized over path length, so a backtracking regression shows up as an ops/sec cliff instead of a wall-clock assertion in a test, per review. Fixes: https://github.com/nodejs/node/issues/61904 Signed-off-by: skdas20 PR-URL: https://github.com/nodejs/node/pull/61951 Reviewed-By: Aviv Keller Reviewed-By: Trivikram Kamat --- benchmark/esm/get-data-protocol-format.js | 30 +++++++++++++++++++++++ lib/internal/modules/esm/get_format.js | 2 +- lib/internal/modules/esm/load.js | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 benchmark/esm/get-data-protocol-format.js diff --git a/benchmark/esm/get-data-protocol-format.js b/benchmark/esm/get-data-protocol-format.js new file mode 100644 index 000000000000..35e54a770035 --- /dev/null +++ b/benchmark/esm/get-data-protocol-format.js @@ -0,0 +1,30 @@ +// Benchmarks defaultGetFormat() on `data:` URLs. The MIME-matching regex used +// to be susceptible to catastrophic backtracking on malformed input lacking a +// `,` separator (https://github.com/nodejs/node/issues/61904); `pathLength` +// scales the malformed path so a regression shows up as a sharp drop in ops/sec +// rather than a hang. +'use strict'; + +const common = require('../common.js'); + +const configs = { + n: [1e4], + pathLength: [1e2, 1e3, 1e4], +}; + +const options = { + flags: ['--expose-internals'], +}; + +const bench = common.createBenchmark(main, configs, options); + +function main({ n, pathLength }) { + const { defaultGetFormat } = require('internal/modules/esm/get_format'); + const url = new URL(`data:a/${'a'.repeat(pathLength)}B`); + + bench.start(); + for (let i = 0; i < n; i++) { + defaultGetFormat(url, { parentURL: undefined }); + } + bench.end(n); +} diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 7bc69b3eb5c3..b486337fac83 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -104,7 +104,7 @@ function detectModuleFormat(source, url) { */ function getDataProtocolModuleFormat(parsed) { const { 1: mime } = RegExpPrototypeExec( - /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, + /^([^/]+\/[^;,]+)(?:;[^,]*)?,/, parsed.pathname, ) || [ null, null, null ]; diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index 94879761553e..ed271a40e5c4 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -203,7 +203,7 @@ function throwIfUnsupportedURLScheme(parsed) { */ function throwUnknownModuleFormat(url, format) { const dataUrl = RegExpPrototypeExec( - /^data:([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, + /^data:([^/]+\/[^;,]+)(?:;[^,]*)?,/, url, ); From acc971621e264692c988c9c59f2377767ed678d8 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Sat, 22 Aug 2026 17:02:18 -0400 Subject: [PATCH 59/97] test: add Headers coverage and benchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add WHATWG Headers unit tests and a fetch/headers benchmark so Node can track the API after the implementation change lands in undici. Refs: https://github.com/nodejs/undici/pull/5699 Signed-off-by: Yagiz Nizipli PR-URL: https://github.com/nodejs/node/pull/65365 Reviewed-By: Matteo Collina Reviewed-By: Gürgün Dayıoğlu --- benchmark/fetch/headers.js | 94 ++++++++++ test/benchmark/test-benchmark-fetch.js | 7 + test/parallel/test-whatwg-headers.js | 245 +++++++++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 benchmark/fetch/headers.js create mode 100644 test/benchmark/test-benchmark-fetch.js create mode 100644 test/parallel/test-whatwg-headers.js diff --git a/benchmark/fetch/headers.js b/benchmark/fetch/headers.js new file mode 100644 index 000000000000..4ff5091ebfbf --- /dev/null +++ b/benchmark/fetch/headers.js @@ -0,0 +1,94 @@ +'use strict'; +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + n: [1e5], + method: [ + 'construct-empty', + 'construct-object', + 'construct-headers', + 'get', + 'get-common', + 'set', + 'append', + 'has', + 'delete', + 'iterate', + ], +}); + +const objectInit = { + 'Accept': 'application/json', + 'Content-Type': 'text/plain', + 'User-Agent': 'benchmark', + 'Authorization': 'Bearer token', + 'Cookie': 'a=1', + 'X-Request-Id': 'abc', + 'Cache-Control': 'no-cache', + 'Host': 'example.com', +}; + +function main({ n, method }) { + const headers = new Headers(objectInit); + const copySource = new Headers(objectInit); + let result; + + bench.start(); + switch (method) { + case 'construct-empty': + for (let i = 0; i < n; i++) + new Headers(); + break; + case 'construct-object': + for (let i = 0; i < n; i++) + new Headers(objectInit); + break; + case 'construct-headers': + for (let i = 0; i < n; i++) + new Headers(copySource); + break; + case 'get': + for (let i = 0; i < n; i++) + result = headers.get('x-request-id'); + break; + case 'get-common': + for (let i = 0; i < n; i++) + result = headers.get('content-type'); + break; + case 'set': + for (let i = 0; i < n; i++) + headers.set('x-count', i); + break; + case 'append': + for (let i = 0; i < n; i++) { + const current = new Headers(); + current.append('Accept', 'text/html'); + current.append('X-Custom', i); + } + break; + case 'has': + for (let i = 0; i < n; i++) + result = headers.has('authorization'); + break; + case 'delete': { + for (let i = 0; i < n; i++) { + const current = new Headers(objectInit); + current.delete('content-type'); + } + break; + } + case 'iterate': + for (let i = 0; i < n; i++) { + for (const entry of headers) + result = entry; + } + break; + default: + throw new Error(`Unexpected method "${method}"`); + } + bench.end(n); + + // Keep a live use so V8 cannot DCE the loop. + if (result === Symbol.for('benchmark-never')) + throw new Error('unreachable'); +} diff --git a/test/benchmark/test-benchmark-fetch.js b/test/benchmark/test-benchmark-fetch.js new file mode 100644 index 000000000000..e9c686003a01 --- /dev/null +++ b/test/benchmark/test-benchmark-fetch.js @@ -0,0 +1,7 @@ +'use strict'; + +require('../common'); + +const runBenchmark = require('../common/benchmark'); + +runBenchmark('fetch', { NODEJS_BENCHMARK_ZERO_ALLOWED: 1 }); diff --git a/test/parallel/test-whatwg-headers.js b/test/parallel/test-whatwg-headers.js new file mode 100644 index 000000000000..94610128ce11 --- /dev/null +++ b/test/parallel/test-whatwg-headers.js @@ -0,0 +1,245 @@ +'use strict'; + +// Tests below are not from WPT. + +require('../common'); +const assert = require('assert'); +const util = require('util'); + +{ + const headers = new Headers(); + assert.strictEqual(headers.get('content-type'), null); + assert.strictEqual(headers.has('content-type'), false); + assert.deepStrictEqual([...headers], []); + assert.deepStrictEqual(headers.getSetCookie(), []); +} + +{ + const headers = new Headers({ + 'Content-Type': 'text/plain', + 'Accept': 'application/json', + 'X-Custom': '1', + }); + assert.strictEqual(headers.get('content-type'), 'text/plain'); + assert.strictEqual(headers.get('Content-Type'), 'text/plain'); + assert.strictEqual(headers.get('ACCEPT'), 'application/json'); + assert.ok(headers.has('accept')); + assert.deepStrictEqual([...headers], [ + ['accept', 'application/json'], + ['content-type', 'text/plain'], + ['x-custom', '1'], + ]); +} + +{ + const headers = new Headers([ + ['X-A', '1'], + ['x-b', '2'], + ['X-A', '3'], + ]); + assert.strictEqual(headers.get('x-a'), '1, 3'); + assert.deepStrictEqual([...headers], [ + ['x-a', '1, 3'], + ['x-b', '2'], + ]); +} + +{ + const source = new Headers({ 'Content-Type': 'text/html' }); + source.append('Set-Cookie', 'a=b'); + source.append('Set-Cookie', 'c=d'); + const copy = new Headers(source); + assert.strictEqual(copy.get('content-type'), 'text/html'); + assert.deepStrictEqual(copy.getSetCookie(), ['a=b', 'c=d']); + assert.deepStrictEqual([...copy], [...source]); + copy.append('X-Copy', 'yes'); + assert.strictEqual(source.has('x-copy'), false); + source.append('Set-Cookie', 'e=f'); + assert.deepStrictEqual(copy.getSetCookie(), ['a=b', 'c=d']); +} + +{ + const headers = new Headers(); + headers.append('Accept', 'text/html'); + headers.append('accept', 'application/json'); + assert.strictEqual(headers.get('ACCEPT'), 'text/html, application/json'); + headers.set('ACCEPT', 'image/png'); + assert.strictEqual(headers.get('accept'), 'image/png'); + headers.delete('Accept'); + assert.strictEqual(headers.has('accept'), false); +} + +{ + const headers = new Headers(); + headers.append('Cookie', 'a=1'); + headers.append('cookie', 'b=2'); + assert.strictEqual(headers.get('cookie'), 'a=1; b=2'); +} + +{ + const headers = new Headers(); + headers.append('set-cookie', 'a=b'); + headers.append('Set-Cookie', 'c=d'); + assert.deepStrictEqual(headers.getSetCookie(), ['a=b', 'c=d']); + const cloned = headers.getSetCookie(); + cloned.push('e=f'); + assert.deepStrictEqual(headers.getSetCookie(), ['a=b', 'c=d']); + headers.set('set-cookie', 'only=one'); + assert.deepStrictEqual(headers.getSetCookie(), ['only=one']); + headers.delete('SET-COOKIE'); + assert.deepStrictEqual(headers.getSetCookie(), []); +} + +{ + const headers = new Headers(); + headers.set('a', ' value '); + assert.strictEqual(headers.get('a'), 'value'); + headers.set('b', '\r\n\t trimmed\t\n'); + assert.strictEqual(headers.get('b'), 'trimmed'); + headers.set('c', '\r'); + assert.strictEqual(headers.get('c'), ''); + headers.set('d', '\n'); + assert.strictEqual(headers.get('d'), ''); +} + +{ + const headers = new Headers(); + headers.set('a', ['b', 'c']); + assert.strictEqual(headers.get('a'), 'b,c'); + headers.set('b', null); + assert.strictEqual(headers.get('b'), 'null'); + headers.set('c', 1); + assert.strictEqual(headers.get('c'), '1'); +} + +{ + const headers = new Headers({ + c: '5', + b: ['3', '4'], + a: ['1', '2'], + }); + assert.deepStrictEqual([...headers.entries()], [ + ['a', '1,2'], + ['b', '3,4'], + ['c', '5'], + ]); +} + +{ + const init = [ + ['foo', '123'], + ['bar', '456'], + ]; + const headers = new Headers(init); + for (const [key, val] of headers) { + headers.delete(key); + headers.set(`x-${key}`, val); + } + assert.deepStrictEqual([...headers], [ + ['foo', '123'], + ['x-x-bar', '456'], + ]); +} + +{ + const headers = new Headers([ + ['b', '2'], + ['c', '3'], + ['e', '5'], + ]); + headers.append('d', '4'); + headers.append('a', '1'); + headers.append('f', '6'); + headers.append('c', '7'); + headers.append('abc', '8'); + assert.deepStrictEqual([...headers], [ + ['a', '1'], + ['abc', '8'], + ['b', '2'], + ['c', '3, 7'], + ['d', '4'], + ['e', '5'], + ['f', '6'], + ]); +} + +{ + const headers = new Headers({ 'Content-Type': 'application/json' }); + headers.set('Authorization', 'Bearer token'); + assert.strictEqual( + util.inspect(headers, { depth: 1 }), + "Headers { 'Content-Type': 'application/json', Authorization: 'Bearer token' }", + ); +} + +{ + const headers = new Headers(); + assert.throws(() => headers.get(), TypeError); + assert.throws(() => headers.has(), TypeError); + assert.throws(() => headers.delete(), TypeError); + assert.throws(() => headers.append('a'), TypeError); + assert.throws(() => headers.set('a'), TypeError); + assert.throws(() => headers.append('invalid @ name', 'x'), TypeError); + assert.throws(() => headers.set('a', 'a\nb'), TypeError); + assert.throws(() => headers.set('a', 'a\rb'), TypeError); + assert.throws(() => headers.set('a', 'a\0b'), TypeError); + assert.throws(() => headers.set(Symbol('x'), 'y'), TypeError); + assert.throws(() => headers.set('a', Symbol('y')), TypeError); + assert.throws(() => headers.set('', 'x'), TypeError); + assert.throws(() => headers.set('a', 'héllo\u0100'), TypeError); + assert.throws(() => new Headers(1), TypeError); + assert.throws(() => new Headers('1'), TypeError); + assert.throws(() => new Headers([['undici', 'fetch'], ['fetch']]), TypeError); +} + +{ + assert.throws(() => Headers.prototype.get.call(null, 'a'), { + name: 'TypeError', + code: 'ERR_INVALID_THIS', + }); + assert.throws(() => Headers.prototype.append.call({}, 'a', 'b'), { + name: 'TypeError', + code: 'ERR_INVALID_THIS', + }); +} + +{ + assert.strictEqual(Headers.prototype.append.length, 2); + assert.strictEqual(Headers.prototype.constructor.length, 0); + assert.strictEqual(Headers.prototype.delete.length, 1); + assert.strictEqual(Headers.prototype.get.length, 1); + assert.strictEqual(Headers.prototype.has.length, 1); + assert.strictEqual(Headers.prototype.set.length, 2); + assert.strictEqual(Headers.prototype.entries, Headers.prototype[Symbol.iterator]); + assert.strictEqual(Headers.prototype[Symbol.toStringTag], 'Headers'); + assert.strictEqual(Object.prototype.toString.call(Headers.prototype), '[object Headers]'); +} + +{ + const headers = new Headers(); + headers.set('content-type', 'text/plain'); + assert.strictEqual(headers.delete('content-type'), undefined); + assert.strictEqual(headers.delete('missing'), undefined); + assert.strictEqual(headers.set('a', 'b'), undefined); +} + +{ + const headers = new Headers(); + for (const name of [ + 'content-type', + 'accept', + 'user-agent', + 'cache-control', + 'set-cookie', + ]) { + headers.set(name, 'value'); + assert.strictEqual(headers.get(name), 'value'); + assert.ok(headers.has(name)); + } +} + +{ + const headers = new Headers(); + headers.append('fhqwhgads', `a${'\t'.repeat(1000)}a`); + assert.strictEqual(headers.get('fhqwhgads'), `a${'\t'.repeat(1000)}a`); +} From 70e41cd96659c88b753b540bced22895f0222815 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 22 Aug 2026 17:23:36 -0800 Subject: [PATCH 60/97] stream: avoid duplicated endReadableNT scheduling Calling read() on an ended stream multiple times before the microtask queue drains scheduled one endReadableNT tick per call, as the only guard was endEmitted, which is set inside the tick itself. A hello-world HTTP server was scheduling it four times per request while dumping the unread request body. Introduce a kEndScheduled flag armed when the tick is scheduled and cleared when it runs. Clearing it unconditionally matters for reused sockets: undestroy() resets endEmitted through the state descriptors but cannot reach this flag, and a stale value would block the 'end' event after a net.Socket reconnect. Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/65310 Reviewed-By: James M Snell Reviewed-By: Robert Nagy Reviewed-By: Paolo Insogna --- lib/internal/streams/readable.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 1c496a28f663..7a089c591630 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -130,6 +130,7 @@ const kFlowing = 1 << 24; const kHasPaused = 1 << 25; const kPaused = 1 << 26; const kDataListening = 1 << 27; +const kEndScheduled = 1 << 28; // TODO(benjamingr) it is likely slower to do it this way than with free functions function makeBitMapDescriptor(bit) { @@ -1742,8 +1743,8 @@ function endReadable(stream) { const state = stream._readableState; debug('endReadable'); - if ((state[kState] & kEndEmitted) === 0) { - state[kState] |= kEnded; + if ((state[kState] & (kEndEmitted | kEndScheduled)) === 0) { + state[kState] |= kEnded | kEndScheduled; process.nextTick(endReadableNT, state, stream); } } @@ -1751,6 +1752,12 @@ function endReadable(stream) { function endReadableNT(state, stream) { debug('endReadableNT'); + // The scheduled tick is running; allow endReadable() to schedule again. + // This matters both when the 'end' emission is skipped below (e.g. after + // an unshift()) and when the stream is later reset for reuse + // (see undestroy()), which clears kEndEmitted but not this flag. + state[kState] &= ~kEndScheduled; + // Check that we didn't get one last unshift. if ((state[kState] & (kErrored | kCloseEmitted | kEndEmitted)) === 0 && state.length === 0) { state[kState] |= kEndEmitted; From b4040c495a4ef2c002fbee9c694784c8d08bc870 Mon Sep 17 00:00:00 2001 From: trivenay Date: Sun, 23 Aug 2026 08:08:57 +0530 Subject: [PATCH 61/97] quic: reset rejected HTTP/3 request streams with H3_REQUEST_REJECTED An incoming HTTP/3 request stream rejected without any application processing (the session has no stream consumer) is now reset with H3_REQUEST_REJECTED so the peer learns the request was not processed, per RFC 9114 section 4.1.1. The code is surfaced per-application like the existing no-error and internal-error codes, so non-HTTP/3 applications are unchanged. Fixes: https://github.com/nodejs/node/issues/65441 Signed-off-by: Naman Trivedi PR-URL: https://github.com/nodejs/node/pull/65442 Reviewed-By: James M Snell Reviewed-By: Trivikram Kamat --- lib/internal/quic/quic.js | 11 +++- lib/internal/quic/state.js | 9 +++ src/quic/application.cc | 5 ++ src/quic/application.h | 8 +++ src/quic/http3.cc | 4 ++ src/quic/session.cc | 2 + .../test-quic-h3-request-rejected.mjs | 58 +++++++++++++++++++ 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-quic-h3-request-rejected.mjs diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index f2855d63f24a..4194aaa55f74 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -4166,7 +4166,16 @@ class QuicSession { // case rather than letting it hold flow control credit. if (!this.#hasStreamConsumer()) { process.emitWarning('A new stream was received but no stream consumer callback was provided'); - stream.destroy(); + // When the negotiated application defines a "request rejected" code + // (HTTP/3: H3_REQUEST_REJECTED), reset the stream with it so the peer + // learns the request was not processed (RFC 9114 section 4.1.1). + // Other applications have no such semantic and are torn down as before. + const rejectedCode = getQuicSessionState(this).requestRejectedCode; + if (getQuicSessionState(this).streamCallbacksSupported === 1) { + stream.destroy(undefined, { code: rejectedCode }); + } else { + stream.destroy(); + } return; } diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index a41790c52024..769fee687498 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -77,6 +77,7 @@ const { IDX_STATE_SESSION_APPLICATION_TYPE, IDX_STATE_SESSION_NO_ERROR_CODE, IDX_STATE_SESSION_INTERNAL_ERROR_CODE, + IDX_STATE_SESSION_REQUEST_REJECTED_CODE, IDX_STATE_SESSION_MAX_DATAGRAM_SIZE, IDX_STATE_SESSION_LAST_DATAGRAM_ID, IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, @@ -125,6 +126,7 @@ assert(IDX_STATE_SESSION_WRAPPED !== undefined); assert(IDX_STATE_SESSION_APPLICATION_TYPE !== undefined); assert(IDX_STATE_SESSION_NO_ERROR_CODE !== undefined); assert(IDX_STATE_SESSION_INTERNAL_ERROR_CODE !== undefined); +assert(IDX_STATE_SESSION_REQUEST_REJECTED_CODE !== undefined); assert(IDX_STATE_SESSION_MAX_DATAGRAM_SIZE !== undefined); assert(IDX_STATE_SESSION_LAST_DATAGRAM_ID !== undefined); assert(IDX_STATE_ENDPOINT_BOUND !== undefined); @@ -552,6 +554,13 @@ class QuicSessionState { handle, this.#offset + IDX_STATE_SESSION_INTERNAL_ERROR_CODE, kIsLittleEndian); } + get requestRejectedCode() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetBigUint64( + handle, this.#offset + IDX_STATE_SESSION_REQUEST_REJECTED_CODE, kIsLittleEndian); + } + /** @type {number} */ get maxDatagramSize() { const handle = this.#handle; diff --git a/src/quic/application.cc b/src/quic/application.cc index 6062f9d08483..d3c5e2611f5f 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -280,6 +280,11 @@ class DefaultApplication final : public Session::Application { return NGTCP2_INTERNAL_ERROR; } + // Raw QUIC has no "request rejected" semantic; reuse the no-error code. + error_code GetRequestRejectedCode() const override { + return GetNoErrorCode(); + } + void EarlyDataRejected() override { // Destroy all open streams — ngtcp2 has already discarded their // internal state when it rejected the early data. Use the diff --git a/src/quic/application.h b/src/quic/application.h index dec5cffb4243..ace6035a0ab7 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -77,6 +77,14 @@ class Session::Application : public MemoryRetainer { // NGTCP2_INTERNAL_ERROR (0x1). virtual error_code GetInternalErrorCode() const = 0; + // The "request rejected" code is sent on RESET_STREAM when an incoming + // request stream is rejected without any application processing (e.g. + // the session has no consumer for it), so the peer learns the request + // was not processed. For HTTP/3 this is NGHTTP3_H3_REQUEST_REJECTED + // (0x10b); other applications have no such semantic and reuse the + // "no error" code. + virtual error_code GetRequestRejectedCode() const = 0; + // Called after Session::Receive processes a packet, outside all callback // scopes. Applications can use this to handle deferred operations that // require calling into JS (e.g., HTTP/3 GOAWAY processing). diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 08aac7cdc6c4..d789eac1af1e 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -172,6 +172,10 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_H3_INTERNAL_ERROR; } + error_code GetRequestRejectedCode() const override { + return NGHTTP3_H3_REQUEST_REJECTED; + } + void EarlyDataRejected() override { // When 0-RTT is rejected, destroy the nghttp3 connection and all // open streams — ngtcp2 has discarded their internal state. diff --git a/src/quic/session.cc b/src/quic/session.cc index 60b27fbfa358..2266f74a1277 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -141,6 +141,7 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) { V(APPLICATION_TYPE, application_type, uint8_t) \ V(NO_ERROR_CODE, no_error_code, error_code) \ V(INTERNAL_ERROR_CODE, internal_error_code, error_code) \ + V(REQUEST_REJECTED_CODE, request_rejected_code, error_code) \ V(MAX_DATAGRAM_SIZE, max_datagram_size, uint16_t) \ V(LAST_DATAGRAM_ID, last_datagram_id, datagram_id) \ V(MAX_PENDING_DATAGRAMS, max_pending_datagrams, uint16_t) @@ -2660,6 +2661,7 @@ void Session::SetApplication(std::unique_ptr app) { // without duplicating the per-application table. impl_->state()->no_error_code = app->GetNoErrorCode(); impl_->state()->internal_error_code = app->GetInternalErrorCode(); + impl_->state()->request_rejected_code = app->GetRequestRejectedCode(); impl_->application_ = std::move(app); } diff --git a/test/parallel/test-quic-h3-request-rejected.mjs b/test/parallel/test-quic-h3-request-rejected.mjs new file mode 100644 index 000000000000..6ed987b395af --- /dev/null +++ b/test/parallel/test-quic-h3-request-rejected.mjs @@ -0,0 +1,58 @@ +// Flags: --experimental-quic --no-warnings + +// An incoming HTTP/3 request stream that is rejected without any +// application processing (here, the session has no stream consumer) is +// reset with H3_REQUEST_REJECTED (0x10b) so the peer learns the request +// was not processed. See RFC 9114 section 4.1.1. +// Refs: https://github.com/nodejs/node/issues/65441 + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +// RFC 9114 H3_REQUEST_REJECTED. +const H3_REQUEST_REJECTED = 0x10bn; + +// The server registers no stream consumer, so an incoming request stream +// is rejected on arrival. +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onerror = () => {}; +}), { + sni: { '*': { keys: [key], certs: [cert] } }, +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', +}); +await clientSession.opened; + +const reset = Promise.withResolvers(); +const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/test', + ':scheme': 'https', + ':authority': 'localhost', + }, +}); +stream.onreset = mustCall((err) => { + assert.strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); + assert.strictEqual(err.errorCode, H3_REQUEST_REJECTED); + reset.resolve(); +}); +await assert.rejects(stream.closed, { code: 'ERR_QUIC_APPLICATION_ERROR' }); + +await reset.promise; +await clientSession.close(); +await serverEndpoint.close(); From cba990121c41964166d0643fe65594d010ddd905 Mon Sep 17 00:00:00 2001 From: armanmikoyan Date: Sun, 23 Aug 2026 06:39:15 +0400 Subject: [PATCH 62/97] dgram: don't swallow bind errors when callback is provided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: armanmikoyan PR-URL: https://github.com/nodejs/node/pull/62602 Reviewed-By: Ethan Arrowood Reviewed-By: Matteo Collina Reviewed-By: Gürgün Dayıoğlu --- lib/dgram.js | 4 ++-- .../test-dgram-bind-error-callback.js | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-dgram-bind-error-callback.js diff --git a/lib/dgram.js b/lib/dgram.js index 517c37a53a2e..9b981be3ef0b 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -296,7 +296,7 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) { const cb = arguments.length && arguments[arguments.length - 1]; if (typeof cb === 'function') { function removeListeners() { - this.removeListener('error', removeListeners); + this.removeListener(EventEmitter.errorMonitor, removeListeners); this.removeListener('listening', onListening); } @@ -305,7 +305,7 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) { FunctionPrototypeCall(cb, this); } - this.on('error', removeListeners); + this.on(EventEmitter.errorMonitor, removeListeners); this.on('listening', onListening); } diff --git a/test/parallel/test-dgram-bind-error-callback.js b/test/parallel/test-dgram-bind-error-callback.js new file mode 100644 index 000000000000..d5009f948228 --- /dev/null +++ b/test/parallel/test-dgram-bind-error-callback.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const dgram = require('node:dgram'); + +// Ensure that bind errors (e.g. EADDRINUSE) are not silently swallowed +// when socket.bind() is called with a callback but without a user +// 'error' handler. + +const socket1 = dgram.createSocket('udp4'); + +socket1.bind(0, common.mustCall(() => { + const { port } = socket1.address(); + const socket2 = dgram.createSocket('udp4'); + + process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err.code, 'EADDRINUSE'); + socket1.close(); + socket2.close(); + })); + + socket2.bind({ port }, common.mustNotCall()); +})); From 66f5d82f1b73909955b9d88fdafa023d42246451 Mon Sep 17 00:00:00 2001 From: Taejin Kim <60560836+kimtaejin3@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:39:25 +0900 Subject: [PATCH 63/97] lib: use bracket notation instead of startsWith/endsWith for single char Signed-off-by: Taejin Kim PR-URL: https://github.com/nodejs/node/pull/61500 Reviewed-By: Colin Ihrig Reviewed-By: Jordan Harband Reviewed-By: Luigi Pinca Reviewed-By: Yagiz Nizipli Reviewed-By: Trivikram Kamat --- lib/internal/socketaddress.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/socketaddress.js b/lib/internal/socketaddress.js index 724ffd90cf77..28696d6cc87f 100644 --- a/lib/internal/socketaddress.js +++ b/lib/internal/socketaddress.js @@ -157,7 +157,7 @@ class SocketAddress { hostname: address, port, } = URLParse(`http://${input}`); - if (address.startsWith('[') && address.endsWith(']')) { + if (address[0] === '[' && address[address.length - 1] === ']') { return new SocketAddress({ address: address.slice(1, -1), port: port | 0, From 5227981d229981022f2f1915fddcdf3b7771dc90 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Tue, 18 Aug 2026 12:49:51 +0000 Subject: [PATCH 64/97] url: speed up WHATWG URL parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse one-byte ASCII inputs in place instead of copying them into a UTF-8 buffer, and reuse the original V8 string when the serialized href is unchanged. Delay URLContext allocation until parse finishes and skip ToString when the input is already a string. Signed-off-by: Yagiz Nizipli Assisted-by: Cursor PR-URL: https://github.com/nodejs/node/pull/65361 Reviewed-By: Matteo Collina Reviewed-By: Daniel Lemire Reviewed-By: Gürgün Dayıoğlu --- lib/internal/url.js | 114 +++++++++--------- src/node_url.cc | 82 ++++++++++--- .../test-whatwg-url-parse-fast-path.js | 88 ++++++++++++++ 3 files changed, 215 insertions(+), 69 deletions(-) create mode 100644 test/parallel/test-whatwg-url-parse-fast-path.js diff --git a/lib/internal/url.js b/lib/internal/url.js index 91316b5701b3..f58a26d94e7e 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -163,41 +163,62 @@ function lazyCryptoRandom() { return cryptoRandom; } +/** + * Copy href and the latest `urlComponents` snapshot into a URLContext. + * Property assignment order matches the historical URLContext fields so + * `util.inspect(..., { showHidden: true })` stays stable. + * @param {object} ctx + * @param {string} href + */ +function setURLContextFromBinding(ctx, href) { + const c = bindingUrl.urlComponents; + ctx.href = href; + ctx.protocol_end = c[0]; + ctx.username_end = c[1]; + ctx.host_start = c[2]; + ctx.host_end = c[3]; + ctx.pathname_start = c[5]; + ctx.search_start = c[6]; + ctx.hash_start = c[7]; + ctx.port = c[4]; + ctx.scheme_type = c[8]; +} + // This class provides the internal state of a URL object. An instance of this // class is stored in every URL object and is accessed internally by setters // and getters. It roughly corresponds to the concept of a URL record in the // URL Standard, with a few differences. It is also the object transported to // the C++ binding. // Refs: https://url.spec.whatwg.org/#concept-url +// +// scheme_type refers to ada::scheme::type: +// HTTP = 0, NOT_SPECIAL = 1, HTTPS = 2, WS = 3, FTP = 4, WSS = 5, FILE = 6 class URLContext { // This is the maximum value uint32_t can get. // Ada uses uint32_t(-1) for declaring omitted values. static #omitted = 4294967295; - href = ''; - protocol_end = 0; - username_end = 0; - host_start = 0; - host_end = 0; - pathname_start = 0; - search_start = 0; - hash_start = 0; - port = 0; /** - * Refers to `ada::scheme::type` - * - * enum type : uint8_t { - * HTTP = 0, - * NOT_SPECIAL = 1, - * HTTPS = 2, - * WS = 3, - * FTP = 4, - * WSS = 5, - * FILE = 6 - * }; - * @type {number} + * @param {string} [href] Parsed href. When omitted, create an empty context + * (used by `URL.parse` on invalid input). When provided, `bindingUrl.parse` + * / `update` has just written `urlComponents`. */ - scheme_type = 1; + constructor(href) { + if (href === undefined) { + this.href = ''; + this.protocol_end = 0; + this.username_end = 0; + this.host_start = 0; + this.host_end = 0; + this.pathname_start = 0; + this.search_start = 0; + this.hash_start = 0; + this.port = 0; + this.scheme_type = 1; + return; + } + setURLContextFromBinding(this, href); + } get hasPort() { return this.port !== URLContext.#omitted; @@ -831,7 +852,7 @@ const kCreateURLFromPosixPathSymbol = Symbol('kCreateURLFromPosixPath'); const kCreateURLFromWindowsPathSymbol = Symbol('kCreateURLFromWindowsPath'); class URL { - #context = new URLContext(); + #context; #searchParams; #searchParamsModified; @@ -856,16 +877,16 @@ class URL { } constructor(input, base = undefined, parseSymbol = undefined) { - markTransferMode(this, false, false); - if (arguments.length === 0) { throw new ERR_MISSING_ARGS('url'); } // StringPrototypeToWellFormed is not needed. - input = `${input}`; + if (typeof input !== 'string') { + input = `${input}`; + } - if (base !== undefined) { + if (base !== undefined && typeof base !== 'string') { base = `${base}`; } @@ -880,9 +901,12 @@ class URL { bindingUrl.pathToFileURL(input, interpretAsWindowsPath, base) : bindingUrl.parse(input, base, raiseException); } - if (href) { - this.#updateContext(href); - } + + // Delay context allocation until parse finishes so invalid URLs that + // throw do not pay for an unused URLContext. Initialize in one shot + // from the binding snapshot instead of writing an empty context first. + this.#context = href ? new URLContext(href) : new URLContext(); + markTransferMode(this, false, false); } static parse(input, base = undefined) { @@ -951,29 +975,7 @@ class URL { const previousSearch = shouldUpdateSearchParams && this.#searchParams && (this.#searchParamsModified ? this.#getSearchFromParams() : this.#getSearchFromContext()); - this.#context.href = href; - - const { - 0: protocol_end, - 1: username_end, - 2: host_start, - 3: host_end, - 4: port, - 5: pathname_start, - 6: search_start, - 7: hash_start, - 8: scheme_type, - } = bindingUrl.urlComponents; - - this.#context.protocol_end = protocol_end; - this.#context.username_end = username_end; - this.#context.host_start = host_start; - this.#context.host_end = host_end; - this.#context.port = port; - this.#context.pathname_start = pathname_start; - this.#context.search_start = search_start; - this.#context.hash_start = hash_start; - this.#context.scheme_type = scheme_type; + setURLContextFromBinding(this.#context, href); if (this.#searchParams) { // If the search string has updated, URL becomes the source of truth, and we update URLSearchParams. @@ -1198,10 +1200,12 @@ class URL { throw new ERR_MISSING_ARGS('url'); } - url = `${url}`; + if (typeof url !== 'string') { + url = `${url}`; + } if (base !== undefined) { - return bindingUrl.canParse(url, `${base}`); + return bindingUrl.canParse(url, typeof base === 'string' ? base : `${base}`); } // It is important to differentiate the canParse call statements diff --git a/src/node_url.cc b/src/node_url.cc index 16cdab073baf..0bdcd3a6591a 100644 --- a/src/node_url.cc +++ b/src/node_url.cc @@ -8,6 +8,7 @@ #include "node_metadata.h" #include "node_process-inl.h" #include "path.h" +#include "simdutf.h" #include "util-inl.h" #include "v8-fast-api-calls.h" #include "v8-local-handle.h" @@ -33,6 +34,38 @@ using v8::SnapshotCreator; using v8::String; using v8::Value; +namespace { + +// Parse a V8 string as a URL. One-byte ASCII inputs are parsed in place +// without allocating a UTF-8 copy. `reuse_input` is set when the serialized +// href is identical to that ASCII input so the caller can return the original +// V8 string. Non-ASCII inputs are never reused: UTF-8 conversion may replace +// unpaired surrogates, so the original string may not match href. +ada::result ParseUrlFromV8String( + Isolate* isolate, + Local input, + const ada::url_aggregator* base_url, + bool* reuse_input) { + { + String::ValueView view(isolate, input); + if (view.is_one_byte()) { + const char* data = reinterpret_cast(view.data8()); + const size_t length = static_cast(view.length()); + if (simdutf::validate_ascii(data, length)) [[likely]] { + const std::string_view input_view(data, length); + auto out = ada::parse(input_view, base_url); + *reuse_input = out.has_value() && out->get_href() == input_view; + return out; + } + } + } + *reuse_input = false; + Utf8Value utf8(isolate, input); + return ada::parse(utf8.ToStringView(), base_url); +} + +} // namespace + void BindingData::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("url_components_buffer", url_components_buffer_); } @@ -392,32 +425,51 @@ void BindingData::Parse(const FunctionCallbackInfo& args) { Realm* realm = Realm::GetCurrent(args); BindingData* binding_data = realm->GetBindingData(); Isolate* isolate = realm->isolate(); - std::optional base_{}; + Local input_string = args[0].As(); - Utf8Value input(isolate, args[0]); ada::result base; ada::url_aggregator* base_pointer = nullptr; if (args[1]->IsString()) { - base_ = Utf8Value(isolate, args[1]).ToString(); - base = ada::parse(*base_); - if (!base && raise_exception) { - return ThrowInvalidURL(realm->env(), input.ToStringView(), base_); - } else if (!base) { + bool unused_reuse = false; + base = ParseUrlFromV8String( + isolate, args[1].As(), nullptr, &unused_reuse); + if (!base) { + if (raise_exception) { + Utf8Value input(isolate, input_string); + Utf8Value base_utf8(isolate, args[1]); + return ThrowInvalidURL( + realm->env(), input.ToStringView(), base_utf8.ToString()); + } return; } base_pointer = &base.value(); } - auto out = - ada::parse(input.ToStringView(), base_pointer); - if (!out && raise_exception) { - return ThrowInvalidURL(realm->env(), input.ToStringView(), base_); - } else if (!out) { + bool reuse_input = false; + auto out = + ParseUrlFromV8String(isolate, input_string, base_pointer, &reuse_input); + if (!out) { + if (raise_exception) { + Utf8Value input(isolate, input_string); + std::optional base_error; + if (args[1]->IsString()) { + base_error = Utf8Value(isolate, args[1]).ToString(); + } + return ThrowInvalidURL( + realm->env(), input.ToStringView(), std::move(base_error)); + } return; } binding_data->UpdateComponents(out->get_components(), out->type); + // Already-serialized ASCII URLs are the common case. Reuse the input + // string instead of allocating an identical V8 string from href. + if (reuse_input) { + args.GetReturnValue().Set(args[0]); + return; + } + Local ret; if (ToV8Value(realm->context(), out->get_href(), isolate).ToLocal(&ret)) [[likely]] { @@ -439,13 +491,15 @@ void BindingData::Update(const FunctionCallbackInfo& args) { return; } enum url_update_action action = static_cast(val); - Utf8Value input(isolate, args[0].As()); Utf8Value new_value(isolate, args[2].As()); std::string_view new_value_view = new_value.ToStringView(); // A serialized URL is not always reparsable: the IDNA encoder can emit a // host label that the decoder rejects. Fail the update instead of crashing. - auto out = ada::parse(input.ToStringView()); + // Existing hrefs are typically already-serialized ASCII, so parse in place. + bool unused_reuse = false; + auto out = ParseUrlFromV8String( + isolate, args[0].As(), nullptr, &unused_reuse); if (!out) { return args.GetReturnValue().Set(false); } diff --git a/test/parallel/test-whatwg-url-parse-fast-path.js b/test/parallel/test-whatwg-url-parse-fast-path.js new file mode 100644 index 000000000000..e6c295f039a3 --- /dev/null +++ b/test/parallel/test-whatwg-url-parse-fast-path.js @@ -0,0 +1,88 @@ +'use strict'; + +// Covers the URL constructor parse paths that avoid a UTF-8 copy and/or +// reuse the input string when it is already a serialized ASCII href. + +const { hasIntl } = require('../common'); +const assert = require('assert'); + +const alreadySerialized = [ + 'https://nodejs.org/en/blog/', + 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' + + '/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' + + '&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' + + 'key=f5c65e1e98fe07e648249ad41e1cfdb0', + 'https://user:pass@example.com/path?search=1', + 'file:///foo/bar/test/node.js', + 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868', +]; + +for (const href of alreadySerialized) { + const url = new URL(href); + assert.strictEqual(url.href, href); + assert.strictEqual(URL.parse(href).href, href); + assert.strictEqual(URL.canParse(href), true); +} + +// Special-scheme URLs with an empty path gain a trailing slash. +{ + const url = new URL('https://example.com'); + assert.strictEqual(url.href, 'https://example.com/'); + assert.strictEqual(url.pathname, '/'); +} + +// Dot-segment normalization must still rewrite the path. +{ + const url = new URL('https://example.org/./a/../b/./c'); + assert.strictEqual(url.href, 'https://example.org/b/c'); + assert.strictEqual(url.pathname, '/b/c'); +} + +// Relative resolution against a base URL. +{ + const url = new URL('/path?x=1#h', 'https://example.com:8443/base'); + assert.strictEqual(url.href, 'https://example.com:8443/path?x=1#h'); + assert.strictEqual(url.host, 'example.com:8443'); +} + +// Non-string input is still stringified. +{ + const url = new URL({ toString: () => 'https://example.com/from-object' }); + assert.strictEqual(url.href, 'https://example.com/from-object'); +} + +// Invalid input still throws from the constructor and is null from parse(). +{ + assert.throws(() => new URL('not a url'), { + code: 'ERR_INVALID_URL', + name: 'TypeError', + }); + assert.strictEqual(URL.parse('not a url'), null); + assert.strictEqual(URL.canParse('not a url'), false); +} + +// Unpaired surrogates must not be returned as-is from href. +{ + const input = 'https://example.com/\uD800'; + const url = new URL(input); + assert.notStrictEqual(url.href, input); + assert.ok(url.href.startsWith('https://example.com/')); +} + +if (hasIntl) { + const url = new URL('http://你好你好.在线'); + assert.ok(url.hostname.startsWith('xn--')); + assert.ok(url.href.startsWith('http://xn--')); +} + +// Setters re-parse the existing href; keep component updates correct. +{ + const url = new URL('https://example.com/old'); + url.pathname = '/new'; + url.search = 'q=1'; + url.hash = 'frag'; + assert.strictEqual(url.href, 'https://example.com/new?q=1#frag'); + assert.strictEqual(url.pathname, '/new'); + assert.strictEqual(url.search, '?q=1'); + assert.strictEqual(url.hash, '#frag'); +} From ef6bb9a157add2d84504f8bca2cc9c7688f127a1 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Wed, 19 Aug 2026 01:00:46 +0000 Subject: [PATCH 65/97] url: skip unused href reuse comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Callers that never reuse the original V8 string (base URL parse and setters) now omit reuse_input so ParseUrlFromV8String does not compare href against the input. Signed-off-by: Yagiz Nizipli Assisted-by: Cursor PR-URL: https://github.com/nodejs/node/pull/65361 Reviewed-By: Matteo Collina Reviewed-By: Daniel Lemire Reviewed-By: Gürgün Dayıoğlu --- src/node_url.cc | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/node_url.cc b/src/node_url.cc index 0bdcd3a6591a..7f32a83177f4 100644 --- a/src/node_url.cc +++ b/src/node_url.cc @@ -37,15 +37,17 @@ using v8::Value; namespace { // Parse a V8 string as a URL. One-byte ASCII inputs are parsed in place -// without allocating a UTF-8 copy. `reuse_input` is set when the serialized -// href is identical to that ASCII input so the caller can return the original -// V8 string. Non-ASCII inputs are never reused: UTF-8 conversion may replace -// unpaired surrogates, so the original string may not match href. +// without allocating a UTF-8 copy. When `reuse_input` is non-null it is set +// if the serialized href is identical to that ASCII input so the caller can +// return the original V8 string. Omit it when the caller will not reuse the +// input, to skip the O(n) href comparison. Non-ASCII inputs are never reused: +// UTF-8 conversion may replace unpaired surrogates, so the original string +// may not match href. ada::result ParseUrlFromV8String( Isolate* isolate, Local input, const ada::url_aggregator* base_url, - bool* reuse_input) { + bool* reuse_input = nullptr) { { String::ValueView view(isolate, input); if (view.is_one_byte()) { @@ -54,12 +56,14 @@ ada::result ParseUrlFromV8String( if (simdutf::validate_ascii(data, length)) [[likely]] { const std::string_view input_view(data, length); auto out = ada::parse(input_view, base_url); - *reuse_input = out.has_value() && out->get_href() == input_view; + if (reuse_input != nullptr) { + *reuse_input = out.has_value() && out->get_href() == input_view; + } return out; } } } - *reuse_input = false; + if (reuse_input != nullptr) *reuse_input = false; Utf8Value utf8(isolate, input); return ada::parse(utf8.ToStringView(), base_url); } @@ -430,9 +434,7 @@ void BindingData::Parse(const FunctionCallbackInfo& args) { ada::result base; ada::url_aggregator* base_pointer = nullptr; if (args[1]->IsString()) { - bool unused_reuse = false; - base = ParseUrlFromV8String( - isolate, args[1].As(), nullptr, &unused_reuse); + base = ParseUrlFromV8String(isolate, args[1].As(), nullptr); if (!base) { if (raise_exception) { Utf8Value input(isolate, input_string); @@ -497,9 +499,7 @@ void BindingData::Update(const FunctionCallbackInfo& args) { // A serialized URL is not always reparsable: the IDNA encoder can emit a // host label that the decoder rejects. Fail the update instead of crashing. // Existing hrefs are typically already-serialized ASCII, so parse in place. - bool unused_reuse = false; - auto out = ParseUrlFromV8String( - isolate, args[0].As(), nullptr, &unused_reuse); + auto out = ParseUrlFromV8String(isolate, args[0].As(), nullptr); if (!out) { return args.GetReturnValue().Set(false); } From 647c8ddb4ee720a05c98053403d91cb080f9ad9d Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 22 Aug 2026 20:53:29 -0700 Subject: [PATCH 66/97] quic: mark drain promise handled Fixes: https://github.com/nodejs/node/issues/64290 Signed-off-by: James M Snell Assisted-by: Opencode/Opus PR-URL: https://github.com/nodejs/node/pull/65319 Reviewed-By: Stephen Belanger --- lib/internal/quic/quic.js | 2 + ...tream-writer-drain-unhandled-rejection.mjs | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 test/parallel/test-quic-stream-writer-drain-unhandled-rejection.mjs diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 4194aaa55f74..111128237e8d 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2184,6 +2184,7 @@ class QuicStream { errored = true; error = reason; if (drainWakeup != null) { + markPromiseAsHandled(drainWakeup.promise); drainWakeup.reject(error); drainWakeup = null; } @@ -2366,6 +2367,7 @@ class QuicStream { getQuicSessionState(stream.#inner.session).internalErrorCode; handle.resetStream(code); if (drainWakeup != null) { + markPromiseAsHandled(drainWakeup.promise); drainWakeup.reject(error); drainWakeup = null; } diff --git a/test/parallel/test-quic-stream-writer-drain-unhandled-rejection.mjs b/test/parallel/test-quic-stream-writer-drain-unhandled-rejection.mjs new file mode 100644 index 000000000000..6420de02ba95 --- /dev/null +++ b/test/parallel/test-quic-stream-writer-drain-unhandled-rejection.mjs @@ -0,0 +1,78 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Regression test for https://github.com/nodejs/node/issues/64290 +// When a stream writer has a pending drain promise and the remote peer +// resets the stream, the rejected drain promise must NOT surface as an +// unhandled rejection. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setImmediate as tick } from 'node:timers/promises'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); +const { drainableProtocol } = await import('stream/iter'); + +// The test fails if any unhandled rejection fires. +process.on('unhandledRejection', + mustNotCall('unexpected unhandled rejection')); + +const serverStreamReady = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall((serverSession) => { + serverSession.onstream = mustCall((stream) => { + serverStreamReady.resolve({ stream, session: serverSession }); + }); +})); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +const stream = await clientSession.createBidirectionalStream(); +const writer = stream.writer; + +// Write a small initial chunk so the server materializes the stream. +writer.writeSync(new Uint8Array([1])); + +const { stream: serverStream, session: serverSession } = + await serverStreamReady.promise; + +// Fill the write buffer to create backpressure. After this, +// writeDesiredSize should be <= 0 and canWrite should be false. +const chunk = new Uint8Array(64 * 1024); +while (writer.canWrite) { + if (!writer.writeSync(chunk)) break; +} + +// Create a drain wakeup via the drainable protocol. This simulates +// what the stream/iter infrastructure does when checking for +// backpressure. We deliberately do NOT await the returned promise — +// that is the whole point of the test. +const drainPromise = writer[drainableProtocol](); +assert.ok(drainPromise instanceof Promise, + 'expected a drain promise (buffer should be full)'); + +// Suppress the expected rejection on both sides' closed promises so +// they do not interfere with the unhandledRejection check. +const clientClosed = stream.closed.catch(() => {}); +const serverClosed = serverStream.closed.catch(() => {}); + +// Have the server send STOP_SENDING. This triggers kStopSending on +// the client writer, which rejects the unobserved drain promise. +// Without the fix this surfaces as an unhandled rejection. +serverStream.stopSending(1n); +serverStream.writer.endSync(); + +// Give the event loop time to process the frame and fire any +// unhandled-rejection events. +await tick(); +await tick(); + +// Clean up. +await Promise.all([clientClosed, serverClosed]); +serverSession.close(); +await clientSession.close(); +await serverEndpoint.close(); From d834d650561922401c33aae3285a8c4670559b68 Mon Sep 17 00:00:00 2001 From: Srinu desetti <159899608+webdevelopersrinu@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:04:20 +0530 Subject: [PATCH 67/97] fs: fix glob early return skipping sibling entries The children loop in the glob traversal returned from the whole method when a child path had already been seen through a different pattern context, silently dropping the remaining sibling entries. Whether this triggered depended on directory iteration order, which also made test-fs-glob.mjs flaky. Remove the check: the cache.add call at the start of the traversal already prevents reprocessing. Fixes: https://github.com/nodejs/node/issues/62897 Co-authored-by: semimikoh Signed-off-by: webdevelopersrinu PR-URL: https://github.com/nodejs/node/pull/64895 Reviewed-By: Aviv Keller Reviewed-By: Trivikram Kamat --- lib/internal/fs/glob.js | 6 --- test/parallel/test-fs-glob.mjs | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index c608016833f9..efa590a55749 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -576,9 +576,6 @@ class Glob { const nSymlinks = new SafeSet(); for (const index of pattern.indexes) { // For each child, check potential patterns - if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { - return; - } const current = pattern.at(index); const nextIndex = index + 1; const next = pattern.at(nextIndex); @@ -793,9 +790,6 @@ class Glob { const nSymlinks = new SafeSet(); for (const index of pattern.indexes) { // For each child, check potential patterns - if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { - return; - } const current = pattern.at(index); const nextIndex = index + 1; const next = pattern.at(nextIndex); diff --git a/test/parallel/test-fs-glob.mjs b/test/parallel/test-fs-glob.mjs index bd95bce7d0e3..9226e491358d 100644 --- a/test/parallel/test-fs-glob.mjs +++ b/test/parallel/test-fs-glob.mjs @@ -1,5 +1,6 @@ import * as common from '../common/index.mjs'; import tmpdir from '../common/tmpdir.js'; +import { spawnSync } from 'node:child_process'; import { resolve, dirname, sep, relative, join, isAbsolute } from 'node:path'; import { mkdir, writeFile, symlink, glob as asyncGlob } from 'node:fs/promises'; import { glob, globSync, Dirent, chmodSync, writeFileSync, rmSync } from 'node:fs'; @@ -669,3 +670,78 @@ describe('globSync - ENOTDIR', function() { } }); }); + +describe('glob - seen cache', function() { + // Refs: https://github.com/nodejs/node/issues/62897 + test('does not skip siblings after a seen child path', () => { + // The glob traversal used to return early from the children loop when a + // child path had already been seen through a different pattern context, + // silently dropping the remaining siblings. Whether the bug triggered + // depended on directory iteration order, so the child process pins the + // order by patching readdir before loading the glob implementation. + const script = ` + const assert = require('node:assert'); + const fs = require('node:fs'); + const fsPromises = require('node:fs/promises'); + const path = require('node:path'); + + const cwd = process.argv[1]; + const a = path.join(cwd, 'a'); + fs.mkdirSync(path.join(a, 'b', 'c', 'd'), { recursive: true }); + fs.mkdirSync(path.join(a, 'c', 'd', 'c'), { recursive: true }); + fs.writeFileSync(path.join(a, 'x'), ''); + fs.writeFileSync(path.join(a, 'z'), ''); + + const originalReaddirSync = fs.readdirSync; + const originalReaddir = fsPromises.readdir; + + const reorder = (target, entries) => { + if (!Array.isArray(entries) || target !== a) return entries; + const names = ['c', 'b', 'x', 'z']; + return names.map((name) => entries.find((entry) => entry.name === name)) + .filter(Boolean); + }; + + fs.readdirSync = function(target, options) { + return reorder(target, originalReaddirSync.call(this, target, options)); + }; + fsPromises.readdir = async function(target, options) { + return reorder(target, await originalReaddir.call(this, target, options)); + }; + + const { Glob } = require('internal/fs/glob'); + const expected = ['a/b', 'a/c', 'a/x', 'a/z']; + const normalize = (results) => + results.map((item) => item.replaceAll(path.sep, '/')).sort(); + + (async () => { + const syncResults = normalize(new Glob('a/**/../*', { cwd }).globSync()); + for (const item of expected) { + assert.ok(syncResults.includes(item), + \`missing \${item} from sync results: \${syncResults}\`); + } + + const asyncResults = []; + for await (const item of new Glob('a/**/../*', { cwd }).glob()) { + asyncResults.push(item); + } + const normalized = normalize(asyncResults); + for (const item of expected) { + assert.ok(normalized.includes(item), + \`missing \${item} from async results: \${normalized}\`); + } + })().catch((err) => { + console.error(err); + process.exitCode = 1; + }); + `; + + const seenDir = tmpdir.resolve('glob-seen'); + const child = spawnSync( + process.execPath, + ['--expose-internals', '-e', script, seenDir], + { encoding: 'utf8' }, + ); + assert.strictEqual(child.status, 0, child.stderr || child.stdout); + }); +}); From 6d260ac5f0b2eee2eee1645ca15c4148ce2a5382 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 22 Aug 2026 23:34:30 -0600 Subject: [PATCH 68/97] quic: changes for nghttp3_conn_close_stream2 nghttp2 will introduce a version 2 callback and function for closing streams. This prepares node.js for the change. Other changes may be required for its full potential. Signed-off-by: Marten Richter PR-URL: https://github.com/nodejs/node/pull/64574 Reviewed-By: James M Snell Reviewed-By: Tim Perry --- src/quic/http3.cc | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index d789eac1af1e..ff54bba0354f 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -510,7 +510,12 @@ class Http3ApplicationImpl final : public Session::Application { code = error.code(); } - int rv = nghttp3_conn_close_stream(*this, stream->id(), code); + int rv = nghttp3_conn_close_stream2( + *this, + NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET, + stream->id(), + code, + 0); // If the call is successful, Http3Application::OnStreamClose callback will // be invoked when the stream is ready to be closed. We'll handle destroying // the actual Stream object there. @@ -804,16 +809,32 @@ class Http3ApplicationImpl final : public Session::Application { return Http3ConnectionPointer(conn); } - void OnStreamClose(Stream* stream, error_code app_error_code) { - if (app_error_code != NGHTTP3_H3_NO_ERROR) { + void OnStreamClose(Stream* stream, + uint32_t flags, + error_code rx_app_error_code, + error_code tx_app_error_code) { + if (flags & NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET) { Debug(&session(), "HTTP/3 application received stream close for stream %" PRIi64 - " with code %" PRIu64, + " with remote error code %" PRIu64, stream->id(), - app_error_code); + rx_app_error_code); + } + if (flags & NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET) { + Debug(&session(), + "HTTP/3 application send stream close for stream %" PRIi64 + " with error code %" PRIu64, + stream->id(), + tx_app_error_code); } auto direction = stream->direction(); - stream->Destroy(QuicError::ForApplication(app_error_code)); + if (flags & NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET) { + stream->Destroy(QuicError::ForApplication(rx_app_error_code)); + } else if (flags & NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET) { + stream->Destroy(QuicError::ForApplication(tx_app_error_code)); + } else { + stream->Destroy(); + } ExtendMaxStreams(EndpointLabel::REMOTE, direction, 1); } @@ -1175,13 +1196,16 @@ class Http3ApplicationImpl final : public Session::Application { } static int on_stream_close(nghttp3_conn* conn, + uint32_t flags, stream_id id, - error_code app_error_code, + error_code rx_app_error_code, + error_code tx_app_error_code, void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); if (auto stream = app.session().FindStream(id)) { - app.OnStreamClose(stream.get(), app_error_code); + app.OnStreamClose( + stream.get(), flags, rx_app_error_code, tx_app_error_code); } return NGTCP2_SUCCESS; } @@ -1389,7 +1413,7 @@ class Http3ApplicationImpl final : public Session::Application { static constexpr nghttp3_callbacks kCallbacks = { on_acked_stream_data, - on_stream_close, + nullptr, // nghttp3_stream_close (deprecated) on_receive_data, on_deferred_consume, on_begin_headers, @@ -1407,10 +1431,7 @@ class Http3ApplicationImpl final : public Session::Application { on_end_origin, on_rand, on_receive_settings, -#ifdef NGHTTP3_CALLBACKS_V4 - nullptr, -#endif // NGHTTP3_CALLBACKS_V4 - }; + on_stream_close}; }; std::optional ParseHttp3TicketData(const uv_buf_t& data) { From 5d8194aff2141852eeef9d82f32c572f889c3f1e Mon Sep 17 00:00:00 2001 From: Donghoon Kang Date: Sun, 23 Aug 2026 20:01:09 +0900 Subject: [PATCH 69/97] test: simplify test-timers-interval-promisified.js Signed-off-by: HoonDongKang PR-URL: https://github.com/nodejs/node/pull/65322 Refs: https://github.com/nodejs/node/pull/57338 Reviewed-By: Luigi Pinca --- test/parallel/test-timers-interval-promisified.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/parallel/test-timers-interval-promisified.js b/test/parallel/test-timers-interval-promisified.js index 8ee8015986d7..b2f94b836807 100644 --- a/test/parallel/test-timers-interval-promisified.js +++ b/test/parallel/test-timers-interval-promisified.js @@ -247,12 +247,10 @@ process.on('multipleResolves', common.mustNotCall()); (async () => { const signal = AbortSignal.abort('boom'); - try { + await assert.rejects(async () => { const iterable = timerPromises.setInterval(2, undefined, { signal }); + // eslint-disable-next-line no-unused-vars, no-empty for await (const _ of iterable) { } - assert.fail('should have failed'); - } catch (err) { - assert.strictEqual(err.cause, 'boom'); - } + }, { cause: 'boom' }, 'should have failed'); })().then(common.mustCall()); From d647949e6976f02f28a0a9c5afdda716cb21667e Mon Sep 17 00:00:00 2001 From: Donghoon Kang Date: Sun, 23 Aug 2026 21:58:43 +0900 Subject: [PATCH 70/97] doc: fix broken links in cli.md Fix the link to the Environment variables section and replace the semi-space link with the relevant V8 documentation. Signed-off-by: HoonDongKang PR-URL: https://github.com/nodejs/node/pull/65412 Reviewed-By: Chengzhong Wu Reviewed-By: Luigi Pinca --- doc/api/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index f8373cb82d4a..e423f9f7b804 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -4328,7 +4328,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [running tests from the command line]: test.md#running-tests-from-the-command-line [scavenge garbage collector]: https://v8.dev/blog/orinoco-parallel-scavenger [security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure -[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space +[semi-space]: https://v8.dev/blog/trash-talk#minor-gc [single executable application]: single-executable-applications.md [snapshot testing]: test.md#snapshot-testing [syntax detection]: packages.md#syntax-detection From ab79cfce68f4877f64466eea1cd7ddab8fbebb8d Mon Sep 17 00:00:00 2001 From: Yuya Inoue <65857152+inoway46@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:40:03 +0900 Subject: [PATCH 71/97] test: remove test-debugger-run-after-quit-restart as flaky on macOS Signed-off-by: inoway46 PR-URL: https://github.com/nodejs/node/pull/65424 Fixes: https://github.com/nodejs/node/issues/64005 Refs: https://github.com/nodejs/node/pull/64006 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Luigi Pinca --- test/parallel/parallel.status | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status index e4c82da4d617..d13efc8ac172 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status @@ -60,8 +60,6 @@ test-http-server-headers-timeout-keepalive: PASS,FLAKY test-http-server-request-timeout-keepalive: PASS,FLAKY # https://github.com/nodejs/node/issues/60050 test-cluster-dgram-1: SKIP -# https://github.com/nodejs/node/issues/64005 -test-debugger-run-after-quit-restart: PASS,FLAKY [$arch==arm || $arch==arm64] # https://github.com/nodejs/node/pull/31178 From 170e563eb36adf1761856afd380b4ebc69e5944a Mon Sep 17 00:00:00 2001 From: Junsoo Ha <35479251+ganjanggejang@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:40:14 +0900 Subject: [PATCH 72/97] test: use spawnSyncAndAssert in windowsHide test Signed-off-by: Junsoo Ha PR-URL: https://github.com/nodejs/node/pull/65351 Reviewed-By: Antoine du Hamel Reviewed-By: Luigi Pinca Reviewed-By: James M Snell Reviewed-By: Stefan Stojanovic --- test/parallel/test-child-process-windows-hide.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/parallel/test-child-process-windows-hide.js b/test/parallel/test-child-process-windows-hide.js index c218c901a7f2..324b342a7c7b 100644 --- a/test/parallel/test-child-process-windows-hide.js +++ b/test/parallel/test-child-process-windows-hide.js @@ -8,6 +8,7 @@ const internalCp = require('internal/child_process'); const cmd = process.execPath; const args = ['-p', '42']; const options = { windowsHide: true }; +const { spawnSyncAndAssert } = require('../common/child_process'); // Since windowsHide isn't really observable, this test relies on monkey // patching spawn() and spawnSync() to verify that the flag is being passed @@ -15,12 +16,12 @@ const options = { windowsHide: true }; test('spawnSync() passes windowsHide correctly', (t) => { const spy = t.mock.method(internalCp, 'spawnSync'); - const child = cp.spawnSync(cmd, args, options); - assert.strictEqual(child.status, 0); - assert.strictEqual(child.signal, null); - assert.strictEqual(child.stdout.toString().trim(), '42'); - assert.strictEqual(child.stderr.toString().trim(), ''); + spawnSyncAndAssert(cmd, args, options, { + stdout: '42', + stderr: '', + trim: true + }); assert.strictEqual(spy.mock.calls.length, 1); assert.strictEqual(spy.mock.calls[0].arguments[0].windowsHide, true); }); From 6842e769081f3417d137954a06e1313af5e7048a Mon Sep 17 00:00:00 2001 From: Nachiketa Pathak Date: Sun, 23 Aug 2026 11:40:24 -0400 Subject: [PATCH 73/97] test: convert forEach to for of test-messageevent-brandcheck file Signed-off-by: Nachiketa Pathak PR-URL: https://github.com/nodejs/node/pull/65279 Reviewed-By: Aviv Keller Reviewed-By: Ethan Arrowood Reviewed-By: Colin Ihrig --- test/parallel/test-messageevent-brandcheck.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/parallel/test-messageevent-brandcheck.js b/test/parallel/test-messageevent-brandcheck.js index 17f2b708cc56..a78affb785af 100644 --- a/test/parallel/test-messageevent-brandcheck.js +++ b/test/parallel/test-messageevent-brandcheck.js @@ -3,12 +3,6 @@ require('../common'); const assert = require('assert'); -[ - 'data', - 'origin', - 'lastEventId', - 'source', - 'ports', -].forEach((i) => { +for (const i of ['data', 'origin', 'lastEventId', 'source', 'ports']) { assert.throws(() => Reflect.get(MessageEvent.prototype, i, {}), TypeError); -}); +} From 43c83fa48d6bd9854502674442b5762bb2f656dd Mon Sep 17 00:00:00 2001 From: GetThatCookie Date: Sun, 23 Aug 2026 17:40:34 +0200 Subject: [PATCH 74/97] stream: reuse unexposed managed read buffers Signed-off-by: GetThatCookie PR-URL: https://github.com/nodejs/node/pull/64990 Reviewed-By: Robert Nagy Reviewed-By: Matteo Collina Reviewed-By: Trivikram Kamat --- src/env.cc | 21 +++++++++++++++++---- src/env.h | 3 +++ src/stream_base.cc | 2 ++ test/cctest/test_environment.cc | 21 +++++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/env.cc b/src/env.cc index 88f188b2baee..84957de5cb01 100644 --- a/src/env.cc +++ b/src/env.cc @@ -76,6 +76,8 @@ using v8::Undefined; using v8::Value; using worker::Worker; +constexpr size_t kManagedBufferCacheSize = 64 * 1024; + int const ContextEmbedderTag::kNodeContextTag = 0x6e6f64; void* const ContextEmbedderTag::kNodeContextTagPtr = const_cast( static_cast(&ContextEmbedderTag::kNodeContextTag)); @@ -766,10 +768,16 @@ void Environment::add_refs(int64_t diff) { } uv_buf_t Environment::allocate_managed_buffer(const size_t suggested_size) { - std::unique_ptr bs = ArrayBuffer::NewBackingStore( - isolate(), - suggested_size, - BackingStoreInitializationMode::kUninitialized); + std::unique_ptr bs; + if (suggested_size == kManagedBufferCacheSize && + managed_buffer_cache_ != nullptr) { + bs = std::move(managed_buffer_cache_); + } else { + bs = ArrayBuffer::NewBackingStore( + isolate(), + suggested_size, + BackingStoreInitializationMode::kUninitialized); + } uv_buf_t buf = uv_buf_init(static_cast(bs->Data()), bs->ByteLength()); released_allocated_buffers_.emplace(buf.base, std::move(bs)); return buf; @@ -787,6 +795,11 @@ std::unique_ptr Environment::release_managed_buffer( return bs; } +void Environment::recycle_managed_buffer(std::unique_ptr bs) { + if (bs != nullptr && bs->ByteLength() == kManagedBufferCacheSize) + managed_buffer_cache_ = std::move(bs); +} + std::string Environment::GetExecPath(const std::vector& argv) { char exec_path_buf[2 * PATH_MAX]; size_t exec_path_len = sizeof(exec_path_buf); diff --git a/src/env.h b/src/env.h index aa75bb24ae1d..c5501cd7914a 100644 --- a/src/env.h +++ b/src/env.h @@ -1057,6 +1057,8 @@ class Environment final : public MemoryRetainer { uv_buf_t allocate_managed_buffer(const size_t suggested_size); std::unique_ptr release_managed_buffer(const uv_buf_t& buf); + // Only buffers that were not exposed externally may be recycled. + void recycle_managed_buffer(std::unique_ptr bs); void AddUnmanagedFd(int fd); void RemoveUnmanagedFd(int fd); @@ -1274,6 +1276,7 @@ class Environment final : public MemoryRetainer { // track of the BackingStore for a given pointer. std::unordered_map> released_allocated_buffers_; + std::unique_ptr managed_buffer_cache_; v8::CpuProfiler* cpu_profiler_ = nullptr; std::vector pending_profiles_; diff --git a/src/stream_base.cc b/src/stream_base.cc index 360986c8935d..f57f75e5f275 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -696,6 +696,7 @@ void EmitToJSStreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) { std::unique_ptr bs = env->release_managed_buffer(buf_); if (nread <= 0) { + env->recycle_managed_buffer(std::move(bs)); if (nread < 0) stream->CallJSOnreadMethod(nread, Local()); return; @@ -707,6 +708,7 @@ void EmitToJSStreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) { bs = ArrayBuffer::NewBackingStore( isolate, nread, BackingStoreInitializationMode::kUninitialized); memcpy(bs->Data(), old_bs->Data(), nread); + env->recycle_managed_buffer(std::move(old_bs)); } stream->CallJSOnreadMethod(nread, ArrayBuffer::New(isolate, std::move(bs))); diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc index d129005b95c1..a219d5125701 100644 --- a/test/cctest/test_environment.cc +++ b/test/cctest/test_environment.cc @@ -38,6 +38,27 @@ class EnvironmentTest : public EnvironmentTestFixture { } }; +TEST_F(EnvironmentTest, ManagedBufferCache) { + constexpr size_t kCacheSize = 64 * 1024; + constexpr size_t kOtherSize = 1024; + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv}; + + (*env)->recycle_managed_buffer(nullptr); + + uv_buf_t buffer = (*env)->allocate_managed_buffer(kCacheSize); + char* cached_data = buffer.base; + (*env)->recycle_managed_buffer((*env)->release_managed_buffer(buffer)); + + buffer = (*env)->allocate_managed_buffer(kOtherSize); + (*env)->recycle_managed_buffer((*env)->release_managed_buffer(buffer)); + + buffer = (*env)->allocate_managed_buffer(kCacheSize); + EXPECT_EQ(buffer.base, cached_data); + (*env)->release_managed_buffer(buffer); +} + TEST_F(EnvironmentTest, EnvironmentWithoutBrowserGlobals) { const v8::HandleScope handle_scope(isolate_); Argv argv; From 01177a61e33fcc66692f83ccdc94b0a03a6b3d0f Mon Sep 17 00:00:00 2001 From: semimikoh Date: Mon, 24 Aug 2026 00:40:58 +0900 Subject: [PATCH 75/97] test_runner: match dotfiles in default coverage exclude The default coverage exclude globs did not match dotfiles, so test files such as `test/.foo.test.js` were incorrectly included in coverage reports. Apply the `dot: true` minimatch option when matching the relative path so the default exclude patterns cover dotfiles, while keeping plain matching for the absolute path to avoid misinterpreting dot segments in the filesystem path (e.g. tmp dirs like `test/.tmp.0`). Fixes: https://github.com/nodejs/node/issues/63397 Signed-off-by: semimikoh PR-URL: https://github.com/nodejs/node/pull/63401 Reviewed-By: Matteo Collina Reviewed-By: Aviv Keller Reviewed-By: Chemi Atlow Reviewed-By: Benjamin Gruenbaum Reviewed-By: Moshe Atlow --- lib/internal/test_runner/coverage.js | 26 +++++++-- .../test/.dotfile.cjs | 7 +++ ...test-runner-coverage-default-exclusion.mjs | 57 ++++++++++--------- 3 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index cdbece1eeae7..44c197d960c8 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -46,6 +46,9 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; +// Match dotfiles (e.g. `test/.foo.js`) when applying coverage globs so the +// default exclude patterns cover them. +const kMatchGlobPatternOptions = { __proto__: null, dot: true }; const kTypeOnlyImportRegex = /^\s*import\s+type\b/u; const kTypeScriptSourceRegex = /\.(?:cts|mts|ts)$/u; @@ -61,6 +64,14 @@ function getStripTypeScriptTypesForCoverage() { return stripTypeScriptTypesForCoverage; } +function createCoverageMatcher(pattern) { + return { + __proto__: null, + relative: createMatcher(pattern, kMatchGlobPatternOptions), + absolute: createMatcher(pattern), + }; +} + class CoverageLine { constructor(line, startOffset, src, length = src?.length) { const newlineLength = src == null ? 0 : @@ -557,23 +568,28 @@ class TestCoverage { // TestCoverage instance, so compile each glob to a matcher once and reuse // it for every file. Building a fresh Minimatch per call (the previous // behavior) dominated the coverage report time, scaling with - // files * globs. + // files * globs. Each glob compiles to a matcher pair: `relative` enables + // dot:true so globs match dotfiles within the project, while `absolute` + // keeps the default behavior to avoid misinterpreting dot segments in the + // absolute filesystem path (e.g. tmp dirs like `test/.tmp.0`). this.#excludeMatchers ??= ArrayPrototypeMap( - this.options.coverageExcludeGlobs ?? [], (pattern) => createMatcher(pattern)); + this.options.coverageExcludeGlobs ?? [], createCoverageMatcher); this.#includeMatchers ??= ArrayPrototypeMap( - this.options.coverageIncludeGlobs ?? [], (pattern) => createMatcher(pattern)); + this.options.coverageIncludeGlobs ?? [], createCoverageMatcher); // This check filters out files that match the exclude globs. for (let i = 0; i < this.#excludeMatchers.length; ++i) { const matcher = this.#excludeMatchers[i]; - if (matcher.match(relativePath) || matcher.match(absolutePath)) return true; + if (matcher.relative.match(relativePath) || + matcher.absolute.match(absolutePath)) return true; } // This check filters out files that do not match the include globs. if (this.#includeMatchers.length > 0) { for (let i = 0; i < this.#includeMatchers.length; ++i) { const matcher = this.#includeMatchers[i]; - if (matcher.match(relativePath) || matcher.match(absolutePath)) return false; + if (matcher.relative.match(relativePath) || + matcher.absolute.match(absolutePath)) return false; } return true; } diff --git a/test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs b/test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs new file mode 100644 index 000000000000..ec0a4c24fffb --- /dev/null +++ b/test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs @@ -0,0 +1,7 @@ +const test = require('node:test'); +const assert = require('node:assert'); +const { foo } = require('../logic-file.js'); + +test('foo returns 1 from a dotfile test', () => { + assert.strictEqual(foo(), 1); +}); diff --git a/test/parallel/test-runner-coverage-default-exclusion.mjs b/test/parallel/test-runner-coverage-default-exclusion.mjs index 44e5f7600d32..f6080612a37c 100644 --- a/test/parallel/test-runner-coverage-default-exclusion.mjs +++ b/test/parallel/test-runner-coverage-default-exclusion.mjs @@ -16,6 +16,16 @@ async function setupFixtures() { await cp(fixtureDir, tmpdir.path, { recursive: true }); } +function assertDefaultExclusions(stdout) { + assert.match(stdout, /# start of coverage report/); + assert.doesNotMatch(stdout, /# file-test\.js\s+\|/); + assert.doesNotMatch(stdout, /# file\.test\.mjs\s+\|/); + assert.doesNotMatch(stdout, /# file\.test\.ts\s+\|/); + assert.doesNotMatch(stdout, /# test\.cjs\s+\|/); + assert.doesNotMatch(stdout, /#\s+not-matching-test-name\.js\s+\|/); + assert.match(stdout, /# end of coverage report/); +} + describe('test runner coverage default exclusion', skipIfNoInspector, () => { before(async () => { await setupFixtures(); @@ -58,18 +68,6 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { }); it('should exclude test files from coverage by default', async () => { - const report = [ - '# start of coverage report', - '# --------------------------------------------------------------', - '# file | line % | branch % | funcs % | uncovered lines', - '# --------------------------------------------------------------', - '# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7', - '# --------------------------------------------------------------', - '# all files | 66.67 | 100.00 | 50.00 | ', - '# --------------------------------------------------------------', - '# end of coverage report', - ].join('\n'); - const args = [ '--no-experimental-strip-types', '--test', @@ -82,23 +80,11 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertDefaultExclusions(result.stdout.toString()); assert.strictEqual(result.status, 0); }); it('should exclude ts test files', async () => { - const report = [ - '# start of coverage report', - '# --------------------------------------------------------------', - '# file | line % | branch % | funcs % | uncovered lines', - '# --------------------------------------------------------------', - '# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7', - '# --------------------------------------------------------------', - '# all files | 66.67 | 100.00 | 50.00 | ', - '# --------------------------------------------------------------', - '# end of coverage report', - ].join('\n'); - const args = [ '--test', '--experimental-test-coverage', @@ -111,7 +97,26 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertDefaultExclusions(result.stdout.toString()); + assert.strictEqual(result.status, 0); + }); + + it('should exclude dotfile test files from coverage by default', async () => { + const args = [ + '--no-experimental-strip-types', + '--test', + '--experimental-test-coverage', + '--test-reporter=tap', + 'test/.dotfile.cjs', + ]; + const result = spawnSync(process.execPath, args, { + env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path }, + cwd: tmpdir.path + }); + + assert.strictEqual(result.stderr.toString(), ''); + assertDefaultExclusions(result.stdout.toString()); + assert.doesNotMatch(result.stdout.toString(), /#\s+\.dotfile\.cjs\s+\|/); assert.strictEqual(result.status, 0); }); }); From 2d7e3f6c9f2d542bcdf8de2fc60293567037ab30 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 24 Aug 2026 03:52:12 +1200 Subject: [PATCH 76/97] tty: add raw-vt and io raw modes Signed-off-by: Samuel Williams PR-URL: https://github.com/nodejs/node/pull/64140 Refs: https://github.com/nodejs/node/issues/63059 Refs: https://github.com/libuv/libuv/issues/32 Reviewed-By: Anna Henningsen Reviewed-By: James M Snell Reviewed-By: Trivikram Kamat --- doc/api/tty.md | 30 ++++++++++++-- lib/tty.js | 33 +++++++++++++--- src/tty_wrap.cc | 10 +++-- test/pseudo-tty/test-set-raw-mode-modes.js | 43 +++++++++++++++++++++ test/pseudo-tty/test-set-raw-mode-modes.out | 4 ++ 5 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 test/pseudo-tty/test-set-raw-mode-modes.js create mode 100644 test/pseudo-tty/test-set-raw-mode-modes.out diff --git a/doc/api/tty.md b/doc/api/tty.md index cbfb3cc78377..f50848101e04 100644 --- a/doc/api/tty.md +++ b/doc/api/tty.md @@ -69,12 +69,18 @@ A `boolean` that is always `true` for `tty.ReadStream` instances. -* `mode` {boolean} If `true`, configures the `tty.ReadStream` to operate as a - raw device. If `false`, configures the `tty.ReadStream` to operate in its - default mode. The `readStream.isRaw` property will be set to the resulting - mode. +* `mode` {boolean|string} If `true` or `'raw'`, configures the + `tty.ReadStream` to operate as a raw device. If `'io'`, configures the + `tty.ReadStream` to operate in binary-safe I/O mode. If `false`, configures + the `tty.ReadStream` to operate in its default mode. The `readStream.isRaw` + property will be set to whether the stream is in raw mode, and the + `readStream.rawMode` property will be set to the resulting mode. * Returns: {this} The read stream instance. Allows configuration of `tty.ReadStream` so that it operates as a raw device. @@ -91,6 +97,22 @@ buffer. When opening `"\\\\.\\CONIN$"` with the [`fs.open()`][] family of APIs (for passing into `new tty.ReadStream()`), be sure to use a read/write flag such as `'r+'`. +When in binary-safe I/O mode, terminal output processing is also disabled. +This corresponds to libuv's `UV_TTY_MODE_IO` mode and is not supported on +Windows. + +### `readStream.rawMode` + + + +* {boolean|string} + +The current raw mode for the `tty.ReadStream`. This is `false` when the stream +is in its default mode, `'raw'` when raw input mode is enabled, and `'io'` when +binary-safe I/O mode is enabled. + ## Class: `tty.WriteStream` + +* `other` {Histogram} The histogram to compare against. +* Returns: {number} A value between -1.0 and 1.0. + +Computes [Cliff's delta][], a non-parametric effect size measure. Returns +the probability that a random value from this histogram exceeds a random +value from `other`, minus the reverse probability. A value of 1 means every +value in this histogram exceeds every value in `other`; -1 means the +opposite; 0 means no tendency in either direction. + +### `histogram.cohensD(other)` + + + +* `other` {Histogram} The histogram to compare against. +* Returns: {number} The effect size. + +Computes [Cohen's d][] effect size, the standardized difference between the +means of this histogram and `other`, using the pooled standard deviation. +Positive values indicate this histogram has a higher mean. By convention, +|d| < 0.2 is a small effect, 0.5 is medium, and 0.8 or greater is large. +Both histograms must have at least 2 recorded values; otherwise returns 0. + ### `histogram.countAt(value)` + +* Type: {number} + +The exponentially weighted moving average of recorded values. Only active +when the histogram was created with a `halfLife` option greater than 0. +Returns `0` when EWMA is disabled or no values have been recorded. + +### `histogram.ewmaStddev` + + + +* Type: {number} + +The exponentially weighted moving standard deviation. Only active when the +histogram was created with a `halfLife` option greater than 0. Returns `0` +when EWMA is disabled or no values have been recorded. + +### `histogram.ewmaErrorRate` + + + +* Type: {number} + +The EWMA-smoothed probability of a recorded value exceeding the configured +`threshold`. Only active when the histogram was created with both `halfLife` +and `threshold` options. Returns `0` when not enabled or no values have been +recorded. + +### `histogram.burnRate(sloTarget)` + + + +* `sloTarget` {number} The SLO target as a fraction between 0 and 1 + (exclusive). For example, `0.999` for a 99.9% SLO. +* Returns: {number} + +Returns the SLO burn rate: `ewmaErrorRate / (1 - sloTarget)`. A burn rate +of 1 means the error budget will be exactly exhausted over the SLO window. +A burn rate greater than 1 means it is being consumed faster than allowed. +Requires the histogram to have been created with both `halfLife` and +`threshold` options. + +```js +const { createHistogram } = require('node:perf_hooks'); + +// Track latency with a 200ms SLO threshold, half-life of 100 samples +const h = createHistogram({ halfLife: 100, threshold: 200_000_000 }); + +// ... record latency values ... + +// Check burn rate against a 99.9% SLO +const rate = h.burnRate(0.999); +if (rate > 1) { + console.log(`SLO burn rate: ${rate.toFixed(2)}x — error budget depleting`); +} +``` + ### `histogram.ksTest(other)` + +* `other` {Histogram} The histogram to compare against. +* Returns: {Object} + * `uStatistic` {number} The Mann-Whitney U statistic. + * `zScore` {number} The z-score (normal approximation). + * `pValue` {number} Two-tailed p-value. + +Performs a [Mann-Whitney U test][] comparing whether this histogram tends to +produce larger or smaller values than `other`. Unlike `welchTest()`, this is a +non-parametric test that makes no assumptions about the shape of the +distributions. Uses the normal approximation with tie correction for the +p-value. + ### `histogram.max` + +* `percentile` {number} A percentile value in the range (0, 100]. +* `options` {Object} + * `confidence` {number} The confidence level for the interval, between + 0 and 1 (exclusive). **Default:** `0.95`. +* Returns: {Object} + * `value` {number} The point estimate (same as `histogram.percentile()`). + * `lower` {number} The lower bound of the confidence interval. + * `upper` {number} The upper bound of the confidence interval. + +Returns a confidence interval for the given percentile using the exact +binomial method. With fewer samples, the interval will be wider, reflecting +the greater uncertainty in the percentile estimate. Requires at least 2 +recorded values; with fewer than 2, `lower` and `upper` will equal `value`. + +```js +const { createHistogram } = require('node:perf_hooks'); + +const h = createHistogram(); +for (let i = 0; i < 1000; i++) { + h.record(Math.floor(Math.random() * 100)); +} + +const ci = h.percentileCI(99); +console.log(ci.value); // The p99 point estimate +console.log(ci.lower); // The lower bound (95% confidence) +console.log(ci.upper); // The upper bound (95% confidence) +``` + ### `histogram.percentiles` + +* `other` {Histogram} The histogram to compare against. +* `options` {Object} + * `confidence` {number} Confidence level for the interval, between 0 and 1. + **Default:** `0.95`. +* Returns: {Object} + * `tStatistic` {number} The Welch t-statistic. + * `degreesOfFreedom` {number} Welch-Satterthwaite degrees of freedom. + * `pValue` {number} Two-tailed p-value. + * `confidenceInterval` {Object} + * `lower` {number} Lower bound of the confidence interval on the + difference of means. + * `upper` {number} Upper bound. + +Performs [Welch's t-test][] comparing the means of this histogram and `other`. +The p-value indicates the probability of observing a difference at least this +extreme under the null hypothesis that the two distributions have the same +mean. Both histograms must have at least 2 recorded values; otherwise the +result has `pValue` 1 and `tStatistic` 0. + ## Class: `ELDHistogram extends Histogram` A `Histogram` that records event loop delay, returned by @@ -2280,6 +2465,32 @@ const violating = latency.ccdf(500_000_000); console.log(`${(violating * 100).toFixed(1)}% of requests violating SLO`); ``` +### SLO burn rate monitoring + +```js +const { createHistogram } = require('node:perf_hooks'); + +// Track latency with EWMA (half-life 100 samples) and a 200ms SLO threshold +const latency = createHistogram({ + halfLife: 100, + threshold: 200_000_000, // 200ms in nanoseconds +}); + +// Record request latencies... + +// Smoothed error rate: probability of exceeding the threshold +console.log(`Error rate: ${(latency.ewmaErrorRate * 100).toFixed(2)}%`); + +// Burn rate against a 99.9% SLO +// >1 means the error budget is depleting faster than allowed +const rate = latency.burnRate(0.999); +console.log(`Burn rate: ${rate.toFixed(2)}x`); + +// EWMA mean and stddev track the smoothed latency +console.log(`EWMA latency: ${latency.ewmaMean.toFixed(0)}ns`); +console.log(`EWMA stddev: ${latency.ewmaStddev.toFixed(0)}ns`); +``` + ### Regression detection with KS test ```js @@ -2331,6 +2542,46 @@ newSnapshot.subtract(snapshot); console.log('Recent p99:', newSnapshot.percentile(99)); ``` +### Benchmark comparison with Welch's t-test + +```js +const { createHistogram } = require('node:perf_hooks'); + +const baseline = createHistogram(); +const candidate = createHistogram(); + +// Record operation rates from the old and new builds... + +const result = baseline.welchTest(candidate); +const improvement = ((candidate.mean - baseline.mean) / baseline.mean * 100); + +console.log(`Improvement: ${improvement.toFixed(2)}%`); +console.log(`p-value: ${result.pValue.toFixed(6)}`); +console.log(`95% CI: [${result.confidenceInterval.lower.toFixed(2)}, ` + + `${result.confidenceInterval.upper.toFixed(2)}]`); + +if (result.pValue < 0.05) { + const d = baseline.cohensD(candidate); + console.log(`Statistically significant (Cohen's d = ${d.toFixed(4)})`); +} +``` + +### Effect size with Cliff's delta + +```js +const { createHistogram } = require('node:perf_hooks'); + +const before = createHistogram(); +const after = createHistogram(); + +// Record latencies before and after a change... + +const delta = before.cliffsD(after); +// A delta > 0: before tends to produce larger values (improvement) +// A delta < 0: after tends to produce larger values (regression) +console.log(`Cliff's delta: ${delta.toFixed(4)}`); +``` + ## Examples ### Measuring the duration of async operations @@ -2585,13 +2836,17 @@ dns.promises.resolve('localhost'); ``` [Async Hooks]: async_hooks.md +[Cliff's delta]: https://en.wikipedia.org/wiki/Effect_size#Cliff's_delta +[Cohen's d]: https://en.wikipedia.org/wiki/Effect_size#Cohen's_d [Fetch Response Body Info]: https://fetch.spec.whatwg.org/#response-body-info [Fetch Timing Info]: https://fetch.spec.whatwg.org/#fetch-timing-info [High Resolution Time]: https://www.w3.org/TR/hr-time-2 +[Mann-Whitney U test]: https://en.wikipedia.org/wiki/Mann%E2%80%93Whitney_U_test [Performance Timeline]: https://w3c.github.io/performance-timeline/ [Resource Timing]: https://www.w3.org/TR/resource-timing-2/ [User Timing]: https://www.w3.org/TR/user-timing/ [Web Performance APIs]: https://w3c.github.io/perf-timing-primer/ +[Welch's t-test]: https://en.wikipedia.org/wiki/Welch%27s_t-test [Worker threads]: worker_threads.md#worker-threads [`'exit'`]: process.md#event-exit [`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options diff --git a/lib/internal/histogram.js b/lib/internal/histogram.js index c16c894dd147..952f6058019f 100644 --- a/lib/internal/histogram.js +++ b/lib/internal/histogram.js @@ -213,6 +213,67 @@ class Histogram { return this[kHandle]?.exceedsBigInt(); } + /** + * Returns the exponentially weighted moving average of recorded values. + * Only active when the histogram was created with a `halfLife` option. + * Returns 0 when EWMA is not enabled or no values have been recorded. + * @readonly + * @type {number} + */ + get ewmaMean() { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + return this[kHandle]?.ewmaMean(); + } + + /** + * Returns the exponentially weighted moving standard deviation. + * Only active when the histogram was created with a `halfLife` option. + * Returns 0 when EWMA is not enabled or no values have been recorded. + * @readonly + * @type {number} + */ + get ewmaStddev() { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + return this[kHandle]?.ewmaStddev(); + } + + /** + * Returns the EWMA-smoothed error rate: the probability of a recorded + * value exceeding the configured `threshold`. Only active when the + * histogram was created with both `halfLife` and `threshold` options. + * Returns 0 when not enabled or no values have been recorded. + * @readonly + * @type {number} + */ + get ewmaErrorRate() { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + return this[kHandle]?.ewmaErrorRate(); + } + + /** + * Returns the SLO burn rate: how fast the error budget is being consumed. + * A burn rate of 1 means the budget will be exactly exhausted over the + * SLO window. A burn rate of 10 means it is being consumed 10x faster. + * Requires `halfLife` and `threshold` to be configured. + * @param {number} sloTarget - The SLO target as a fraction (e.g. 0.999 + * for 99.9%). + * @returns {number} + */ + burnRate(sloTarget) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + validateNumber(sloTarget, 'sloTarget'); + if (NumberIsNaN(sloTarget) || sloTarget <= 0 || sloTarget >= 1) + throw new ERR_OUT_OF_RANGE('sloTarget', '> 0 && < 1', sloTarget); + const errorRate = this[kHandle]?.ewmaErrorRate(); + if (errorRate === undefined) return undefined; + const errorBudget = 1 - sloTarget; + return errorRate / errorBudget; + } + /** * Returns the Kolmogorov-Smirnov test statistic comparing this * histogram's distribution to another's. Returns a value between @@ -228,6 +289,95 @@ class Histogram { return this[kHandle]?.ksTest(other[kHandle]); } + /** + * Performs Welch's t-test comparing this histogram to another. + * Returns an object with the t-statistic, degrees of freedom, + * two-tailed p-value, and confidence interval on the difference + * of means. + * @param {Histogram} other + * @param {{ confidence?: number }} [options] + * @returns {{ tStatistic: number, degreesOfFreedom: number, + * pValue: number, + * confidenceInterval: { lower: number, upper: number } }} + */ + welchTest(other, options = kEmptyObject) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + if (!isHistogram(other)) + throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other); + validateObject(options, 'options'); + const { confidence = 0.95 } = options; + validateNumber(confidence, 'options.confidence'); + if (NumberIsNaN(confidence) || confidence <= 0 || confidence >= 1) + throw new ERR_OUT_OF_RANGE('options.confidence', + '> 0 && < 1', confidence); + const result = this[kHandle]?.welchTest(other[kHandle], confidence); + if (result === undefined) return undefined; + return { + __proto__: null, + tStatistic: result[0], + degreesOfFreedom: result[1], + pValue: result[2], + confidenceInterval: { + __proto__: null, + lower: result[3], + upper: result[4], + }, + }; + } + + /** + * Performs a Mann-Whitney U test comparing this histogram to + * another. Returns an object with the U statistic, z-score, + * and two-tailed p-value (normal approximation). + * @param {Histogram} other + * @returns {{ uStatistic: number, zScore: number, pValue: number }} + */ + mannWhitneyTest(other) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + if (!isHistogram(other)) + throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other); + const result = this[kHandle]?.mannWhitneyTest(other[kHandle]); + if (result === undefined) return undefined; + return { + __proto__: null, + uStatistic: result[0], + zScore: result[1], + pValue: result[2], + }; + } + + /** + * Computes Cohen's d effect size comparing this histogram to + * another. Uses the pooled standard deviation. Positive values + * indicate this histogram has a higher mean. + * @param {Histogram} other + * @returns {number} + */ + cohensD(other) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + if (!isHistogram(other)) + throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other); + return this[kHandle]?.cohensD(other[kHandle]); + } + + /** + * Computes Cliff's delta comparing this histogram to another. + * Returns a value between -1 and 1. Positive values indicate + * this histogram tends to produce larger values. + * @param {Histogram} other + * @returns {number} + */ + cliffsD(other) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + if (!isHistogram(other)) + throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other); + return this[kHandle]?.cliffsD(other[kHandle]); + } + /** * Returns the excess kurtosis of the recorded values, a measure of * the heaviness of the distribution's tails. A positive value indicates @@ -326,6 +476,36 @@ class Histogram { return this[kHandle]?.percentileBigInt(percentile); } + /** + * Returns a confidence interval for the given percentile using the + * exact binomial method. The result contains the point estimate and + * the lower/upper bounds of the interval. + * @param {number} percentile + * @param {{ confidence?: number }} [options] + * @returns {{ value: number, lower: number, upper: number }} + */ + percentileCI(percentile, options = kEmptyObject) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + validateNumber(percentile, 'percentile'); + if (NumberIsNaN(percentile) || percentile <= 0 || percentile > 100) + throw new ERR_OUT_OF_RANGE('percentile', '> 0 && <= 100', percentile); + validateObject(options, 'options'); + const { confidence = 0.95 } = options; + validateNumber(confidence, 'options.confidence'); + if (NumberIsNaN(confidence) || confidence <= 0 || confidence >= 1) + throw new ERR_OUT_OF_RANGE('options.confidence', + '> 0 && < 1', confidence); + const result = this[kHandle]?.percentileCI(percentile, confidence); + if (result === undefined) return undefined; + return { + __proto__: null, + value: result[0], + lower: result[1], + upper: result[2], + }; + } + /** * @readonly * @type {Map} @@ -397,7 +577,7 @@ class Histogram { } toJSON() { - return { + const json = { count: this.count, min: this.min, max: this.max, @@ -406,8 +586,12 @@ class Histogram { stddev: this.stddev, skewness: this.skewness, kurtosis: this.kurtosis, + ewmaMean: this.ewmaMean, + ewmaStddev: this.ewmaStddev, + ewmaErrorRate: this.ewmaErrorRate, percentiles: ObjectFromEntries(MapPrototypeEntries(this.percentiles)), }; + return json; } } @@ -538,7 +722,9 @@ function createRecordableHistogram(handle) { * @param {{ * lowest? : number, * highest? : number, - * figures? : number + * figures? : number, + * halfLife? : number, + * threshold? : number * }} [options] * @returns {RecordableHistogram} */ @@ -548,6 +734,8 @@ function createHistogram(options = kEmptyObject) { lowest = 1, highest = NumberMAX_SAFE_INTEGER, figures = 3, + halfLife = 0, + threshold = 0, } = options; if (typeof lowest !== 'bigint') validateInteger(lowest, 'options.lowest', 1, NumberMAX_SAFE_INTEGER); @@ -558,7 +746,14 @@ function createHistogram(options = kEmptyObject) { throw new ERR_INVALID_ARG_VALUE.RangeError('options.highest', highest); } validateInteger(figures, 'options.figures', 1, 5); - return createRecordableHistogram(new _Histogram(lowest, highest, figures)); + validateNumber(halfLife, 'options.halfLife'); + if (halfLife < 0) + throw new ERR_OUT_OF_RANGE('options.halfLife', '>= 0', halfLife); + validateNumber(threshold, 'options.threshold'); + if (threshold < 0) + throw new ERR_OUT_OF_RANGE('options.threshold', '>= 0', threshold); + return createRecordableHistogram( + new _Histogram(lowest, highest, figures, halfLife, threshold)); } module.exports = { diff --git a/src/histogram-inl.h b/src/histogram-inl.h index 7c3545f53aad..eea0e89bef11 100644 --- a/src/histogram-inl.h +++ b/src/histogram-inl.h @@ -9,11 +9,39 @@ namespace node { +void Histogram::UpdateEwma(double value) { + // Called inside a write lock. No-op when EWMA is disabled. + if (ewma_alpha_ <= 0) return; + if (!ewma_initialized_) { + ewma_mean_ = value; + ewma_variance_ = 0; + ewma_initialized_ = true; + if (threshold_ > 0) { + ewma_error_rate_ = (value > static_cast(threshold_)) ? 1.0 : 0.0; + } + return; + } + double diff = value - ewma_mean_; + ewma_mean_ += ewma_alpha_ * diff; + ewma_variance_ = + (1.0 - ewma_alpha_) * (ewma_variance_ + ewma_alpha_ * diff * diff); + + // Binary EWMA for SLO error rate: feed 1 if over threshold, 0 otherwise. + if (threshold_ > 0) { + double exceeded = (value > static_cast(threshold_)) ? 1.0 : 0.0; + ewma_error_rate_ += ewma_alpha_ * (exceeded - ewma_error_rate_); + } +} + void Histogram::Reset() { RwLock::ScopedWriteLock lock(mutex_); hdr_reset(histogram_.get()); exceeds_ = 0; prev_ = 0; + ewma_mean_ = 0; + ewma_variance_ = 0; + ewma_error_rate_ = 0; + ewma_initialized_ = false; } double Histogram::Add(const Histogram& other) { @@ -74,6 +102,21 @@ double Histogram::Stddev() const { return hdr_stddev(histogram_.get()); } +double Histogram::EwmaMean() const { + RwLock::ScopedReadLock lock(mutex_); + return ewma_initialized_ ? ewma_mean_ : 0; +} + +double Histogram::EwmaStddev() const { + RwLock::ScopedReadLock lock(mutex_); + return ewma_initialized_ ? std::sqrt(ewma_variance_) : 0; +} + +double Histogram::EwmaErrorRate() const { + RwLock::ScopedReadLock lock(mutex_); + return ewma_initialized_ ? ewma_error_rate_ : 0; +} + int64_t Histogram::Percentile(double percentile) const { RwLock::ScopedReadLock lock(mutex_); CHECK_GT(percentile, 0); @@ -101,14 +144,20 @@ bool Histogram::RecordCorrected(int64_t value, int64_t expected_interval) { RwLock::ScopedWriteLock lock(mutex_); bool recorded = hdr_record_corrected_value(histogram_.get(), value, expected_interval); - if (!recorded) exceeds_++; + if (!recorded) + exceeds_++; + else + UpdateEwma(static_cast(value)); return recorded; } bool Histogram::Record(int64_t value) { RwLock::ScopedWriteLock lock(mutex_); bool recorded = hdr_record_value(histogram_.get(), value); - if (!recorded) exceeds_++; + if (!recorded) + exceeds_++; + else + UpdateEwma(static_cast(value)); return recorded; } @@ -119,7 +168,10 @@ uint64_t Histogram::RecordDelta() { if (prev_ > 0) { CHECK_GE(time, prev_); delta = time - prev_; - if (!hdr_record_value(histogram_.get(), delta)) exceeds_++; + if (!hdr_record_value(histogram_.get(), delta)) + exceeds_++; + else + UpdateEwma(static_cast(delta)); } prev_ = time; return delta; diff --git a/src/histogram.cc b/src/histogram.cc index 2d639a203501..7127bb80cc8f 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -6,14 +6,19 @@ #include "node_errors.h" #include "node_external_reference.h" #include "util.h" +#include "v8-typed-array.h" +#include +#include #include namespace node { +using v8::Array; using v8::BigInt; using v8::CFunction; using v8::Context; +using v8::Float64Array; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::Integer; @@ -48,6 +53,12 @@ Histogram::Histogram(const Options& options) { options.figures, &histogram)); histogram_.reset(histogram); + + // alpha = 1 - 2^(-1/halfLife). With halfLife <= 0, EWMA is disabled. + if (options.half_life > 0) { + ewma_alpha_ = 1.0 - std::exp(-std::log(2.0) / options.half_life); + } + threshold_ = options.threshold; } void Histogram::MemoryInfo(MemoryTracker* tracker) const { @@ -217,6 +228,368 @@ void Histogram::PercentilesAt(const double* percentiles, hdr_value_at_percentiles(histogram_.get(), percentiles, values, length); } +namespace { +// Continued fraction evaluation for the regularized incomplete beta +// function using Lentz's modified method. Reference: Numerical Recipes +// in C, 2nd edition, section 6.4. +static double BetaContinuedFraction(double a, double b, double x) { + constexpr double FPMIN = 1e-30; + constexpr int MAXIT = 200; + constexpr double EPS = 3e-12; + + double qab = a + b; + double qap = a + 1.0; + double qam = a - 1.0; + double c = 1.0; + double d = 1.0 - qab * x / qap; + if (std::fabs(d) < FPMIN) d = FPMIN; + d = 1.0 / d; + double h = d; + + for (int m = 1; m <= MAXIT; m++) { + int m2 = 2 * m; + // Even step. + double aa = m * (b - m) * x / ((qam + m2) * (a + m2)); + d = 1.0 + aa * d; + if (std::fabs(d) < FPMIN) d = FPMIN; + c = 1.0 + aa / c; + if (std::fabs(c) < FPMIN) c = FPMIN; + d = 1.0 / d; + h *= d * c; + // Odd step. + aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2)); + d = 1.0 + aa * d; + if (std::fabs(d) < FPMIN) d = FPMIN; + c = 1.0 + aa / c; + if (std::fabs(c) < FPMIN) c = FPMIN; + d = 1.0 / d; + double del = d * c; + h *= del; + if (std::fabs(del - 1.0) <= EPS) break; + } + return h; +} + +// Regularized incomplete beta function I_x(a, b). +// Returns the probability that a Beta(a,b) random variable is <= x. +static double RegularizedIncompleteBeta(double a, double b, double x) { + if (x <= 0.0) return 0.0; + if (x >= 1.0) return 1.0; + + double ln_front = std::lgamma(a + b) - std::lgamma(a) - std::lgamma(b) + + a * std::log(x) + b * std::log(1.0 - x); + double bt = std::exp(ln_front); + + // Use the symmetry relation to ensure the continued fraction + // converges in the region where it is most accurate. + if (x < (a + 1.0) / (a + b + 2.0)) { + return bt * BetaContinuedFraction(a, b, x) / a; + } + return 1.0 - bt * BetaContinuedFraction(b, a, 1.0 - x) / b; +} + +// Standard normal CDF: Phi(x) = P(Z <= x). +static double NormalCdf(double x) { + return 0.5 * std::erfc(-x * std::numbers::sqrt2 / 2.0); +} + +// Student's t-distribution CDF: P(T <= t) for df degrees of freedom. +static double StudentTCdf(double t, double df) { + double x = df / (df + t * t); + double ibeta = RegularizedIncompleteBeta(df / 2.0, 0.5, x); + if (t >= 0.0) { + return 1.0 - 0.5 * ibeta; + } + return 0.5 * ibeta; +} + +// Student's t-distribution quantile (inverse CDF) using bisection. +// Returns the value t such that P(T <= t) = p. +static double StudentTQuantile(double p, double df) { + if (p <= 0.0) return -std::numeric_limits::infinity(); + if (p >= 1.0) return std::numeric_limits::infinity(); + if (p == 0.5) return 0.0; + + // Bisection search. The range [-1e6, 1e6] is sufficient for any + // practical confidence level and degrees of freedom. + double lo = -1e6; + double hi = 1e6; + for (int i = 0; i < 100; i++) { + double mid = (lo + hi) / 2.0; + if (StudentTCdf(mid, df) < p) { + lo = mid; + } else { + hi = mid; + } + } + return (lo + hi) / 2.0; +} + +// Binomial CDF: P(X <= k) for X ~ Binomial(n, p). +// Uses the identity P(X <= k) = I_{1-p}(n-k, k+1). +static double BinomialCdf(int64_t k, int64_t n, double p) { + if (k < 0) return 0.0; + if (k >= n) return 1.0; + return RegularizedIncompleteBeta( + static_cast(n - k), static_cast(k + 1), 1.0 - p); +} +} // namespace + +Histogram::WelchTestResult Histogram::WelchTest(const Histogram& other, + double confidence) const { + auto do_welch = [&]() -> WelchTestResult { + int64_t n1 = histogram_->total_count; + int64_t n2 = other.histogram_->total_count; + if (n1 < 2 || n2 < 2) return {0, 0, 1, 0, 0}; + + double mean1 = hdr_mean(histogram_.get()); + double mean2 = hdr_mean(other.histogram_.get()); + double sd1 = hdr_stddev(histogram_.get()); + double sd2 = hdr_stddev(other.histogram_.get()); + + // HdrHistogram computes population stddev (divides by N). + // Welch's t-test requires sample variance (divides by N-1). + double var1 = + sd1 * sd1 * static_cast(n1) / static_cast(n1 - 1); + double var2 = + sd2 * sd2 * static_cast(n2) / static_cast(n2 - 1); + + double se1 = var1 / static_cast(n1); + double se2 = var2 / static_cast(n2); + double se_sum = se1 + se2; + if (se_sum == 0.0) return {0, 0, 1, 0, 0}; + + double t = (mean1 - mean2) / std::sqrt(se_sum); + + // Welch-Satterthwaite degrees of freedom. + double df = (se_sum * se_sum) / (se1 * se1 / static_cast(n1 - 1) + + se2 * se2 / static_cast(n2 - 1)); + + // Two-tailed p-value. + double p = 2.0 * StudentTCdf(-std::fabs(t), df); + + // Confidence interval on the difference of means. + double alpha = 1.0 - confidence; + double t_crit = StudentTQuantile(1.0 - alpha / 2.0, df); + double margin = t_crit * std::sqrt(se_sum); + double diff = mean1 - mean2; + + return {t, df, p, diff - margin, diff + margin}; + }; + + if (this == &other) return {0, 0, 1, 0, 0}; + + if (this < &other) { + RwLock::ScopedReadLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_welch(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedReadLock lock2(mutex_); + return do_welch(); +} + +Histogram::MannWhitneyResult Histogram::MannWhitneyTest( + const Histogram& other) const { + auto do_mw = [&]() -> MannWhitneyResult { + int64_t n1 = histogram_->total_count; + int64_t n2 = other.histogram_->total_count; + if (n1 == 0 || n2 == 0) return {0, 0, 1}; + + // Walk the counts arrays to compute the U statistic. + // At each bucket index, values from histogram 1 at index i "beat" + // all values from histogram 2 at indices < i (concordant pairs). + int32_t len = + std::max(histogram_->counts_len, other.histogram_->counts_len); + + // Forward pass: count concordant pairs (h1 values > h2 values). + int64_t cum2 = 0; + double concordant = 0.0; + double tied = 0.0; + for (int32_t i = 0; i < len; i++) { + int64_t c1 = (i < histogram_->counts_len) ? histogram_->counts[i] : 0; + int64_t c2 = + (i < other.histogram_->counts_len) ? other.histogram_->counts[i] : 0; + concordant += static_cast(c1) * static_cast(cum2); + tied += static_cast(c1) * static_cast(c2); + cum2 += c2; + } + + // U statistic for sample 1: concordant + half of ties. + double u = concordant + 0.5 * tied; + double dn1 = static_cast(n1); + double dn2 = static_cast(n2); + double mu = dn1 * dn2 / 2.0; + + // Tie correction for the variance. + // sigma^2 = n1*n2/12 * (N+1 - sum(t_k^3 - t_k) / (N*(N-1))) + // where t_k is the number of observations tied at rank k. + double n_total = dn1 + dn2; + double tie_correction = 0.0; + for (int32_t i = 0; i < len; i++) { + int64_t c1 = (i < histogram_->counts_len) ? histogram_->counts[i] : 0; + int64_t c2 = + (i < other.histogram_->counts_len) ? other.histogram_->counts[i] : 0; + double tk = static_cast(c1 + c2); + if (tk > 1) { + tie_correction += tk * tk * tk - tk; + } + } + + double sigma_sq = + (dn1 * dn2 / 12.0) * + (n_total + 1.0 - tie_correction / (n_total * (n_total - 1.0))); + if (sigma_sq <= 0.0) return {u, 0, 1}; + + // Continuity-corrected z-score. + double z = (u - mu) / std::sqrt(sigma_sq); + // Two-tailed p-value using normal approximation. + double p = 2.0 * NormalCdf(-std::fabs(z)); + + return {u, z, p}; + }; + + if (this == &other) return {0, 0, 1}; + + if (this < &other) { + RwLock::ScopedReadLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_mw(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedReadLock lock2(mutex_); + return do_mw(); +} + +double Histogram::CohensD(const Histogram& other) const { + auto do_cohens = [&]() -> double { + int64_t n1 = histogram_->total_count; + int64_t n2 = other.histogram_->total_count; + if (n1 < 2 || n2 < 2) return 0.0; + + double mean1 = hdr_mean(histogram_.get()); + double mean2 = hdr_mean(other.histogram_.get()); + double sd1 = hdr_stddev(histogram_.get()); + double sd2 = hdr_stddev(other.histogram_.get()); + + // Convert population variance to sample variance (Bessel's correction). + double var1 = + sd1 * sd1 * static_cast(n1) / static_cast(n1 - 1); + double var2 = + sd2 * sd2 * static_cast(n2) / static_cast(n2 - 1); + + double pooled_sd = std::sqrt((static_cast(n1 - 1) * var1 + + static_cast(n2 - 1) * var2) / + static_cast(n1 + n2 - 2)); + if (pooled_sd == 0.0) return 0.0; + + return (mean1 - mean2) / pooled_sd; + }; + + if (this == &other) return 0.0; + + if (this < &other) { + RwLock::ScopedReadLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_cohens(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedReadLock lock2(mutex_); + return do_cohens(); +} + +double Histogram::CliffsD(const Histogram& other) const { + auto do_cliffs = [&]() -> double { + int64_t n1 = histogram_->total_count; + int64_t n2 = other.histogram_->total_count; + if (n1 == 0 || n2 == 0) return 0.0; + + int32_t len = + std::max(histogram_->counts_len, other.histogram_->counts_len); + + // Forward pass: count pairs where h1 value > h2 value (concordant). + int64_t cum2 = 0; + double concordant = 0.0; + double tied = 0.0; + for (int32_t i = 0; i < len; i++) { + int64_t c1 = (i < histogram_->counts_len) ? histogram_->counts[i] : 0; + int64_t c2 = + (i < other.histogram_->counts_len) ? other.histogram_->counts[i] : 0; + concordant += static_cast(c1) * static_cast(cum2); + tied += static_cast(c1) * static_cast(c2); + cum2 += c2; + } + + double discordant = + static_cast(n1) * static_cast(n2) - concordant - tied; + + return (concordant - discordant) / + (static_cast(n1) * static_cast(n2)); + }; + + if (this == &other) return 0.0; + + if (this < &other) { + RwLock::ScopedReadLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_cliffs(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedReadLock lock2(mutex_); + return do_cliffs(); +} + +Histogram::PercentileCIResult Histogram::PercentileCI(double percentile, + double confidence) const { + RwLock::ScopedReadLock lock(mutex_); + + int64_t value = hdr_value_at_percentile(histogram_.get(), percentile); + int64_t n = histogram_->total_count; + + if (n < 2) { + return {value, value, value}; + } + + double p = percentile / 100.0; + double alpha = 1.0 - confidence; + + // Lower rank: largest j such that BinomialCdf(j-1, n, p) <= alpha/2. + // Binary search over [0, n]. + int64_t lo = 0; + int64_t hi = n; + while (lo < hi) { + int64_t mid = lo + (hi - lo + 1) / 2; + if (BinomialCdf(mid - 1, n, p) <= alpha / 2.0) { + lo = mid; + } else { + hi = mid - 1; + } + } + double lower_pct = static_cast(lo) / static_cast(n) * 100.0; + + // Upper rank: smallest k such that BinomialCdf(k-1, n, p) >= 1 - alpha/2. + lo = 0; + hi = n; + while (lo < hi) { + int64_t mid = lo + (hi - lo) / 2; + if (BinomialCdf(mid - 1, n, p) >= 1.0 - alpha / 2.0) { + hi = mid; + } else { + lo = mid + 1; + } + } + double upper_pct = static_cast(lo) / static_cast(n) * 100.0; + + int64_t lower_val = hdr_value_at_percentile(histogram_.get(), lower_pct); + int64_t upper_val = hdr_value_at_percentile(histogram_.get(), upper_pct); + + return {value, lower_val, upper_val}; +} + HistogramImpl::HistogramImpl(const Histogram::Options& options) : histogram_(new Histogram(options)) {} @@ -247,6 +620,12 @@ CFunction HistogramImpl::fast_get_cdf_( CFunction::Make(&HistogramImpl::FastGetCdf)); CFunction HistogramImpl::fast_get_count_at_( CFunction::Make(&HistogramImpl::FastGetCountAt)); +CFunction HistogramImpl::fast_get_ewma_mean_( + CFunction::Make(&HistogramImpl::FastGetEwmaMean)); +CFunction HistogramImpl::fast_get_ewma_stddev_( + CFunction::Make(&HistogramImpl::FastGetEwmaStddev)); +CFunction HistogramImpl::fast_get_ewma_error_rate_( + CFunction::Make(&HistogramImpl::FastGetEwmaErrorRate)); CFunction HistogramBase::fast_record_( CFunction::Make(&HistogramBase::FastRecord)); CFunction HistogramBase::fast_record_delta_( @@ -296,6 +675,21 @@ void HistogramImpl::AddMethods(Isolate* isolate, Local tmpl) { SetProtoMethodNoSideEffect(isolate, tmpl, "percentilesAt", GetPercentilesAt); SetProtoMethodNoSideEffect(isolate, tmpl, "linearBuckets", GetLinearBuckets); SetProtoMethodNoSideEffect(isolate, tmpl, "logBuckets", GetLogBuckets); + SetProtoMethodNoSideEffect(isolate, tmpl, "welchTest", GetWelchTest); + SetProtoMethodNoSideEffect( + isolate, tmpl, "mannWhitneyTest", GetMannWhitneyTest); + SetProtoMethodNoSideEffect(isolate, tmpl, "cohensD", GetCohensD); + SetProtoMethodNoSideEffect(isolate, tmpl, "cliffsD", GetCliffsD); + SetProtoMethodNoSideEffect(isolate, tmpl, "percentileCI", GetPercentileCI); + SetFastMethodNoSideEffect( + isolate, instance, "ewmaMean", GetEwmaMean, &fast_get_ewma_mean_); + SetFastMethodNoSideEffect( + isolate, instance, "ewmaStddev", GetEwmaStddev, &fast_get_ewma_stddev_); + SetFastMethodNoSideEffect(isolate, + instance, + "ewmaErrorRate", + GetEwmaErrorRate, + &fast_get_ewma_error_rate_); SetFastMethod(isolate, instance, "reset", DoReset, &fast_reset_); } @@ -334,6 +728,17 @@ void HistogramImpl::RegisterExternalReferences( registry->Register(GetPercentilesAt); registry->Register(GetLinearBuckets); registry->Register(GetLogBuckets); + registry->Register(GetWelchTest); + registry->Register(GetMannWhitneyTest); + registry->Register(GetCohensD); + registry->Register(GetCliffsD); + registry->Register(GetPercentileCI); + registry->Register(GetEwmaMean); + registry->Register(GetEwmaStddev); + registry->Register(GetEwmaErrorRate); + registry->Register(fast_get_ewma_mean_); + registry->Register(fast_get_ewma_stddev_); + registry->Register(fast_get_ewma_error_rate_); registry->Register(fast_get_skewness_); registry->Register(fast_get_kurtosis_); registry->Register(fast_get_cdf_); @@ -507,9 +912,18 @@ void HistogramBase::New(const FunctionCallbackInfo& args) { } int32_t figures = args[2].As()->Value(); - new HistogramBase(env, args.This(), Histogram::Options { - lowest, highest, figures - }); + double half_life = 0; + if (args.Length() > 3 && args[3]->IsNumber()) { + half_life = args[3].As()->Value(); + } + int64_t threshold = 0; + if (args.Length() > 4 && args[4]->IsNumber()) { + threshold = static_cast(args[4].As()->Value()); + } + new HistogramBase( + env, + args.This(), + Histogram::Options{lowest, highest, figures, half_life, threshold}); } Local HistogramBase::GetConstructorTemplate( @@ -1014,13 +1428,109 @@ void HistogramImpl::GetKsTest(const FunctionCallbackInfo& args) { args.GetReturnValue().Set((*histogram)->KsTest(*(other->histogram()))); } +void HistogramImpl::GetWelchTest(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + HistogramImpl* other = HistogramImpl::FromJSObject(args[0]); + CHECK(args[1]->IsNumber()); + double confidence = args[1].As()->Value(); + + auto result = (*histogram)->WelchTest(*(other->histogram()), confidence); + + Local values[] = {Number::New(isolate, result.t_statistic), + Number::New(isolate, result.degrees_of_freedom), + Number::New(isolate, result.p_value), + Number::New(isolate, result.ci_lower), + Number::New(isolate, result.ci_upper)}; + Local arr = Array::New(isolate, &values[0], arraysize(values)); + args.GetReturnValue().Set(arr); +} + +void HistogramImpl::GetMannWhitneyTest( + const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + HistogramImpl* other = HistogramImpl::FromJSObject(args[0]); + + auto result = (*histogram)->MannWhitneyTest(*(other->histogram())); + + Local values[] = {Number::New(isolate, result.u_statistic), + Number::New(isolate, result.z_score), + Number::New(isolate, result.p_value)}; + Local arr = Array::New(isolate, &values[0], arraysize(values)); + args.GetReturnValue().Set(arr); +} + +void HistogramImpl::GetCohensD(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + HistogramImpl* other = HistogramImpl::FromJSObject(args[0]); + args.GetReturnValue().Set((*histogram)->CohensD(*(other->histogram()))); +} + +void HistogramImpl::GetCliffsD(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + HistogramImpl* other = HistogramImpl::FromJSObject(args[0]); + args.GetReturnValue().Set((*histogram)->CliffsD(*(other->histogram()))); +} + +void HistogramImpl::GetPercentileCI(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsNumber()); + double percentile = args[0].As()->Value(); + double confidence = args[1].As()->Value(); + + auto result = (*histogram)->PercentileCI(percentile, confidence); + + Local values[] = { + Number::New(isolate, static_cast(result.value)), + Number::New(isolate, static_cast(result.lower)), + Number::New(isolate, static_cast(result.upper))}; + Local arr = Array::New(isolate, &values[0], arraysize(values)); + args.GetReturnValue().Set(arr); +} + +void HistogramImpl::GetEwmaMean(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + args.GetReturnValue().Set((*histogram)->EwmaMean()); +} + +double HistogramImpl::FastGetEwmaMean(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.ewmaMean"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->EwmaMean(); +} + +void HistogramImpl::GetEwmaStddev(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + args.GetReturnValue().Set((*histogram)->EwmaStddev()); +} + +double HistogramImpl::FastGetEwmaStddev(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.ewmaStddev"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->EwmaStddev(); +} + +void HistogramImpl::GetEwmaErrorRate(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + args.GetReturnValue().Set((*histogram)->EwmaErrorRate()); +} + +double HistogramImpl::FastGetEwmaErrorRate(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.ewmaErrorRate"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->EwmaErrorRate(); +} + void HistogramImpl::GetPercentilesAt(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); CHECK(args[0]->IsMap()); Local map = args[0].As(); CHECK(args[1]->IsFloat64Array()); - Local input = args[1].As(); + Local input = args[1].As(); size_t length = input->Length(); auto backing = input->Buffer()->GetBackingStore(); double* percentiles = reinterpret_cast( diff --git a/src/histogram.h b/src/histogram.h index 5fbffa2a4879..31a2e9833d3e 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -30,6 +30,10 @@ class Histogram : public MemoryRetainer { int64_t lowest = 1; int64_t highest = std::numeric_limits::max(); int figures = kDefaultHistogramFigures; + double half_life = 0; // EWMA half-life in number of samples (0 = off) + int64_t threshold = 0; // SLO threshold (0 = off). When set with + // half_life, tracks EWMA error rate for values + // exceeding this threshold. }; explicit Histogram(const Options& options); @@ -41,6 +45,9 @@ class Histogram : public MemoryRetainer { inline int64_t Max() const; inline double Mean() const; inline double Stddev() const; + inline double EwmaMean() const; + inline double EwmaStddev() const; + inline double EwmaErrorRate() const; inline int64_t Percentile(double percentile) const; inline size_t Exceeds() const; inline size_t Count() const; @@ -67,6 +74,35 @@ class Histogram : public MemoryRetainer { int64_t* values, size_t length) const; + // Statistical hypothesis testing + struct WelchTestResult { + double t_statistic; + double degrees_of_freedom; + double p_value; + double ci_lower; + double ci_upper; + }; + + struct MannWhitneyResult { + double u_statistic; + double z_score; + double p_value; + }; + + struct PercentileCIResult { + int64_t value; + int64_t lower; + int64_t upper; + }; + + WelchTestResult WelchTest(const Histogram& other, + double confidence = 0.95) const; + MannWhitneyResult MannWhitneyTest(const Histogram& other) const; + double CohensD(const Histogram& other) const; + double CliffsD(const Histogram& other) const; + PercentileCIResult PercentileCI(double percentile, + double confidence = 0.95) const; + inline bool RecordCorrected(int64_t value, int64_t expected_interval); template @@ -82,10 +118,23 @@ class Histogram : public MemoryRetainer { SET_SELF_SIZE(Histogram) private: + inline void UpdateEwma(double value); + using HistogramPointer = DeleteFnPtr; HistogramPointer histogram_; uint64_t prev_ = 0; size_t exceeds_ = 0; + + // EWMA state (active when ewma_alpha_ > 0) + double ewma_alpha_ = 0; + double ewma_mean_ = 0; + double ewma_variance_ = 0; + bool ewma_initialized_ = false; + + // SLO error rate EWMA (active when threshold_ > 0 and ewma_alpha_ > 0) + int64_t threshold_ = 0; + double ewma_error_rate_ = 0; + RwLock mutex_; }; @@ -131,6 +180,15 @@ class HistogramImpl { static void GetPercentilesAt(const v8::FunctionCallbackInfo& args); static void GetLinearBuckets(const v8::FunctionCallbackInfo& args); static void GetLogBuckets(const v8::FunctionCallbackInfo& args); + static void GetWelchTest(const v8::FunctionCallbackInfo& args); + static void GetMannWhitneyTest( + const v8::FunctionCallbackInfo& args); + static void GetCohensD(const v8::FunctionCallbackInfo& args); + static void GetCliffsD(const v8::FunctionCallbackInfo& args); + static void GetPercentileCI(const v8::FunctionCallbackInfo& args); + static void GetEwmaMean(const v8::FunctionCallbackInfo& args); + static void GetEwmaStddev(const v8::FunctionCallbackInfo& args); + static void GetEwmaErrorRate(const v8::FunctionCallbackInfo& args); static void FastReset(v8::Local receiver); static double FastGetCount(v8::Local receiver); @@ -146,6 +204,9 @@ class HistogramImpl { static double FastGetCdf(v8::Local receiver, const int64_t value); static double FastGetCountAt(v8::Local receiver, const int64_t value); + static double FastGetEwmaMean(v8::Local receiver); + static double FastGetEwmaStddev(v8::Local receiver); + static double FastGetEwmaErrorRate(v8::Local receiver); static void AddMethods(v8::Isolate* isolate, v8::Local tmpl); @@ -169,6 +230,9 @@ class HistogramImpl { static v8::CFunction fast_get_kurtosis_; static v8::CFunction fast_get_cdf_; static v8::CFunction fast_get_count_at_; + static v8::CFunction fast_get_ewma_mean_; + static v8::CFunction fast_get_ewma_stddev_; + static v8::CFunction fast_get_ewma_error_rate_; }; class HistogramBase final : public BaseObject, public HistogramImpl { diff --git a/test/parallel/test-perf-hooks-histogram-stats.js b/test/parallel/test-perf-hooks-histogram-stats.js new file mode 100644 index 000000000000..9b05a1061682 --- /dev/null +++ b/test/parallel/test-perf-hooks-histogram-stats.js @@ -0,0 +1,561 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +require('../common'); +const assert = require('assert'); +const { createHistogram } = require('perf_hooks'); + +// --------------------------------------------------------------------------- +// welchTest(other) — Welch's t-test +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + // Both empty → p-value 1 (no evidence of difference) + const empty = h1.welchTest(h2); + assert.strictEqual(empty.pValue, 1); + assert.strictEqual(empty.tStatistic, 0); + + // Identical distributions → high p-value (not significant) + for (let i = 0; i < 100; i++) { + h1.record(50 + Math.ceil(Math.random() * 10)); + h2.record(50 + Math.ceil(Math.random() * 10)); + } + const identical = h1.welchTest(h2); + assert.strictEqual(typeof identical.tStatistic, 'number'); + assert.strictEqual(typeof identical.degreesOfFreedom, 'number'); + assert.strictEqual(typeof identical.pValue, 'number'); + assert.ok(identical.pValue >= 0 && identical.pValue <= 1); + assert.ok(identical.degreesOfFreedom > 0); + assert.strictEqual(typeof identical.confidenceInterval.lower, 'number'); + assert.strictEqual(typeof identical.confidenceInterval.upper, 'number'); + assert.ok(identical.confidenceInterval.lower <= + identical.confidenceInterval.upper); + + // Very different distributions → low p-value (significant) + const hLow = createHistogram(); + const hHigh = createHistogram(); + for (let i = 0; i < 200; i++) hLow.record(10 + Math.ceil(Math.random() * 5)); + for (let i = 0; i < 200; i++) { + hHigh.record(1000 + Math.ceil(Math.random() * 5)); + } + const different = hLow.welchTest(hHigh); + assert.ok(different.pValue < 0.001, + `Expected p < 0.001, got ${different.pValue}`); + assert.ok(different.tStatistic < 0, 'hLow mean < hHigh mean → negative t'); + + // Confidence interval should not contain 0 when significant + assert.ok(different.confidenceInterval.upper < 0 || + different.confidenceInterval.lower > 0); + + // Same histogram → p-value 1 + const self = hLow.welchTest(hLow); + assert.strictEqual(self.pValue, 1); + + // Custom confidence level + const ci90 = hLow.welchTest(hHigh, { confidence: 0.90 }); + const ci99 = hLow.welchTest(hHigh, { confidence: 0.99 }); + // 99% CI should be wider than 90% CI + const width90 = ci90.confidenceInterval.upper - + ci90.confidenceInterval.lower; + const width99 = ci99.confidenceInterval.upper - + ci99.confidenceInterval.lower; + assert.ok(width99 > width90); + + // Validation + assert.throws(() => h1.welchTest('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.welchTest(h2, { confidence: 0 }), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h1.welchTest(h2, { confidence: 1 }), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h1.welchTest(h2, { confidence: 'high' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// mannWhitneyTest(other) — Mann-Whitney U test +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + // Both empty → p-value 1 + const empty = h1.mannWhitneyTest(h2); + assert.strictEqual(empty.pValue, 1); + assert.strictEqual(empty.uStatistic, 0); + assert.strictEqual(empty.zScore, 0); + + // Very different distributions → significant + const hLow = createHistogram(); + const hHigh = createHistogram(); + for (let i = 0; i < 100; i++) hLow.record(1 + Math.ceil(Math.random() * 5)); + for (let i = 0; i < 100; i++) { + hHigh.record(1000 + Math.ceil(Math.random() * 5)); + } + const result = hLow.mannWhitneyTest(hHigh); + assert.strictEqual(typeof result.uStatistic, 'number'); + assert.strictEqual(typeof result.zScore, 'number'); + assert.strictEqual(typeof result.pValue, 'number'); + assert.ok(result.pValue < 0.001, + `Expected p < 0.001, got ${result.pValue}`); + + // Same histogram → p-value 1 + const self = hLow.mannWhitneyTest(hLow); + assert.strictEqual(self.pValue, 1); + + // Identical data → high p-value + const a = createHistogram(); + const b = createHistogram(); + for (let i = 1; i <= 50; i++) { a.record(i); b.record(i); } + const same = a.mannWhitneyTest(b); + assert.ok(same.pValue > 0.05, + `Expected p > 0.05, got ${same.pValue}`); + + // Validation + assert.throws(() => h1.mannWhitneyTest('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// cohensD(other) — Cohen's d effect size +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + // Both empty → 0 + assert.strictEqual(h1.cohensD(h2), 0); + + // Same histogram → 0 + for (let i = 1; i <= 100; i++) h1.record(i); + assert.strictEqual(h1.cohensD(h1), 0); + + // Identical distributions → near 0 + const a = createHistogram(); + const b = createHistogram(); + for (let i = 0; i < 100; i++) { + const v = 50 + Math.ceil(Math.random() * 10); + a.record(v); + b.record(v); + } + assert.ok(Math.abs(a.cohensD(b)) < 0.5); + + // Very different distributions → large |d| + const hLow = createHistogram(); + const hHigh = createHistogram(); + for (let i = 0; i < 200; i++) hLow.record(8 + Math.ceil(Math.random() * 5)); + for (let i = 0; i < 200; i++) { + hHigh.record(998 + Math.ceil(Math.random() * 5)); + } + const d = hLow.cohensD(hHigh); + assert.ok(Math.abs(d) > 1.0, + `Expected |d| > 1, got ${d}`); + // hLow has lower mean → d should be negative + assert.ok(d < 0); + + // Antisymmetry: d(a,b) = -d(b,a) + const dReverse = hHigh.cohensD(hLow); + assert.ok(Math.abs(d + dReverse) < 1e-10); + + // Uniform variance → 0 + const u1 = createHistogram(); + const u2 = createHistogram(); + for (let i = 0; i < 100; i++) u1.record(5); + for (let i = 0; i < 100; i++) u2.record(5); + assert.strictEqual(u1.cohensD(u2), 0); + + // Validation + assert.throws(() => h1.cohensD('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// cliffsD(other) — Cliff's delta +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + // Both empty → 0 + assert.strictEqual(h1.cliffsD(h2), 0); + + // Same histogram → 0 + for (let i = 1; i <= 100; i++) h1.record(i); + assert.strictEqual(h1.cliffsD(h1), 0); + + // All values in h1 > all values in h2 → delta = 1 + const hHigh = createHistogram(); + const hLow = createHistogram(); + for (let i = 0; i < 100; i++) hHigh.record(1000); + for (let i = 0; i < 100; i++) hLow.record(1); + assert.strictEqual(hHigh.cliffsD(hLow), 1); + + // All values in h1 < all values in h2 → delta = -1 + assert.strictEqual(hLow.cliffsD(hHigh), -1); + + // Antisymmetry: d(a,b) = -d(b,a) + const a = createHistogram(); + const b = createHistogram(); + for (let i = 0; i < 50; i++) a.record(1 + Math.ceil(Math.random() * 100)); + for (let i = 0; i < 50; i++) { + b.record(50 + Math.ceil(Math.random() * 100)); + } + const dAB = a.cliffsD(b); + const dBA = b.cliffsD(a); + assert.ok(Math.abs(dAB + dBA) < 1e-10); + + // Range check: -1 <= delta <= 1 + assert.ok(dAB >= -1 && dAB <= 1); + + // Identical data → 0 + const x = createHistogram(); + const y = createHistogram(); + for (let i = 1; i <= 50; i++) { x.record(i); y.record(i); } + assert.strictEqual(x.cliffsD(y), 0); + + // Validation + assert.throws(() => h1.cliffsD('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// percentileCI(percentile[, options]) — percentile confidence intervals +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // With < 2 samples, lower/upper equal value + h.record(50); + const one = h.percentileCI(99); + assert.strictEqual(one.lower, one.value); + assert.strictEqual(one.upper, one.value); + + // Fill with enough data for a meaningful CI + for (let i = 1; i <= 1000; i++) h.record(i); + const ci = h.percentileCI(50); + assert.strictEqual(typeof ci.value, 'number'); + assert.strictEqual(typeof ci.lower, 'number'); + assert.strictEqual(typeof ci.upper, 'number'); + assert.ok(ci.lower <= ci.value, `lower ${ci.lower} <= value ${ci.value}`); + assert.ok(ci.upper >= ci.value, `upper ${ci.upper} >= value ${ci.value}`); + + // 99% CI should be wider than 90% CI + const ci90 = h.percentileCI(50, { confidence: 0.90 }); + const ci99 = h.percentileCI(50, { confidence: 0.99 }); + assert.ok((ci99.upper - ci99.lower) >= (ci90.upper - ci90.lower), + '99% CI should be at least as wide as 90% CI'); + + // Extreme percentile: p99 CI + const ci99p = h.percentileCI(99); + assert.ok(ci99p.lower <= ci99p.value); + assert.ok(ci99p.upper >= ci99p.value); + + // Constant values → CI collapses to a single value + const constant = createHistogram(); + for (let i = 0; i < 100; i++) constant.record(42); + const constCI = constant.percentileCI(50); + assert.strictEqual(constCI.lower, constCI.value); + assert.strictEqual(constCI.upper, constCI.value); + + // More samples → narrower CI + const small = createHistogram(); + const large = createHistogram(); + for (let i = 1; i <= 50; i++) { small.record(i); large.record(i); } + for (let i = 1; i <= 950; i++) large.record(i % 50 + 1); + const ciSmall = small.percentileCI(50); + const ciLarge = large.percentileCI(50); + assert.ok((ciSmall.upper - ciSmall.lower) >= (ciLarge.upper - ciLarge.lower), + 'CI should narrow with more samples'); + + // Validation + assert.throws(() => h.percentileCI(0), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentileCI(101), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentileCI('fifty'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.percentileCI(50, { confidence: 0 }), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentileCI(50, { confidence: 1 }), + { code: 'ERR_OUT_OF_RANGE' }); +} + +// --------------------------------------------------------------------------- +// EWMA — exponentially weighted moving average +// --------------------------------------------------------------------------- +{ + // Without halfLife, EWMA is disabled (returns 0) + const noEwma = createHistogram(); + for (let i = 1; i <= 100; i++) noEwma.record(i); + assert.strictEqual(noEwma.ewmaMean, 0); + assert.strictEqual(noEwma.ewmaStddev, 0); + + // With halfLife, EWMA tracks the smoothed mean + const h = createHistogram({ halfLife: 10 }); + assert.strictEqual(h.ewmaMean, 0); + assert.strictEqual(h.ewmaStddev, 0); + + // First record initializes the mean + h.record(100); + assert.strictEqual(h.ewmaMean, 100); + assert.strictEqual(h.ewmaStddev, 0); + + // Record the same value repeatedly — mean should stay stable + for (let i = 0; i < 50; i++) h.record(100); + assert.ok(Math.abs(h.ewmaMean - 100) < 1, + `Expected ewmaMean near 100, got ${h.ewmaMean}`); + assert.ok(h.ewmaStddev < 1, + `Expected near-zero stddev for constant input, got ${h.ewmaStddev}`); + + // Shift to a new value — mean should move towards it + const meanBefore = h.ewmaMean; + for (let i = 0; i < 100; i++) h.record(200); + assert.ok(h.ewmaMean > meanBefore, + 'EWMA mean should increase when recording larger values'); + assert.ok(Math.abs(h.ewmaMean - 200) < 5, + `Expected ewmaMean near 200, got ${h.ewmaMean}`); + + // Stddev should be small after converging + for (let i = 0; i < 100; i++) h.record(200); + assert.ok(h.ewmaStddev < 5, + `Expected small stddev after convergence, got ${h.ewmaStddev}`); + + // Reset clears EWMA state + h.reset(); + assert.strictEqual(h.ewmaMean, 0); + assert.strictEqual(h.ewmaStddev, 0); + + // Shorter halfLife reacts faster + const fast = createHistogram({ halfLife: 2 }); + const slow = createHistogram({ halfLife: 100 }); + for (let i = 0; i < 20; i++) { fast.record(100); slow.record(100); } + for (let i = 0; i < 20; i++) { fast.record(200); slow.record(200); } + // Fast should be closer to 200 than slow + assert.ok(fast.ewmaMean > slow.ewmaMean, + `fast.ewmaMean (${fast.ewmaMean}) should be > ` + + `slow.ewmaMean (${slow.ewmaMean})`); + + // toJSON includes separate EWMA fields + const j = createHistogram({ halfLife: 10, threshold: 50 }); + j.record(50); + j.record(60); + const json = j.toJSON(); + // mean/stddev are always the histogram (non-EWMA) values + assert.strictEqual(json.mean, j.mean); + assert.strictEqual(json.stddev, j.stddev); + // EWMA fields are present and match getter values + assert.strictEqual(json.ewmaMean, j.ewmaMean); + assert.strictEqual(json.ewmaStddev, j.ewmaStddev); + assert.strictEqual(json.ewmaErrorRate, j.ewmaErrorRate); + assert.ok(json.ewmaMean > 0); + assert.ok(json.ewmaErrorRate > 0); + + // toJSON still includes EWMA fields when EWMA is not enabled (all zero) + const noEwmaJson = createHistogram(); + noEwmaJson.record(50); + noEwmaJson.record(60); + const json2 = noEwmaJson.toJSON(); + assert.strictEqual(json2.mean, noEwmaJson.mean); + assert.strictEqual(json2.stddev, noEwmaJson.stddev); + assert.strictEqual(json2.ewmaMean, 0); + assert.strictEqual(json2.ewmaStddev, 0); + assert.strictEqual(json2.ewmaErrorRate, 0); + + // Validation + assert.throws(() => createHistogram({ halfLife: -1 }), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => createHistogram({ halfLife: 'ten' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// ewmaErrorRate / burnRate — SLO error rate tracking +// --------------------------------------------------------------------------- +{ + // Without threshold, error rate is 0 + const noThreshold = createHistogram({ halfLife: 10 }); + for (let i = 0; i < 50; i++) noThreshold.record(100); + assert.strictEqual(noThreshold.ewmaErrorRate, 0); + + // Without halfLife, error rate is 0 even with threshold + const noHalfLife = createHistogram({ threshold: 50 }); + for (let i = 0; i < 50; i++) noHalfLife.record(100); + assert.strictEqual(noHalfLife.ewmaErrorRate, 0); + + // All values below threshold → error rate converges to 0 + const allGood = createHistogram({ halfLife: 10, threshold: 200 }); + for (let i = 0; i < 100; i++) allGood.record(100); + assert.ok(allGood.ewmaErrorRate < 0.01, + `Expected near-zero error rate, got ${allGood.ewmaErrorRate}`); + + // All values above threshold → error rate converges to 1 + const allBad = createHistogram({ halfLife: 10, threshold: 50 }); + for (let i = 0; i < 100; i++) allBad.record(100); + assert.ok(allBad.ewmaErrorRate > 0.99, + `Expected near-1 error rate, got ${allBad.ewmaErrorRate}`); + + // Mixed: ~50% above threshold + const mixed = createHistogram({ halfLife: 50, threshold: 50 }); + for (let i = 0; i < 500; i++) { + mixed.record(i % 2 === 0 ? 100 : 10); // Alternating above/below + } + assert.ok(mixed.ewmaErrorRate > 0.3 && mixed.ewmaErrorRate < 0.7, + `Expected ~0.5 error rate, got ${mixed.ewmaErrorRate}`); + + // burnRate calculation + const h = createHistogram({ halfLife: 10, threshold: 50 }); + for (let i = 0; i < 100; i++) h.record(100); // All exceed + // Error rate ~1.0, SLO target 0.999 → budget 0.001 → burn rate ~1000 + const rate = h.burnRate(0.999); + assert.ok(rate > 500, + `Expected high burn rate, got ${rate}`); + + // When error rate is 0, burn rate is 0 + const perfect = createHistogram({ halfLife: 10, threshold: 200 }); + for (let i = 0; i < 100; i++) perfect.record(100); + assert.ok(perfect.burnRate(0.999) < 1, + `Expected low burn rate, got ${perfect.burnRate(0.999)}`); + + // Reset clears error rate + h.reset(); + assert.strictEqual(h.ewmaErrorRate, 0); + assert.strictEqual(h.burnRate(0.999), 0); + + // burnRate validation + assert.throws(() => h.burnRate(0), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.burnRate(1), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.burnRate(NaN), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.burnRate('high'), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // createHistogram threshold validation + assert.throws(() => createHistogram({ threshold: -1 }), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => createHistogram({ threshold: 'high' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// ERR_INVALID_THIS for all new methods on wrong receiver +// --------------------------------------------------------------------------- +{ + const { Histogram } = require('internal/histogram'); + const h = createHistogram(); + for (let i = 1; i <= 10; i++) h.record(i); + const wrongThis = {}; + + const methods = [ + ['welchTest', [h]], + ['mannWhitneyTest', [h]], + ['cohensD', [h]], + ['cliffsD', [h]], + ['percentileCI', [50]], + ['burnRate', [0.999]], + ]; + + for (const [method, args] of methods) { + assert.throws( + () => Histogram.prototype[method].call(wrongThis, ...args), + { code: 'ERR_INVALID_THIS' }, + `${method} should throw ERR_INVALID_THIS`, + ); + } + + // Getter properties + const getters = ['ewmaMean', 'ewmaStddev', 'ewmaErrorRate']; + for (const getter of getters) { + const desc = Object.getOwnPropertyDescriptor(Histogram.prototype, getter); + assert.throws( + () => desc.get.call(wrongThis), + { code: 'ERR_INVALID_THIS' }, + `${getter} should throw ERR_INVALID_THIS`, + ); + } +} + +// --------------------------------------------------------------------------- +// Undefined return when kHandle is missing native methods +// --------------------------------------------------------------------------- +{ + const { + Histogram, + kHandle, + kSkipThrow, + } = require('internal/histogram'); + const h = createHistogram(); + for (let i = 1; i <= 10; i++) h.record(i); + + // Create a histogram instance with a null handle. This passes + // isHistogram() (null !== undefined) but the optional chaining + // (this[kHandle]?.method()) short-circuits to undefined. + const stub = new Histogram(kSkipThrow); + stub[kHandle] = null; + + assert.strictEqual(stub.welchTest(h), undefined); + assert.strictEqual(stub.mannWhitneyTest(h), undefined); + assert.strictEqual(stub.percentileCI(50), undefined); + assert.strictEqual(stub.burnRate(0.999), undefined); +} + +// --------------------------------------------------------------------------- +// Fast API path coverage for EWMA getters +// --------------------------------------------------------------------------- +{ + const h = createHistogram({ halfLife: 10, threshold: 50 }); + for (let i = 1; i <= 100; i++) h.record(i); + + // Call in a tight loop to trigger V8 fast-path optimization. + function readEwma(histogram, iterations) { + let mean = 0; + let stddev = 0; + let errorRate = 0; + for (let i = 0; i < iterations; i++) { + mean = histogram.ewmaMean; + stddev = histogram.ewmaStddev; + errorRate = histogram.ewmaErrorRate; + } + return { mean, stddev, errorRate }; + } + + const result = readEwma(h, 1e4); + assert.strictEqual(typeof result.mean, 'number'); + assert.ok(result.mean > 0); + assert.strictEqual(typeof result.stddev, 'number'); + assert.ok(result.stddev > 0); + assert.strictEqual(typeof result.errorRate, 'number'); + assert.ok(result.errorRate > 0); +} + +// --------------------------------------------------------------------------- +// Cross-consistency: when welchTest is significant, cohensD should +// indicate a non-trivial effect, and cliffsD should agree on direction. +// --------------------------------------------------------------------------- +{ + const baseline = createHistogram(); + const regressed = createHistogram(); + for (let i = 0; i < 500; i++) { + baseline.record(10 + Math.ceil(Math.random() * 20)); + } + for (let i = 0; i < 500; i++) { + regressed.record(50 + Math.ceil(Math.random() * 20)); + } + + const welch = baseline.welchTest(regressed); + const d = baseline.cohensD(regressed); + const cliff = baseline.cliffsD(regressed); + + // Should be highly significant + assert.ok(welch.pValue < 0.001); + // Cohen's d should indicate a large effect (|d| > 0.8) + assert.ok(Math.abs(d) > 0.8); + // Cliff's delta should indicate baseline < regressed + assert.ok(cliff < -0.5); + // All three agree on the direction + assert.ok(d < 0); // Baseline mean < regressed mean + assert.ok(welch.tStatistic < 0); +} From 6a04f7df1c67e102249353ed6d0f767726131455 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 19 Aug 2026 21:44:10 -0700 Subject: [PATCH 81/97] benchmark: add --analyze mode to compare.js Add an --analyze flag that performs statistical analysis directly after benchmarks complete, eliminating the need for R and compare.R. When --analyze is specified, compare.js collects the rate data during the run and prints a statistical summary table instead of CSV output. The table matches the format of compare.R: improvement percentage, significance stars (* p<0.05, ** p<0.01, *** p<0.001), and confidence intervals at three risk levels. Also adds a --max-regression N option that causes the compare.js to exit with 1 (error) when the `--new` is N% slower. Useful for CI use to detect regressions. Uses the histogram API's welchTest() and cohensD() methods introduced in the previous commit. Benchmark rates are scaled to integers for HdrHistogram recording; the --scale option (default 1000) controls the multiplier for precision. Usage: node benchmark/compare.js --old ./node-old --new ./node-new \ --analyze url Signed-off-by: James M Snell Assisted-by: Opencode/Opus PR-URL: https://github.com/nodejs/node/pull/65416 Reviewed-By: Matteo Collina Reviewed-By: Chengzhong Wu --- benchmark/_benchmark_progress.js | 8 +- benchmark/compare.js | 239 +++++++++++++++++- .../writing-and-running-benchmarks.md | 67 +++-- 3 files changed, 288 insertions(+), 26 deletions(-) diff --git a/benchmark/_benchmark_progress.js b/benchmark/_benchmark_progress.js index 6c925f34e682..117e86609028 100644 --- a/benchmark/_benchmark_progress.js +++ b/benchmark/_benchmark_progress.js @@ -25,9 +25,10 @@ function getTime(diff) { // A run is an item in the job queue: { binary, filename, iter } // A config is an item in the subqueue: { binary, filename, iter, configs } class BenchmarkProgress { - constructor(queue, benchmarks) { + constructor(queue, benchmarks, options = {}) { this.queue = queue; // Scheduled runs. this.benchmarks = benchmarks; // Filenames of scheduled benchmarks. + this.analyze = !!options.analyze; // stdout is not piped, but unused. this.completedRuns = 0; // Number of completed runs. this.scheduledRuns = queue.length; // Number of scheduled runs. // Time when starting to run benchmarks. @@ -107,7 +108,10 @@ class BenchmarkProgress { } updateProgress() { - if (!process.stderr.isTTY || process.stdout.isTTY) { + // Progress renders on stderr when stdout is piped (not a TTY). + // In --analyze mode, stdout is the terminal but is unused during + // the run, so treat it the same as piped. + if (!process.stderr.isTTY || (process.stdout.isTTY && !this.analyze)) { return; } readline.clearLine(process.stderr); diff --git a/benchmark/compare.js b/benchmark/compare.js index ad3084db3904..6aaaee7a9190 100644 --- a/benchmark/compare.js +++ b/benchmark/compare.js @@ -13,7 +13,8 @@ const cli = new CLI(`usage: ./node compare.js [options] [--] ... Run each benchmark in the directory many times using two different node versions. More than one directory can be specified. The output is formatted as csv, which can be processed using for - example 'compare.R'. + example 'compare.R'. Use --analyze to perform statistical analysis + directly without R. --new ./new-node-binary new node binary (required) --old ./old-node-binary old node binary (required) @@ -24,13 +25,21 @@ const cli = new CLI(`usage: ./node compare.js [options] [--] ... repeated) --set variable=value set benchmark variable (can be repeated) --no-progress don't show benchmark progress indicator + --analyze perform statistical analysis after benchmarks + complete (Welch's t-test, effect size) instead + of printing csv output + --scale 1000 rate-to-integer multiplier for histogram + precision when using --analyze (default: 1000) + --max-regression N exit with code 1 if any statistically + significant regression exceeds N% (implies + --analyze) Examples: --set CPUSET=0 Runs benchmarks on CPU core 0. --set CPUSET=0-2 Specifies that benchmarks should run on CPU cores 0 to 2. Note: The CPUSET format should match the specifications of the 'taskset' command -`, { arrayArgs: ['set', 'filter', 'exclude'], boolArgs: ['no-progress'] }); +`, { arrayArgs: ['set', 'filter', 'exclude'], boolArgs: ['no-progress', 'analyze'] }); if (!cli.optional.new || !cli.optional.old) { cli.abort(cli.usage); @@ -38,6 +47,11 @@ if (!cli.optional.new || !cli.optional.old) { const binaries = ['old', 'new']; const runs = cli.optional.runs ? parseInt(cli.optional.runs, 10) : 30; +const maxRegression = cli.optional['max-regression'] ? + parseFloat(cli.optional['max-regression']) : + 0; +const analyze = !!cli.optional.analyze || maxRegression > 0; +const scale = cli.optional.scale ? parseInt(cli.optional.scale, 10) : 1000; const benchmarks = cli.benchmarks(); if (benchmarks.length === 0) { @@ -46,6 +60,9 @@ if (benchmarks.length === 0) { return; } +// When --analyze is set, collect results for statistical analysis. +const results = analyze ? new Map() : null; + // Create queue from the benchmarks list such both node versions are tested // `runs` amount of times each. // Note: BenchmarkProgress relies on this order to estimate @@ -61,15 +78,17 @@ for (const filename of benchmarks) { } // queue.length = binary.length * runs * benchmarks.length -// Print csv header -console.log('"binary","filename","configuration","rate","time"'); +// Print csv header (unless analyzing inline). +if (!analyze) { + console.log('"binary","filename","configuration","rate","time"'); +} const kStartOfQueue = 0; const showProgress = !cli.optional['no-progress']; let progress; if (showProgress) { - progress = new BenchmarkProgress(queue, benchmarks); + progress = new BenchmarkProgress(queue, benchmarks, { analyze }); progress.startQueue(kStartOfQueue); } @@ -99,11 +118,20 @@ if (showProgress) { conf += ` ${key}=${inspect(data.conf[key])}`; } conf = conf.slice(1); - // Escape quotes (") for correct csv formatting - conf = conf.replace(/"/g, '""'); - console.log(`"${job.binary}","${job.filename}","${conf}",` + - `${data.rate},${data.time}`); + if (analyze) { + // Collect results for post-run analysis. + const name = `${job.filename} ${conf}`; + if (!results.has(name)) { + results.set(name, { old: [], new: [] }); + } + results.get(name)[job.binary].push(data.rate); + } else { + // Escape quotes (") for correct csv formatting + conf = conf.replace(/"/g, '""'); + console.log(`"${job.binary}","${job.filename}","${conf}",` + + `${data.rate},${data.time}`); + } if (showProgress) { // One item in the subqueue has been completed. progress.completeConfig(data); @@ -125,6 +153,199 @@ if (showProgress) { // If there are more benchmarks execute the next if (i + 1 < queue.length) { recursive(i + 1); + } else if (analyze) { + printAnalysis(results, scale, maxRegression); } }); })(kStartOfQueue); + +function printAnalysis(results, scale, maxRegression) { + const { createHistogram } = require('node:perf_hooks'); + + // Build per-benchmark histograms and run statistical tests. + const rows = []; + let maxNameLen = 0; + + let skipped = 0; + + for (const [name, { old: oldRates, new: newRates }] of results) { + if (oldRates.length < 2 || newRates.length < 2) { + skipped++; + continue; + } + + const hOld = createHistogram({ figures: 3 }); + const hNew = createHistogram({ figures: 3 }); + + for (const r of oldRates) hOld.record(Math.max(1, Math.round(r * scale))); + for (const r of newRates) hNew.record(Math.max(1, Math.round(r * scale))); + + const oldMean = oldRates.reduce((a, b) => a + b, 0) / oldRates.length; + const newMean = newRates.reduce((a, b) => a + b, 0) / newRates.length; + const improvement = ((newMean - oldMean) / oldMean) * 100; + + // Query the three confidence levels. The p-value and t-statistic + // are the same regardless of the confidence level, so we extract + // them from the first result. + const w95 = hOld.welchTest(hNew, { confidence: 0.95 }); + const w99 = hOld.welchTest(hNew, { confidence: 0.99 }); + const w999 = hOld.welchTest(hNew, { confidence: 0.999 }); + + // Significance stars matching compare.R convention. + let stars = ''; + if (w95.pValue < 0.001) stars = '***'; + else if (w95.pValue < 0.01) stars = ' **'; + else if (w95.pValue < 0.05) stars = ' *'; + + // Confidence intervals expressed as percentage of the old mean. + const ciPct = (w) => { + const half = + (w.confidenceInterval.upper - w.confidenceInterval.lower) / 2; + return (half / (oldMean * scale)) * 100; + }; + + rows.push({ + name, + stars, + improvement, + ci95: ciPct(w95), + ci99: ciPct(w99), + ci999: ciPct(w999), + pValue: w95.pValue, + }); + + if (name.length > maxNameLen) maxNameLen = name.length; + } + + // Print header. + const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.length)); + const rpad = (s, n) => ' '.repeat(Math.max(0, n - s.length)) + s; + + console.log(`${pad('', maxNameLen)} confidence` + + ` improvement accuracy (*) (**) (***)`); + + for (const row of rows) { + const imp = `${row.improvement >= 0 ? '+' : ''}${row.improvement.toFixed(2)} %`; + console.log( + `${pad(row.name, maxNameLen)} ${pad(row.stars, 10)}` + + ` ${rpad(imp, 11)}` + + ` ±${row.ci95.toFixed(2)}%` + + ` ±${row.ci99.toFixed(2)}%` + + ` ±${row.ci999.toFixed(2)}%`, + ); + } + + if (skipped > 0) { + console.log(''); + console.log( + `Note: ${skipped} configuration${skipped === 1 ? ' was' : 's were'}` + + ` skipped because Welch's t-test requires at least 2 samples per` + + ` binary. Use --runs 2 or higher.`, + ); + } + + // --- Bar chart visualization --- + printChart(rows, maxNameLen); + + console.log(''); + console.log( + `Rates were scaled by ${scale}x into HdrHistogram (3 significant figures).\n` + + `Use --scale to adjust precision if needed.\n`, + ); + console.log( + `Be aware that when doing many comparisons the risk of a false-positive\n` + + `result increases. In this case, there are ${rows.length} comparisons, ` + + `you can thus\nexpect the following amount of false-positive results:\n` + + ` ${(rows.length * 0.05).toFixed(2)} false positives, when considering ` + + `a 5% risk acceptance (*, **, ***),\n` + + ` ${(rows.length * 0.01).toFixed(2)} false positives, when considering ` + + `a 1% risk acceptance (**, ***),\n` + + ` ${(rows.length * 0.001).toFixed(2)} false positives, when considering ` + + `a 0.1% risk acceptance (***)`, + ); + + // Gate: exit with error if any significant regression exceeds the limit. + if (maxRegression > 0) { + const failures = rows.filter( + (r) => r.stars.trim() !== '' && r.improvement < -maxRegression, + ); + if (failures.length > 0) { + console.log(''); + console.log( + `FAIL: ${failures.length} benchmark${failures.length === 1 ? '' : 's'}` + + ` showed a statistically significant regression exceeding` + + ` ${maxRegression}%:`, + ); + for (const f of failures) { + console.log(` ${f.name} ${f.improvement.toFixed(2)}%`); + } + process.exitCode = 1; + } + } +} + +function printChart(rows, maxNameLen) { + if (rows.length === 0) return; + + // Determine the chart scale from the data. The bar region covers + // the range [-maxAbs, +maxAbs] so the zero line sits in the center. + const barWidth = 40; + const halfWidth = barWidth / 2; + let maxAbs = 0; + for (const row of rows) { + const extent = Math.abs(row.improvement) + row.ci95; + if (extent > maxAbs) maxAbs = extent; + } + if (maxAbs === 0) maxAbs = 1; + + const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.length)); + + // Scale axis labels. + const axisLeft = `-${maxAbs.toFixed(1)}%`; + const axisRight = `+${maxAbs.toFixed(1)}%`; + const axisCenter = '0%'; + + // Print axis header. + const labelPad = maxNameLen + 5; + const leftLabel = ' '.repeat(labelPad) + + axisLeft + + ' '.repeat(Math.max(0, halfWidth - axisLeft.length - Math.floor(axisCenter.length / 2))) + + axisCenter + + ' '.repeat(Math.max(0, halfWidth - Math.ceil(axisCenter.length / 2) - axisRight.length)) + + axisRight; + console.log(''); + console.log(leftLabel); + + for (const row of rows) { + const imp = row.improvement; + const ci = row.ci95; + + // Position of the improvement value in the bar region [0, barWidth]. + const center = halfWidth; + const impPos = center + (imp / maxAbs) * halfWidth; + + // CI extent in bar positions. + const ciLeft = center + ((imp - ci) / maxAbs) * halfWidth; + const ciRight = center + ((imp + ci) / maxAbs) * halfWidth; + + // Build the bar character by character. + const chars = []; + for (let x = 0; x < barWidth; x++) { + const pos = x + 0.5; // Center of this character cell. + if (x === Math.floor(center)) { + chars.push('|'); + } else if ((imp >= 0 && pos > center && pos <= impPos) || + (imp < 0 && pos < center && pos >= impPos)) { + chars.push(row.stars ? '\u2588' : '\u2593'); // solid or dark shade + } else if (pos >= ciLeft && pos <= ciRight) { + chars.push('\u2591'); // Light shade for CI region + } else { + chars.push(' '); + } + } + + const label = `${row.improvement >= 0 ? '+' : ''}${row.improvement.toFixed(2)}%`; + const sig = row.stars.trim(); + console.log(`${pad(row.name, maxNameLen)} ${chars.join('')} ${label} ${sig}`); + } +} diff --git a/doc/contributing/writing-and-running-benchmarks.md b/doc/contributing/writing-and-running-benchmarks.md index 014c2977406c..b1b7cc95fa65 100644 --- a/doc/contributing/writing-and-running-benchmarks.md +++ b/doc/contributing/writing-and-running-benchmarks.md @@ -14,6 +14,8 @@ * [Specifying CPU Cores for Benchmarks with run.js](#specifying-cpu-cores-for-benchmarks-with-runjs) * [Filtering benchmarks](#filtering-benchmarks) * [Comparing Node.js versions](#comparing-nodejs-versions) + * [Using `--analyze` (no external tools needed)](#using---analyze-no-external-tools-needed) + * [Using R scripts or node-benchmark-compare](#using-r-scripts-or-node-benchmark-compare) * [Comparing parameters](#comparing-parameters) * [Running benchmarks on the CI](#running-benchmarks-on-the-ci) * [Creating a benchmark](#creating-a-benchmark) @@ -69,18 +71,27 @@ node benchmark/http2/simple.js benchmarker=h2load ### Benchmark analysis requirements -To analyze the results statistically, you can use either the -[node-benchmark-compare][] tool or the R script `benchmark/compare.R`. +To analyze the results statistically, there are three options: -[node-benchmark-compare][] is a Node.js script that can be installed with -`npm install -g node-benchmark-compare`. +* **`--analyze` flag** (built-in, no dependencies): Pass `--analyze` to + `benchmark/compare.js` to perform Welch's t-test directly after the + benchmarks complete. This uses the histogram API's statistical testing + methods and requires no external tools. +* **R scripts** (`benchmark/compare.R`, `benchmark/bar.R`): Perform the same + Welch's t-test analysis as `--analyze`, with the additional ability to + generate plots. Requires R with the `ggplot2` and `plyr` packages. +* **[node-benchmark-compare][]** (legacy): A Node.js script that can be + installed with `npm install -g node-benchmark-compare`. It reads the CSV + output of `benchmark/compare.js`. Predates the built-in `--analyze` flag + and is no longer necessary for most workflows. -To draw comparison plots when analyzing the results, `R` must be installed. -Use one of the available package managers or download it from -. +For most use cases, `--analyze` is the simplest option since it requires +nothing beyond Node.js itself. -The R packages `ggplot2` and `plyr` are also used and can be installed using -the R REPL. +To install R for plot generation, use one of the available package managers or +download it from . + +The R packages `ggplot2` and `plyr` can be installed using the R REPL. ```console $ R @@ -399,16 +410,38 @@ module, you can use the `--filter` option:_ repeated) --set variable=value set benchmark variable (can be repeated) --no-progress don't show benchmark progress indicator + --analyze perform statistical analysis inline (no R needed) + --scale 1000 rate multiplier for --analyze precision + --max-regression N exit with code 1 if any significant regression + exceeds N% (implies --analyze) +``` + +#### Using `--analyze` (no external tools needed) + +The simplest way to get statistical results is to pass `--analyze`: + +```bash +node benchmark/compare.js --old ./node-main --new ./node-pr-5134 --analyze string_decoder +``` - Examples: - --set CPUSET=0 Runs benchmarks on CPU core 0. - --set CPUSET=0-2 Specifies that benchmarks should run on CPU cores 0 to 2. +This runs the benchmarks and prints the analysis directly: - Note: The CPUSET format should match the specifications of the 'taskset' command +```console + confidence improvement accuracy (*) (**) (***) +string_decoder/string-decoder.js n=2500000 chunkLen=16 inLen=128 encoding='ascii' *** -3.76 % ±1.36% ±1.82% ±2.40% +string_decoder/string-decoder.js n=2500000 chunkLen=16 inLen=128 encoding='utf8' ** -0.81 % ±0.53% ±0.71% ±0.93% +... ``` -For analyzing the benchmark results, use [node-benchmark-compare][] or the R -scripts: +The `--analyze` mode uses the histogram API's `welchTest()` method to perform +the same Welch's t-test that the R script uses. Benchmark rates are scaled to +integers for the histogram (controlled by `--scale`, default 1000). With the +default settings, results are identical to the R script at two decimal places. + +#### Using R scripts or node-benchmark-compare + +Alternatively, save the CSV output and analyze it separately using +[node-benchmark-compare][] or the R scripts: * `benchmark/compare.R` * `benchmark/bar.R` @@ -424,6 +457,10 @@ $ node-benchmark-compare compare-pr-5134.csv # or cat compare-pr-5134.csv | Rscr ... ``` +The R approach is still useful when you need to generate plots (box plots via +`compare.R --plot`, scatter plots via `scatter.R --plot`) or when you want to +analyze previously saved CSV files. + In the output, _improvement_ is the relative improvement of the new version, hopefully this is positive. _confidence_ tells if there is enough statistical evidence to validate the _improvement_. If there is enough evidence From 0212fe0a591577e9f8128bcb3f8b52783eeb5eb2 Mon Sep 17 00:00:00 2001 From: Ilyas Shabi Date: Sun, 23 Aug 2026 22:44:40 +0200 Subject: [PATCH 82/97] src: fix heap value deduplication in embedder graph Signed-off-by: ishabi PR-URL: https://github.com/nodejs/node/pull/64801 Reviewed-By: James M Snell Reviewed-By: Chengzhong Wu --- src/heap_utils.cc | 40 ++++++++++++----------- test/parallel/test-heap-embedder-graph.js | 23 +++++++++++++ 2 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 test/parallel/test-heap-embedder-graph.js diff --git a/src/heap_utils.cc b/src/heap_utils.cc index e52685546a7a..72c1a73aa6c0 100644 --- a/src/heap_utils.cc +++ b/src/heap_utils.cc @@ -57,19 +57,13 @@ class JSGraphJSNode : public EmbedderGraph::Node { CHECK(!val.IsEmpty()); } - struct Equal { - inline bool operator()(JSGraphJSNode* a, JSGraphJSNode* b) const { - Local data_a = a->V8Value(); - Local data_b = a->V8Value(); - if (data_a->IsValue()) { - if (!data_b->IsValue()) { - return false; - } - return data_a.As()->SameValue(data_b.As()); - } - return data_a == data_b; + bool IsSame(Local other) { + Local value = V8Value(); + if (value->IsValue() && other->IsValue()) { + return value.As()->SameValue(other.As()); } - }; + return value == other; + } private: Global persistent_; @@ -80,12 +74,15 @@ class JSGraph : public EmbedderGraph { explicit JSGraph(Isolate* isolate) : isolate_(isolate) {} Node* V8Node(const Local& value) override { - std::unique_ptr n { new JSGraphJSNode(isolate_, value) }; - auto it = engine_nodes_.find(n.get()); - if (it != engine_nodes_.end()) - return *it; - engine_nodes_.insert(n.get()); - return AddNode(std::unique_ptr(n.release())); + for (JSGraphJSNode* node : engine_nodes_) { + if (node->IsSame(value)) { + return node; + } + } + + auto node = std::make_unique(isolate_, value); + engine_nodes_.push_back(node.get()); + return AddNode(std::move(node)); } Node* V8Node(const Local& value) override { @@ -207,7 +204,7 @@ class JSGraph : public EmbedderGraph { private: Isolate* isolate_; std::unordered_set> nodes_; - std::set engine_nodes_; + std::vector engine_nodes_; std::unordered_map>> edges_; }; @@ -215,6 +212,11 @@ void BuildEmbedderGraph(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); JSGraph graph(env->isolate()); Environment::BuildEmbedderGraph(env->isolate(), &graph, env); + // This binding is used only by tests. Include supplied values so tests can + // verify that JSGraph returns one graph node for each distinct V8 value. + for (int i = 0; i < args.Length(); i++) { + graph.V8Node(args[i]); + } Local ret; if (graph.CreateObject().ToLocal(&ret)) args.GetReturnValue().Set(ret); diff --git a/test/parallel/test-heap-embedder-graph.js b/test/parallel/test-heap-embedder-graph.js new file mode 100644 index 000000000000..1873fd1a49ca --- /dev/null +++ b/test/parallel/test-heap-embedder-graph.js @@ -0,0 +1,23 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const assert = require('assert'); +const { internalBinding } = require('internal/test/binding'); + +const { buildEmbedderGraph } = internalBinding('heap_utils'); + +const first = {}; +const second = {}; +const bigint = BigInt('123456789012345678901234567890'); +const sameBigint = BigInt('123456789012345678901234567890'); +const graph = buildEmbedderGraph(first, first, second, bigint, sameBigint); + +function findNodes(value) { + return graph.filter((node) => Object.hasOwn(node, 'value') && + Object.is(node.value, value)); +} + +assert.strictEqual(findNodes(first).length, 1); +assert.strictEqual(findNodes(second).length, 1); +assert.strictEqual(findNodes(bigint).length, 1); From 02acadbf08e1c2c547fefa727c3f9cb818dfd253 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:43:31 -0700 Subject: [PATCH 83/97] stream: prevent share from eagerly draining source Wait for buffer space after drop-newest discards an upstream result. This keeps one consumer pull from draining the source or looping indefinitely while a slower consumer keeps the buffer full. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/65338 Fixes: https://github.com/nodejs/node/issues/65337 Reviewed-By: James M Snell --- lib/internal/streams/iter/share.js | 14 +++++ test/parallel/test-stream-iter-share-from.js | 62 ++++++++++++-------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/lib/internal/streams/iter/share.js b/lib/internal/streams/iter/share.js index 662e57a7df55..711abeb21b9a 100644 --- a/lib/internal/streams/iter/share.js +++ b/lib/internal/streams/iter/share.js @@ -199,6 +199,9 @@ class ShareImpl { } await self.#pullFromSource(!shouldBuffer); + if (!shouldBuffer) { + await self.#waitForBufferSpaceAfterDrop(); + } } }; @@ -317,6 +320,17 @@ class ShareImpl { return true; } + async #waitForBufferSpaceAfterDrop() { + while (this.#bufferedBytes >= this.#options.budget && + !this.#cancelled && + this.#sourceError === undefined && + !this.#sourceExhausted) { + const { promise, resolve } = PromiseWithResolvers(); + ArrayPrototypePush(this.#pullWaiters, resolve); + await promise; + } + } + #pullFromSource(discard = false) { if (this.#sourceExhausted || this.#cancelled) { return PromiseResolve(); diff --git a/test/parallel/test-stream-iter-share-from.js b/test/parallel/test-stream-iter-share-from.js index 0b7e22ee5cee..806e30876302 100644 --- a/test/parallel/test-stream-iter-share-from.js +++ b/test/parallel/test-stream-iter-share-from.js @@ -170,40 +170,50 @@ async function testShareDropOldest() { } async function testShareDropNewest() { - // With drop-newest and a stalled consumer, the async path allows the - // buffer to grow beyond budget (the "drop" applies to the - // backpressure signal, not the buffer contents). Both consumers - // ultimately see all items. + let pulls = 0; + let secondPull; + const secondPullStarted = new Promise((resolve) => { + secondPull = resolve; + }); + async function* source() { - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 7; i++) { + pulls++; + if (pulls === 2) secondPull(); const chunk = new Uint8Array(16384); chunk[0] = i; yield [chunk]; } } - const shared = share(source(), { budget: 32768, backpressure: 'drop-newest' }); - const fast = shared.pull(); - const slow = shared.pull(); + const shared = share(source(), { + budget: 16384, + backpressure: 'drop-newest', + }); + const fast = shared.pull()[Symbol.asyncIterator](); + const slow = shared.pull()[Symbol.asyncIterator](); - // Fast consumer reads all items - const fastIndices = []; - for await (const batch of fast) { - for (const chunk of batch) { - fastIndices.push(chunk[0]); - } - } - assert.strictEqual(fastIndices.length, 2); + const first = await fast.next(); + assert.strictEqual(first.value[0][0], 0); - // Slow consumer also sees all items (buffer grew past budget) - const slowIndices = []; - for await (const batch of slow) { - for (const chunk of batch) { - slowIndices.push(chunk[0]); - } - } - assert.strictEqual(slowIndices.length, 2); - assert.strictEqual(slowIndices[0], 0); - assert.strictEqual(slowIndices[1], 1); + let nextSettled = false; + const next = fast.next().then((result) => { + nextSettled = true; + return result; + }); + + await secondPullStarted; + await new Promise(setImmediate); + assert.strictEqual(pulls, 2); + assert.strictEqual(nextSettled, false); + + const slowResult = await slow.next(); + assert.strictEqual(slowResult.value[0][0], 0); + + const nextResult = await next; + assert.strictEqual(nextResult.value[0][0], 2); + assert.strictEqual(pulls, 3); + + shared.cancel(); } // ============================================================================= From 0714ed5f486fa60f22a0b3eda35d67f252e8627b Mon Sep 17 00:00:00 2001 From: Dushyant Singh Hada Date: Mon, 24 Aug 2026 03:14:23 +0000 Subject: [PATCH 84/97] util: fix OSC 8 hyperlink stripping in stripVTControlCharacters The bundled ansi-regex OSC pattern used a restrictive URI character class that failed when URIs contained RFC 3986-valid characters such as parentheses. Match OSC sequences generically as ESC ] ... ST, aligned with ansi-regex v6.2.0. Co-authored-by: Cursor Signed-off-by: dushyant PR-URL: https://github.com/nodejs/node/pull/64319 Fixes: https://github.com/nodejs/node/issues/64313 Reviewed-By: Aviv Keller --- lib/internal/util/inspect.js | 12 +++++----- .../test-util-stripvtcontrolcharacters.js | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 89b30b4304c4..8061cf51771c 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -282,16 +282,14 @@ const meta = [ ]; // Regex used for ansi escape code splitting -// Ref: https://github.com/chalk/ansi-regex/blob/f338e1814144efb950276aac84135ff86b72dc8e/index.js +// Ref: https://github.com/chalk/ansi-regex/blob/72bc570aaf25fca25541b49c6a8564f3ec63e835/index.js // License: MIT by Sindre Sorhus // Matches all ansi escape code sequences in a string const ansi = new RegExp( - '[\\u001B\\u009B][[\\]()#;?]*' + - '(?:(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*' + - '|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]*)*)?' + - '(?:\\u0007|\\u001B\\u005C|\\u009C))' + - '|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?' + - '[\\dA-PR-TZcf-nq-uy=><~]))', 'g', + '(?:\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\u005C|\\u009C))' + + '|[\\u001B\\u009B][[\\]()#;?]*' + + '(?:\\d{1,4}(?:[;:]\\d{0,4})*)?' + + '[\\dA-PR-TZcf-nq-uy=><~]', 'g', ); let getStringWidth; diff --git a/test/parallel/test-util-stripvtcontrolcharacters.js b/test/parallel/test-util-stripvtcontrolcharacters.js index a33d18d26dbc..efda16687821 100644 --- a/test/parallel/test-util-stripvtcontrolcharacters.js +++ b/test/parallel/test-util-stripvtcontrolcharacters.js @@ -18,9 +18,31 @@ for (const ST of ['\u0007', '\u001B\u005C', '\u009C']) { tests.push( [`\u001B]8;;mailto:no-replay@mail.com${ST}mail\u001B]8;;${ST}`, 'mail'], [`\u001B]8;k=v;https://example-a.com/?a_b=1&c=2#tit%20le${ST}click\u001B]8;;${ST}`, 'click'], + [`\u001B]8;;https://example.com/(foo${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo)bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/!foo${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo+bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/[foo]${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo$bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo'bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo*bar${ST}label\u001B]8;;${ST}`, 'label'], + [`\u001B]8;;https://example.com/foo,bar${ST}label\u001B]8;;${ST}`, 'label'], ); } +// Colon-delimited CSI sub-parameters (SGR) should be stripped like the +// semicolon-delimited form. +tests.push( + ['\u001B[38:2:255:0:0mHello\u001B[0m', 'Hello'], + ['\u001B[4:3mUnderline\u001B[4:0m', 'Underline'], +); + +// Unterminated OSC does not match the OSC alternative; the CSI alternative may +// still consume a short prefix (here ESC ] 8 ;; h), leaving the remainder. +tests.push( + ['\u001B]8;;https://example.com/no-terminator', 'ttps://example.com/no-terminator'], +); + test('util.stripVTControlCharacters', (t) => { for (const [before, expected] of tests) { t.assert.strictEqual(util.stripVTControlCharacters(before), expected); From 87156be1a80238200837579b65648c858a2c1ed7 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Mon, 24 Aug 2026 13:56:04 +0930 Subject: [PATCH 85/97] fs: fix realpath of namespaced drive paths The JavaScript realpath implementation probes a namespaced drive root through the fs binding. Windows path resolution drops the trailing separator from that probe, so lstat receives C: and reports EISDIR. Use the regular drive-root spelling only for the probe. Preserve the namespaced spelling for traversal and returned paths. Signed-off-by: Jason Zhang PR-URL: https://github.com/nodejs/node/pull/65378 Fixes: https://github.com/nodejs/node/issues/62446 Reviewed-By: James M Snell Reviewed-By: Stefan Stojanovic --- lib/fs.js | 20 ++++-- test/es-module/test-esm-long-path-win.js | 18 ++++++ .../test-fs-realpath-namespaced-drive-win.js | 63 +++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-fs-realpath-namespaced-drive-win.js diff --git a/lib/fs.js b/lib/fs.js index 5834a19b83cc..e533e61e381d 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -2716,13 +2716,22 @@ function unwatchFile(filename, listener) { let splitRoot; +let getRealpathRootLstatPath; if (isWindows) { // Regex to find the device root on Windows (e.g. 'c:\\'), including trailing // slash. const splitRootRe = /^(?:[a-zA-Z]:|[\\/]{2}[^\\/]+[\\/][^\\/]+)?[\\/]*/; + const namespacedDriveRootRe = /^\\\\\?\\([a-zA-Z]:\\)$/; splitRoot = function splitRoot(str) { return SideEffectFreeRegExpPrototypeExec(splitRootRe, str)[0]; }; + + // The root probe is the only use of this path. Passing a namespaced drive + // root to the binding would lose its trailing separator during resolution. + getRealpathRootLstatPath = function getRealpathRootLstatPath(path) { + const match = SideEffectFreeRegExpPrototypeExec(namespacedDriveRootRe, path); + return match === null ? path : match[1]; + }; } else { splitRoot = function splitRoot(str) { for (let i = 0; i < str.length; ++i) { @@ -2731,6 +2740,7 @@ if (isWindows) { } return str; }; + } function encodeRealpathResult(result, options) { @@ -2807,7 +2817,8 @@ function realpathSync(p, options) { // On windows, check that the root exists. On unix there is no need. if (isWindows) { - const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */); + const out = binding.lstat( + getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */); if (out === undefined) { return; } @@ -2892,7 +2903,8 @@ function realpathSync(p, options) { // On windows, check that the root exists. On unix there is no need. if (isWindows && !knownHard.has(base)) { - const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */); + const out = binding.lstat( + getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */); if (out === undefined) { return; } @@ -2966,7 +2978,7 @@ function realpath(p, options, callback) { // On windows, check that the root exists. On unix there is no need. if (isWindows && !knownHard.has(base)) { - fs.lstat(base, (err) => { + fs.lstat(getRealpathRootLstatPath(base), (err) => { if (err) return callback(err); knownHard.add(base); LOOP(); @@ -3055,7 +3067,7 @@ function realpath(p, options, callback) { // On windows, check that the root exists. On unix there is no need. if (isWindows && !knownHard.has(base)) { - fs.lstat(base, (err) => { + fs.lstat(getRealpathRootLstatPath(base), (err) => { if (err) return callback(err); knownHard.add(base); LOOP(); diff --git a/test/es-module/test-esm-long-path-win.js b/test/es-module/test-esm-long-path-win.js index d125d341f092..d8aaabcca857 100644 --- a/test/es-module/test-esm-long-path-win.js +++ b/test/es-module/test-esm-long-path-win.js @@ -47,6 +47,24 @@ describe('long path on Windows', () => { tmpdir.refresh(); }); + it('runs an extended-length path as the entry point', async () => { + // The module loader resolves argv[1] through the JavaScript realpath + // implementation before executing it. + tmpdir.refresh(); + const entry = tmpdir.resolve('extended-entry.js'); + fs.writeFileSync(entry, 'console.log("hello world");'); + + const { code, signal, stderr, stdout } = await spawnPromisified( + execPath, + [path.toNamespacedPath(entry)], + ); + + assert.strictEqual(stderr, ''); + assert.strictEqual(stdout.trim(), 'hello world'); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + }); + it('check long path in LegacyMainResolve - 1', () => { // Module layout will be the following: // package.json diff --git a/test/parallel/test-fs-realpath-namespaced-drive-win.js b/test/parallel/test-fs-realpath-namespaced-drive-win.js new file mode 100644 index 000000000000..eac72eb8dfa2 --- /dev/null +++ b/test/parallel/test-fs-realpath-namespaced-drive-win.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('../common'); +if (!common.isWindows) { + common.skip('This test is Windows-specific.'); +} + +// Verify that the JavaScript realpath implementation accepts namespaced drive +// paths, including when a junction switches the walk back to a regular drive +// path, and reports a missing entry instead of treating the drive as a file. + +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); +const { test } = require('node:test'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const entry = tmpdir.resolve('entry.js'); +const namespacedEntry = path.toNamespacedPath(entry); +const namespacedMissing = path.toNamespacedPath(tmpdir.resolve('missing.js')); +const targetDir = tmpdir.resolve('target'); +const targetEntry = path.join(targetDir, 'entry.js'); +const junctionDir = tmpdir.resolve('junction'); +const namespacedJunctionEntry = path.toNamespacedPath( + path.join(junctionDir, 'entry.js'), +); + +fs.writeFileSync(entry, ''); +fs.mkdirSync(targetDir); +fs.writeFileSync(targetEntry, ''); +fs.symlinkSync(targetDir, junctionDir, 'junction'); + +function assertNamespacedRealpath(result) { + assert.strictEqual(path.toNamespacedPath(result), namespacedEntry); +} + +test('fs.realpathSync resolves a namespaced drive path', () => { + assertNamespacedRealpath(fs.realpathSync(namespacedEntry)); +}); + +test('fs.realpathSync reports ENOENT for a missing namespaced drive path', () => { + assert.throws(() => fs.realpathSync(namespacedMissing), { code: 'ENOENT' }); +}); + +test('fs.realpathSync resolves a namespaced path through a junction', () => { + assert.strictEqual(fs.realpathSync(namespacedJunctionEntry), targetEntry); +}); + +test('fs.realpath resolves a namespaced drive path', (t, done) => { + fs.realpath(namespacedEntry, common.mustSucceed((result) => { + assertNamespacedRealpath(result); + done(); + })); +}); + +test('fs.realpath resolves a namespaced path through a junction', (t, done) => { + fs.realpath(namespacedJunctionEntry, common.mustSucceed((result) => { + assert.strictEqual(result, targetEntry); + done(); + })); +}); From cf32bedbac3c388b58bb3aba5c9729a18208889d Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 24 Aug 2026 09:43:57 +0200 Subject: [PATCH 86/97] fs: use sized reads for large files in readFileUtf8 fs.readFileSync(path, 'utf8') read the whole file in 8 KiB read() calls appended to a std::string, i.e. one syscall and a potential reallocation per 8 KiB (an 8 MiB file took ~1400 read() calls). Keep the exact old sequence for small files (one read into the 8 KiB stack buffer, one read reporting EOF). Once a read fills the stack buffer, read the rest directly into one heap buffer sized from fstat() (plus one byte so that the EOF read does not force growth), growing geometrically only when the size is unavailable or wrong. The size is only an allocation hint: reading continues until read() reports EOF, so procfs/sysfs files, FIFOs, files that change while being read and file descriptors positioned mid-file behave as before, and the bytes handed to StringBytes::Encode() are exactly the ones read. Signed-off-by: Shelley Vohr PR-URL: https://github.com/nodejs/node/pull/65328 Reviewed-By: Matteo Collina Reviewed-By: Yagiz Nizipli --- src/node_file.cc | 64 +++++++++++- .../test-fs-readfilesync-utf8-sizes.js | 98 +++++++++++++++++++ 2 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-fs-readfilesync-utf8-sizes.js diff --git a/src/node_file.cc b/src/node_file.cc index 638b17717d09..a576b4a9d4eb 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -2923,10 +2923,20 @@ static void ReadFileUtf8(const FunctionCallbackInfo& args) { uv_fs_req_cleanup(&req); }); + // Past the first 8 KiB, read into one heap buffer sized from fstat(); the + // size is only a hint, reading continues until read() reports EOF. std::string result{}; char buffer[8192]; uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer)); + char* big = nullptr; + size_t big_len = 0; + size_t big_cap = 0; + bool sized = false; + auto free_big = OnScopeLeave([&big]() { free(big); }); + constexpr size_t kMinChunk = 64 * 1024; + constexpr size_t kMaxChunk = 8 * 1024 * 1024; + FS_SYNC_TRACE_BEGIN(read); while (true) { auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr); @@ -2939,12 +2949,62 @@ static void ReadFileUtf8(const FunctionCallbackInfo& args) { if (r <= 0) { break; } - result.append(buf.base, r); + if (big == nullptr) { + result.append(buf.base, r); + if (static_cast(r) < sizeof(buffer)) { + continue; + } + // Switch to the heap buffer. + uv_fs_req_cleanup(&req); + big_cap = kMinChunk; + big = UncheckedMalloc(big_cap); + if (big == nullptr) { + FS_SYNC_TRACE_END(read); + return THROW_ERR_MEMORY_ALLOCATION_FAILED(env); + } + memcpy(big, result.data(), result.size()); + big_len = result.size(); + result = std::string(); + } else { + big_len += static_cast(r); + } + if (big_len == big_cap) { + // +1 leaves room for the read() that reports EOF. + size_t new_cap = + big_cap + std::min(kMaxChunk, std::max(kMinChunk, big_cap)); + if (!sized) { + sized = true; + uv_fs_req_cleanup(&req); + uv_fs_t stat_req; + if (uv_fs_fstat(nullptr, &stat_req, file, nullptr) == 0) { + const uv_stat_t* const st = + static_cast(stat_req.ptr); + if ((st->st_mode & S_IFMT) == S_IFREG && + static_cast(st->st_size) > big_len && + static_cast(st->st_size) < + static_cast(v8::String::kMaxLength)) { + new_cap = static_cast(st->st_size) + 1; + } + } + uv_fs_req_cleanup(&stat_req); + } + char* const grown = UncheckedRealloc(big, new_cap); + if (grown == nullptr) { + FS_SYNC_TRACE_END(read); + return THROW_ERR_MEMORY_ALLOCATION_FAILED(env); + } + big = grown; + big_cap = new_cap; + } + buf = uv_buf_init(big + big_len, std::min(kMaxChunk, big_cap - big_len)); } FS_SYNC_TRACE_END(read); Local val; - if (!ToV8Value(env->context(), result, isolate).ToLocal(&val)) { + const std::string_view content = big != nullptr + ? std::string_view(big, big_len) + : std::string_view(result); + if (!ToV8Value(env->context(), content, isolate).ToLocal(&val)) { return; } diff --git a/test/parallel/test-fs-readfilesync-utf8-sizes.js b/test/parallel/test-fs-readfilesync-utf8-sizes.js new file mode 100644 index 000000000000..ac8670836d44 --- /dev/null +++ b/test/parallel/test-fs-readfilesync-utf8-sizes.js @@ -0,0 +1,98 @@ +'use strict'; +// fs.readFileSync(path, 'utf8') takes a dedicated native path. Its result must +// equal fs.readFileSync(path).toString('utf8') for every file size (in +// particular around its internal 8 KiB stack buffer and for multi-megabyte +// files), for file descriptors positioned mid-file, and for files whose +// reported size is wrong (procfs reports 0, sysfs reports a page). +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +tmpdir.refresh(); + +function content(size) { + // Multi-byte characters straddling every possible chunk boundary. + const unit = 'abcdé€\u{1F600}\n'; + let s = unit.repeat(Math.ceil(size / unit.length)); + s = s.slice(0, size); + // Avoid ending on a lone surrogate produced by slice(). + if (/[\ud800-\udbff]$/.test(s)) s = s.slice(0, -1) + 'x'; + return s; +} + +const sizes = [0, 1, 8190, 8191, 8192, 8193, 8194, 16383, 16384, 16385, + 65535, 65536, 65537, 100000, (1 << 20) - 1, 1 << 20, (1 << 20) + 1, + (8 << 20) + 5]; +for (const size of sizes) { + const file = tmpdir.resolve(`f-${size}.txt`); + const str = content(size); + fs.writeFileSync(file, str); + const expected = fs.readFileSync(file).toString('utf8'); + assert.strictEqual(fs.readFileSync(file, 'utf8'), expected, `size ${size} by path`); + assert.strictEqual(fs.readFileSync(file, { encoding: 'utf-8' }), expected, `size ${size} utf-8 alias`); + // By fd: from the start (leaves the fd at EOF), then at EOF, then from a + // mid-file position on a fresh fd. + let fd = fs.openSync(file, 'r'); + try { + assert.strictEqual(fs.readFileSync(fd, 'utf8'), expected, `size ${size} by fd`); + assert.strictEqual(fs.readFileSync(fd, 'utf8'), '', `size ${size} by fd at EOF`); + } finally { + fs.closeSync(fd); + } + if (size > 10) { + fd = fs.openSync(file, 'r'); + try { + // Advance the fd 3 bytes (inside the ASCII prefix, so still valid UTF-8). + assert.strictEqual(fs.readSync(fd, Buffer.alloc(3), 0, 3, null), 3); + assert.strictEqual(fs.readFileSync(fd, 'utf8'), Buffer.from(expected).subarray(3).toString('utf8'), + `size ${size} by fd at offset 3`); + } finally { + fs.closeSync(fd); + } + } +} + +// Binary garbage is decoded with replacement characters identically. +{ + const file = tmpdir.resolve('binary.bin'); + const buf = Buffer.alloc(20000); + for (let i = 0; i < buf.length; i++) buf[i] = (i * 7919) & 0xff; + fs.writeFileSync(file, buf); + assert.strictEqual(fs.readFileSync(file, 'utf8'), buf.toString('utf8')); +} + +// Files whose st_size does not describe their content. +if (common.isLinux) { + for (const file of ['/proc/self/status', '/proc/self/smaps', '/proc/cpuinfo', + '/proc/version', '/sys/kernel/mm/transparent_hugepage/enabled']) { + let viaBuffer; + try { + viaBuffer = fs.readFileSync(file); + } catch { + continue; // Not available in this environment. + } + const viaUtf8 = fs.readFileSync(file, 'utf8'); + if (file !== '/proc/version' && file.startsWith('/proc/')) { + // Content legitimately differs between two reads; compare shape instead. + assert.ok(viaUtf8.length > 0); + assert.strictEqual(viaUtf8.split('\n').length > 5, true, file); + // Of these, smaps reliably exceeds the 8 KiB stack buffer. + if (file === '/proc/self/smaps') assert.ok(viaUtf8.length > 8192, `smaps is only ${viaUtf8.length} chars`); + } else { + assert.strictEqual(viaUtf8, viaBuffer.toString('utf8'), file); + } + } +} + +// Directory: same outcome either way (EISDIR, except on platforms where +// read() accepts directories, e.g. AIX). +function outcome(read) { + try { + return read(); + } catch (err) { + return err.code; + } +} +assert.strictEqual(outcome(() => fs.readFileSync(tmpdir.path, 'utf8')), + outcome(() => fs.readFileSync(tmpdir.path).toString('utf8'))); From 5b13d30c64896df1318be356127d30bfb8c8df53 Mon Sep 17 00:00:00 2001 From: Tim Perry <1526883+pimterry@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:54:15 +0100 Subject: [PATCH 87/97] http: improve performance with known-length calls to end() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This boosts RPS performance for the common API case where you call `res.end(data)` with the entire response by up to 9%. Signed-off-by: Tim Perry PR-URL: https://github.com/nodejs/node/pull/65466 Reviewed-By: Matteo Collina Reviewed-By: Yagiz Nizipli Reviewed-By: Luigi Pinca Reviewed-By: Gürgün Dayıoğlu --- benchmark/http/end-string.js | 35 +++++++++++++ lib/_http_outgoing.js | 51 ++++++++++++++++--- .../test-http-server-response-standalone.js | 11 +--- 3 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 benchmark/http/end-string.js diff --git a/benchmark/http/end-string.js b/benchmark/http/end-string.js new file mode 100644 index 000000000000..9c5c6afc5869 --- /dev/null +++ b/benchmark/http/end-string.js @@ -0,0 +1,35 @@ +// Responses sent as a single res.end(string) with a known Content-Length - +// the shape a JSON or HTML endpoint produces. +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + len: [4, 64, 1024, 16384, 102400], + c: [50], + duration: 5, +}); + +function main({ len, c, duration }) { + const http = require('http'); + const body = 'a'.repeat(len); + const headers = { + 'Content-Type': 'text/plain', + 'Content-Length': `${len}`, + }; + + const server = http.createServer((req, res) => { + res.writeHead(200, headers); + res.end(body); + }); + + server.listen(0, () => { + bench.http({ + connections: c, + duration, + port: server.address().port, + }, () => { + server.close(); + }); + }); +} diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index b266cf49971e..23e2856e0b99 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -1026,6 +1026,25 @@ function write_(msg, chunk, encoding, callback, fromEnd) { } +// If this last write can be delivered immediately as the final chunk, this +// prepares to do so, and then returns true. If not, it returns false and +// a separate _send call and tick will be required to finish up. +function maybePrepareFinalChunk(msg, chunk, encoding) { + if (typeof chunk !== 'string' && !isUint8Array(chunk)) + return false; + + if (msg.destroyed || msg.strictContentLength) + return false; + + if (!msg._header) { + msg._contentLength = typeof chunk === 'string' ? + Buffer.byteLength(chunk, encoding) : chunk.byteLength; + msg._implicitHeader(); + } + + return !!msg._header && msg._hasBody && !msg.chunkedEncoding; +} + function connectionCorkNT(conn) { conn.uncork(); } @@ -1131,6 +1150,8 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { encoding = null; } + let finishCallback = null; + if (chunk) { if (this.finished) { onError(this, @@ -1143,7 +1164,18 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { this[kSocket].cork(); } - write_(this, chunk, encoding, null, true); + if (maybePrepareFinalChunk(this, chunk, encoding)) { + // If just one final write is required, with nothing to follow, we + // attach finish to the write to avoid a separate send() & tick step + // later on - this is purely a performance optimization. + if (typeof callback === 'function') { + queueEndCallback(this, callback); + callback = undefined; + } + finishCallback = onFinish.bind(undefined, this); + } + + write_(this, chunk, encoding, finishCallback, true); } else if (this.finished) { if (typeof callback === 'function') { queueEndCallback(this, callback); @@ -1165,14 +1197,17 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(this[kBytesWritten], this._contentLength); } - const finish = onFinish.bind(undefined, this); + if (finishCallback === null) { + // If we didn't early finish, send the last data and schedule 'finish' now: + finishCallback = onFinish.bind(undefined, this); - if (this._hasBody && this.chunkedEncoding) { - this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish); - } else if (!this._headerSent || this.writableLength || chunk) { - this._send('', 'latin1', finish); - } else { - process.nextTick(finish); + if (this._hasBody && this.chunkedEncoding) { + this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finishCallback); + } else if (!this._headerSent || this.writableLength || chunk) { + this._send('', 'latin1', finishCallback); + } else { + process.nextTick(finishCallback); + } } if (this[kSocket]) { diff --git a/test/parallel/test-http-server-response-standalone.js b/test/parallel/test-http-server-response-standalone.js index bc7ca56f894b..00ca7c0a96d6 100644 --- a/test/parallel/test-http-server-response-standalone.js +++ b/test/parallel/test-http-server-response-standalone.js @@ -15,18 +15,11 @@ const res = new ServerResponse({ httpVersionMinor: 1 }); -let firstChunk = true; - const ws = new Writable({ write: common.mustCall((chunk, encoding, callback) => { - if (firstChunk) { - assert(chunk.toString().endsWith('hello world')); - firstChunk = false; - } else { - assert.strictEqual(chunk.length, 0); - } + assert(chunk.toString().endsWith('hello world')); setImmediate(callback); - }, 2) + }, 1) }); res.assignSocket(ws); From b85ff9604e4b90063caace21c72bd1a8a5348d39 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Mon, 24 Aug 2026 09:43:36 -0300 Subject: [PATCH 88/97] permission: enforce addon permission in GetLinkedBinding Signed-off-by: RafaelGSS PR-URL: https://github.com/nodejs/node/pull/65432 Reviewed-By: Chengzhong Wu Reviewed-By: Beth Griggs Reviewed-By: James M Snell --- src/node_binding.cc | 3 +++ .../test-permission-linked-binding-drop.js | 19 +++++++++++++++++++ .../test-permission-linked-binding.js | 15 +++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 test/parallel/test-permission-linked-binding-drop.js create mode 100644 test/parallel/test-permission-linked-binding.js diff --git a/src/node_binding.cc b/src/node_binding.cc index fa11b58725cf..9f34534d2fa8 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -670,6 +670,9 @@ void GetLinkedBinding(const FunctionCallbackInfo& args) { node::Utf8Value module_name_v(env->isolate(), module_name); const char* name = *module_name_v; + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, permission::PermissionScope::kAddon, module_name_v.ToStringView()); + node_module* mod = nullptr; // Iterate from here to the nearest non-Worker Environment to see if there's diff --git a/test/parallel/test-permission-linked-binding-drop.js b/test/parallel/test-permission-linked-binding-drop.js new file mode 100644 index 000000000000..865761a2bd7e --- /dev/null +++ b/test/parallel/test-permission-linked-binding-drop.js @@ -0,0 +1,19 @@ +// Flags: --permission --allow-addons --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); + +assert.strictEqual(process.permission.has('addon'), true); + +process.permission.drop('addon'); + +assert.strictEqual(process.permission.has('addon'), false); + +assert.throws(() => { + process._linkedBinding('missing'); +}, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'Addon', + resource: 'missing', +})); diff --git a/test/parallel/test-permission-linked-binding.js b/test/parallel/test-permission-linked-binding.js new file mode 100644 index 000000000000..b30ea9aaa24b --- /dev/null +++ b/test/parallel/test-permission-linked-binding.js @@ -0,0 +1,15 @@ +// Flags: --permission --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); + +assert.strictEqual(process.permission.has('addon'), false); + +assert.throws(() => { + process._linkedBinding('missing'); +}, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'Addon', + resource: 'missing', +})); From 810b8e2cc121345b8d9cdacefdaeea3cf04640ed Mon Sep 17 00:00:00 2001 From: Sankalp Thakur <31366524+sankalpsthakur@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:01:20 +0530 Subject: [PATCH 89/97] tls: throw on invalid ALPNProtocols instead of aborting tls.connect() with malformed ALPNProtocols hit CHECK_EQ(0, SSL_set_alpn_protos(...)) in the C++ layer and aborted the process with SIGABRT. Validate in JS instead, in convertALPNProtocols, so both client and server fail early with a recoverable ERR_INVALID_ARG_VALUE: - zero-length string protocols now throw from convertProtocols - wire-format buffers are checked for zero-length and truncated entries - an empty buffer or array is still accepted and means skip ALPN, matching the historical behavior for [] The C++ CHECK_EQ is left unchanged: once JS has validated the input, a non-zero SSL_set_alpn_protos return is an internal invariant failure rather than user-facing input. Fixes: https://github.com/nodejs/node/issues/65069 Signed-off-by: Sankalp Thakur PR-URL: https://github.com/nodejs/node/pull/65076 Reviewed-By: Tim Perry Reviewed-By: James M Snell Reviewed-By: Trivikram Kamat --- lib/tls.js | 31 ++++++- .../test-tls-alpn-protocols-validation.js | 85 +++++++++++++++++++ test/parallel/test-tls-basic-validations.js | 14 +-- 3 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-tls-alpn-protocols-validation.js diff --git a/lib/tls.js b/lib/tls.js index d2ecc7f0a583..a1d978c50f27 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -251,6 +251,10 @@ function convertProtocols(protocols) { const lens = new Array(protocols.length); const buff = Buffer.allocUnsafe(protocols.reduce((p, c, i) => { const len = Buffer.byteLength(c); + if (len === 0) { + throw new ERR_INVALID_ARG_VALUE(`protocols[${i}]`, c, + 'must be a non-empty string'); + } if (len > 255) { throw new ERR_OUT_OF_RANGE('The byte length of the protocol at index ' + `${i} exceeds the maximum length.`, '<= 255', len, true); @@ -269,18 +273,41 @@ function convertProtocols(protocols) { return buff; } +function validateALPNBuffer(buffer) { + // Wire format: sequence of where len is 1 byte (1-255) and + // exactly len bytes follow, no trailing bytes, no zero-length entries. + // Empty buffer is allowed and means skip ALPN (same as []). + let offset = 0; + while (offset < buffer.length) { + const len = buffer[offset]; + if (len === 0) { + throw new ERR_INVALID_ARG_VALUE('ALPNProtocols', buffer, + 'must not contain zero-length protocol'); + } + if (offset + 1 + len > buffer.length) { + throw new ERR_INVALID_ARG_VALUE('ALPNProtocols', buffer, + 'contains truncated protocol'); + } + offset += 1 + len; + } +} + exports.convertALPNProtocols = function convertALPNProtocols(protocols, out) { // If protocols is Array - translate it into buffer if (ArrayIsArray(protocols)) { out.ALPNProtocols = convertProtocols(protocols); } else if (isUint8Array(protocols)) { // Copy new buffer not to be modified by user. - out.ALPNProtocols = Buffer.from(protocols); + const buf = Buffer.from(protocols); + validateALPNBuffer(buf); + out.ALPNProtocols = buf; } else if (isArrayBufferView(protocols)) { - out.ALPNProtocols = Buffer.from(protocols.buffer.slice( + const buf = Buffer.from(protocols.buffer.slice( protocols.byteOffset, protocols.byteOffset + protocols.byteLength, )); + validateALPNBuffer(buf); + out.ALPNProtocols = buf; } }; diff --git a/test/parallel/test-tls-alpn-protocols-validation.js b/test/parallel/test-tls-alpn-protocols-validation.js new file mode 100644 index 000000000000..2a93cca891ce --- /dev/null +++ b/test/parallel/test-tls-alpn-protocols-validation.js @@ -0,0 +1,85 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); + +// Array with empty string should throw (zero-length protocol entry) +assert.throws(() => { + const out = {}; + tls.convertALPNProtocols([''], out); +}, { + code: 'ERR_INVALID_ARG_VALUE', +}); + +// Array with empty string mixed +assert.throws(() => { + const out = {}; + tls.convertALPNProtocols(['h2', ''], out); +}, { + code: 'ERR_INVALID_ARG_VALUE', +}); + +// Buffer wire format with leading zero length +assert.throws(() => { + const out = {}; + tls.convertALPNProtocols(Buffer.from([0]), out); +}, { + code: 'ERR_INVALID_ARG_VALUE', +}); + +// Buffer truncated (claims 2 bytes but only 1 follows) +assert.throws(() => { + const out = {}; + tls.convertALPNProtocols(Buffer.from([2, 0x61]), out); +}, { + code: 'ERR_INVALID_ARG_VALUE', +}); + +// Buffer with trailing invalid byte +assert.throws(() => { + const out = {}; + tls.convertALPNProtocols(Buffer.from([1, 0x61, 0x62, 0x62]), out); +}, { + code: 'ERR_INVALID_ARG_VALUE', +}); + +// Empty array means skip ALPN (allowed) +{ + const out = {}; + tls.convertALPNProtocols([], out); + assert.ok(Buffer.isBuffer(out.ALPNProtocols)); + assert.strictEqual(out.ALPNProtocols.length, 0); +} + +// Empty buffer means skip ALPN (allowed; same as []) +{ + const out = {}; + tls.convertALPNProtocols(Buffer.alloc(0), out); + assert.ok(Buffer.isBuffer(out.ALPNProtocols)); + assert.strictEqual(out.ALPNProtocols.length, 0); +} + +// Empty Uint8Array means skip ALPN +{ + const out = {}; + tls.convertALPNProtocols(new Uint8Array(0), out); + assert.ok(Buffer.isBuffer(out.ALPNProtocols)); + assert.strictEqual(out.ALPNProtocols.length, 0); +} + +// Valid inputs should not throw +{ + const out = {}; + tls.convertALPNProtocols(['h2', 'http/1.1'], out); + assert.ok(out.ALPNProtocols.length > 0); +} +{ + const out = {}; + tls.convertALPNProtocols(Buffer.from([ + 2, 0x61, 0x62, 8, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, + ]), out); + assert.strictEqual(out.ALPNProtocols.length, 12); +} diff --git a/test/parallel/test-tls-basic-validations.js b/test/parallel/test-tls-basic-validations.js index 0446b6aef219..d2bbc26b003a 100644 --- a/test/parallel/test-tls-basic-validations.js +++ b/test/parallel/test-tls-basic-validations.js @@ -81,17 +81,19 @@ assert.throws(() => tls.createServer({ ticketKeys: Buffer.alloc(0) }), { }); { - const buffer = Buffer.from('abcd'); + const buffer = Buffer.from([3, 0x61, 0x62, 0x63]); const out = {}; tls.convertALPNProtocols(buffer, out); - out.ALPNProtocols.write('efgh'); - assert(buffer.equals(Buffer.from('abcd'))); - assert(out.ALPNProtocols.equals(Buffer.from('efgh'))); + out.ALPNProtocols.write('def', 1); + assert(buffer.equals(Buffer.from([3, 0x61, 0x62, 0x63]))); + assert(out.ALPNProtocols.equals(Buffer.from([3, 0x64, 0x65, 0x66]))); } { - const arrayBufferViewStr = 'abcd'; - const inputBuffer = Buffer.from(arrayBufferViewStr.repeat(8), 'utf8'); + const inputBuffer = Buffer.concat([ + Buffer.from([31]), + Buffer.alloc(31, 0x61), + ]); for (const expectView of common.getArrayBufferViews(inputBuffer)) { const out = {}; const expected = Buffer.from(expectView.buffer.slice(), From 7434261592cd94ef8da7a6ea87d3fb40a0a1d41b Mon Sep 17 00:00:00 2001 From: Hamid Reza Ghavami Date: Mon, 24 Aug 2026 17:31:34 +0300 Subject: [PATCH 90/97] util: allow single-line format when break length is infinite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hamid Reza Ghavami PR-URL: https://github.com/nodejs/node/pull/64238 Reviewed-By: Jordan Harband Reviewed-By: Juan José Arboleda --- lib/internal/util/inspect.js | 8 ++++++++ test/parallel/test-util-inspect.js | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 8061cf51771c..17e9de541289 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -2350,6 +2350,14 @@ function isBelowBreakLength(ctx, output, start, base) { // TODO(BridgeAR): Add unicode support. Use the readline getStringWidth // function. Check the performance overhead and make it an opt-in in case it's // significant. + // allow the single-line format if the length limit is infinite and no items have newlines + if (ctx.breakLength === Infinity) { + if (base !== '' && StringPrototypeIncludes(base, '\n')) return false; + for (let i = 0; i < output.length; i++) { + if (typeof output[i] === 'string' && StringPrototypeIncludes(output[i], '\n')) return false; + } + return true; + } let totalLength = output.length + start; if (totalLength + output.length > ctx.breakLength) return false; diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index d4dfe9acf006..1278e4eed471 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -4064,3 +4064,9 @@ ${error.stack.split('\n').slice(1).join('\n')}`, assert.match(inspect(DOMException.prototype), /^\[object DOMException\] \{/); delete Error[Symbol.hasInstance]; } + +{ + const obj = { a: 'short string', b: [1, 2], c: { d: true } }; + const expected = "{ a: 'short string', b: [ 1, 2 ], c: { d: true } }"; + assert.strictEqual(util.inspect(obj, { breakLength: Infinity }), expected); +} From 51c757cba371fd959e0a03dde64345739f4ee705 Mon Sep 17 00:00:00 2001 From: Taeuk Ha <102611556+ChocoChip0519@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:30:12 +0900 Subject: [PATCH 91/97] doc: update AHAFS reference link Signed-off-by: Taeuk Ha PR-URL: https://github.com/nodejs/node/pull/65481 Reviewed-By: Daeyeon Jeong Reviewed-By: Colin Ihrig Reviewed-By: Beth Griggs --- doc/api/fs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index 60a90b4f2557..824247db7136 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -9315,7 +9315,7 @@ the file contents. [MSDN-Rel-Path]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#fully-qualified-vs-relative-paths [MSDN-Using-Streams]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/using-streams [Naming Files, Paths, and Namespaces]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file -[`AHAFS`]: https://developer.ibm.com/articles/au-aix_event_infrastructure/ +[`AHAFS`]: https://www.ibm.com/docs/en/aix/7.3.0?topic=management-aix-event-infrastructure-aix-aix-clusters-ahafs [`Buffer.byteLength`]: buffer.md#static-method-bufferbytelengthstring-encoding [`FSEvents`]: https://developer.apple.com/documentation/coreservices/file_system_events [`Number.MAX_SAFE_INTEGER`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER From ad21f90247d5287dd60c486d0f760d4be7db69ab Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 24 Aug 2026 11:17:10 -0700 Subject: [PATCH 92/97] stream: encode whole chunks in TextEncoderStream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The encode-and-enqueue transform walked the chunk code unit by code unit, materializing a single-character string per index and building the output with string concatenation. The only state that crosses chunks is a trailing high (leading) surrogate, and TextEncoder.encode's USVString conversion already replaces every interior lone surrogate with U+FFFD, which is exactly what the spec loop produces. Join a pending high surrogate with the incoming chunk, hold back a new trailing high surrogate, and encode the rest in a single native call. The streaming decode path also reuses a single options object instead of allocating { stream: true } per chunk. An encoding-streams benchmark is added since the suite had no TextEncoderStream/TextDecoderStream row. Encoding improves by ~546% with 1KB string chunks and ~20% with 16-character chunks; decode is unchanged. Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/65414 Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Yagiz Nizipli Reviewed-By: Mattias Buelens --- benchmark/webstreams/encoding-streams.js | 39 +++++++++++ benchmark/webstreams/from.js | 29 ++++++++ lib/internal/webstreams/encoding.js | 48 ++++++------- lib/internal/webstreams/readablestream.js | 83 ++++++++++++++++++++--- lib/internal/webstreams/util.js | 2 +- 5 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 benchmark/webstreams/encoding-streams.js create mode 100644 benchmark/webstreams/from.js diff --git a/benchmark/webstreams/encoding-streams.js b/benchmark/webstreams/encoding-streams.js new file mode 100644 index 000000000000..00759bc09eb7 --- /dev/null +++ b/benchmark/webstreams/encoding-streams.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common.js'); +const { + ReadableStream, + TextEncoderStream, + TextDecoderStream, +} = require('node:stream/web'); + +const bench = common.createBenchmark(main, { + n: [1e5], + kind: ['encode', 'decode'], + len: [16, 1024], +}); + +async function main({ n, kind, len }) { + const encoded = new TextEncoder().encode('a'.repeat(len)); + const decoded = 'a'.repeat(len); + let i = 0; + const rs = new ReadableStream({ + pull(controller) { + if (i++ < n) { + controller.enqueue(kind === 'encode' ? decoded : encoded); + } else { + controller.close(); + } + }, + }); + const ts = kind === 'encode' ? + new TextEncoderStream() : + new TextDecoderStream(); + + const reader = rs.pipeThrough(ts).getReader(); + bench.start(); + for (;;) { + const { done } = await reader.read(); + if (done) break; + } + bench.end(n); +} diff --git a/benchmark/webstreams/from.js b/benchmark/webstreams/from.js new file mode 100644 index 000000000000..05eca4079f1d --- /dev/null +++ b/benchmark/webstreams/from.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common.js'); +const { + ReadableStream, +} = require('node:stream/web'); + +const bench = common.createBenchmark(main, { + n: [1e6], + kind: ['sync', 'async'], +}); + +async function main({ n, kind }) { + function* syncGen() { + for (let i = 0; i < n; i++) yield i; + } + + async function* asyncGen() { + for (let i = 0; i < n; i++) yield i; + } + + const reader = ReadableStream.from( + kind === 'sync' ? syncGen() : asyncGen()).getReader(); + bench.start(); + for (;;) { + const { done } = await reader.read(); + if (done) break; + } + bench.end(n); +} diff --git a/lib/internal/webstreams/encoding.js b/lib/internal/webstreams/encoding.js index f316222ccbf0..038b64030a7a 100644 --- a/lib/internal/webstreams/encoding.js +++ b/lib/internal/webstreams/encoding.js @@ -4,6 +4,7 @@ const { ObjectDefineProperties, String, StringPrototypeCharCodeAt, + StringPrototypeSlice, Uint8Array, } = primordials; @@ -31,6 +32,9 @@ const { kEnumerableProperty, } = require('internal/util'); +// Shared per-chunk decode options; decode() only reads the flag. +const kDecodeStreamingOptions = { __proto__: null, stream: true }; + /** * @typedef {import('./readablestream').ReadableStream} ReadableStream * @typedef {import('./writablestream').WritableStream} WritableStream @@ -46,34 +50,26 @@ class TextEncoderStream { this.#transform = new TransformStream({ transform: (chunk, controller) => { // https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk + // The only cross-chunk state is a trailing high surrogate; + // encode() replaces interior lone surrogates with U+FFFD exactly + // like the spec's per-code-unit walk. chunk = String(chunk); - let finalChunk = ''; - for (let i = 0; i < chunk.length; i++) { - const item = chunk[i]; - const codeUnit = StringPrototypeCharCodeAt(item, 0); - if (this.#pendingHighSurrogate !== null) { - const highSurrogate = this.#pendingHighSurrogate; - this.#pendingHighSurrogate = null; - if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) { - finalChunk += highSurrogate + item; - continue; - } - finalChunk += '\uFFFD'; - } - if (0xD800 <= codeUnit && codeUnit <= 0xDBFF) { - this.#pendingHighSurrogate = item; - continue; - } - if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) { - finalChunk += '\uFFFD'; - continue; - } - finalChunk += item; + if (chunk.length === 0) + return; + if (this.#pendingHighSurrogate !== null) { + chunk = this.#pendingHighSurrogate + chunk; + this.#pendingHighSurrogate = null; } - if (finalChunk) { - const value = this.#handle.encode(finalChunk); - controller.enqueue(value); + const lastCodeUnit = + StringPrototypeCharCodeAt(chunk, chunk.length - 1); + if (0xD800 <= lastCodeUnit && lastCodeUnit <= 0xDBFF) { + this.#pendingHighSurrogate = + StringPrototypeSlice(chunk, -1); + chunk = StringPrototypeSlice(chunk, 0, -1); + if (chunk.length === 0) + return; } + controller.enqueue(this.#handle.encode(chunk)); }, flush: (controller) => { // https://encoding.spec.whatwg.org/#encode-and-flush @@ -137,7 +133,7 @@ class TextDecoderStream { if (chunk === undefined) { throw new ERR_INVALID_ARG_TYPE('chunk', 'string', chunk); } - const value = this.#handle.decode(chunk, { stream: true }); + const value = this.#handle.decode(chunk, kDecodeStreamingOptions); if (value) controller.enqueue(value); }, diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index ef2ec6214ade..dfb3b7e11af5 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -112,6 +112,7 @@ const { getNonWritablePropertyDescriptor, isBrandCheck, kEmptyQueue, + kParkedAlgorithmResult, kResolvedPromise, kState, kType, @@ -1446,19 +1447,85 @@ function readableStreamFromIterable(iterable) { if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) { throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object'); } + // Per GetIteratorDirect, the next method is looked up once. + const nextMethod = iterator.next; const startAlgorithm = nonOpCallback; - async function pullAlgorithm() { - const iterResult = await iterator.next(); + // Callback-style pull: the reaction steps are reused across chunks and + // completion is delivered to the controller's cached pull reactions + // (the kParkedAlgorithmResult contract). One pull runs at a time, so a + // single slot carries a non-thenable next() result between steps. + let pendingIterResult; + + function rejectPull(error) { + readableStreamDefaultControllerError(stream[kState].controller, error); + } + + function processIterResult(iterResult) { + const controller = stream[kState].controller; if (typeof iterResult !== 'object' || iterResult === null) { - throw new ERR_INVALID_STATE.TypeError( - 'The promise returned by the iterator.next() method must fulfill with an object'); + rejectPull(new ERR_INVALID_STATE.TypeError( + 'The promise returned by the iterator.next() method must fulfill with an object')); + return; } - if (iterResult.done) { - readableStreamDefaultControllerClose(stream[kState].controller); - } else { - readableStreamDefaultControllerEnqueue(stream[kState].controller, await iterResult.value); + try { + if (iterResult.done) { + readableStreamDefaultControllerClose(controller); + } else { + const value = iterResult.value; + if (value !== null && + (typeof value === 'object' || typeof value === 'function')) { + // Adopted like `await iterResult.value`, keeping the observable + // .then lookup on plain objects. + PromisePrototypeThen(PromiseResolve(value), enqueueValue, rejectPull); + return; + } + readableStreamDefaultControllerEnqueue(controller, value); + } + } catch (error) { + rejectPull(error); + return; + } + // pullFulfilled exists: the controller creates it before the pull. + controller[kState].pullFulfilled(); + } + + function enqueueValue(value) { + const controller = stream[kState].controller; + try { + readableStreamDefaultControllerEnqueue(controller, value); + } catch (error) { + rejectPull(error); + return; + } + controller[kState].pullFulfilled(); + } + + function processPendingIterResult() { + const iterResult = pendingIterResult; + pendingIterResult = undefined; + processIterResult(iterResult); + } + + function pullAlgorithm() { + let nextResult; + try { + nextResult = FunctionPrototypeCall(nextMethod, iterator); + } catch (error) { + return PromiseReject(error); + } + if (nextResult !== null && + (typeof nextResult === 'object' || typeof nextResult === 'function')) { + // Mirrors `await iterator.next()`: processIterResult runs at the + // microtask position the await resumed. + PromisePrototypeThen( + PromiseResolve(nextResult), processIterResult, rejectPull); + return kParkedAlgorithmResult; } + // A non-thenable next() result fails validation a microtask later. + pendingIterResult = nextResult; + PromisePrototypeThen(kResolvedPromise, processPendingIterResult); + return kParkedAlgorithmResult; } async function cancelAlgorithm(reason) { diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js index 9598796f35c8..9a93a2b17d41 100644 --- a/lib/internal/webstreams/util.js +++ b/lib/internal/webstreams/util.js @@ -359,7 +359,7 @@ const kResolvedPromise = PromiseResolve(); // operation and takes responsibility for delivering the fulfilled (or // rejected) continuation itself later, instead of settling a promise // (see the transform stream source pull algorithm). -const kParkedAlgorithmResult = { __proto__: null }; +const kParkedAlgorithmResult = Symbol('kParkedAlgorithmResult'); // Wires the (possibly non-thenable) result of an underlying algorithm // callback to its fulfilled/rejected continuations. A non-thenable result From e8cec4dd8e7d7b7b5bc476b1f6efd63bd290f99e Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Mon, 24 Aug 2026 20:49:15 -0400 Subject: [PATCH 93/97] crypto: update root certificates to NSS 3.126 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the certdata.txt[0] from NSS 3.126. This is the version of NSS that shipped in Firefox 154.0 on 2026-08-18. Certificates added: - SECOM TLS RSA Root CA 2024 - SECOM TLS ECC Root CA 2024 - Telia EC TLS Root CA v3 - Telia RSA TLS Root CA v3 Certificates removed: - ePKI Root Certification Authority - Atos TrustedRoot 2011 [0] https://raw.githubusercontent.com/nss-dev/nss/refs/tags/NSS_3_126_RTM/lib/ckfw/builtins/certdata.txt PR-URL: https://github.com/nodejs/node/pull/65495 Reviewed-By: René Reviewed-By: Luigi Pinca Reviewed-By: Colin Ihrig --- src/node_root_certs.h | 145 +++-- tools/certdata.txt | 1187 +++++++++++++++++++++++++++++++++++------ 2 files changed, 1103 insertions(+), 229 deletions(-) diff --git a/src/node_root_certs.h b/src/node_root_certs.h index 517dc8814c6d..1d3af4841611 100644 --- a/src/node_root_certs.h +++ b/src/node_root_certs.h @@ -17,38 +17,6 @@ "V9mSOdY=\n" "-----END CERTIFICATE-----", -/* ePKI Root Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYD\n" -"VQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsM\n" -"IWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0z\n" -"NDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29t\n" -"IENvLiwgTHRkLjEqMCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5\n" -"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U\n" -"82N0ywEhajfqhFAHSyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrB\n" -"p0xtInAhijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X\n" -"DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZr\n" -"xQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ad\n" -"o4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffAsgRFelQArr5T9rXn4fg8ozHS\n" -"qf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ETOxQvdibBjWzwloPn9s9h6PYq2l\n" -"Y9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUa\n" -"dCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+Xk\n" -"wY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3\n" -"pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF\n" -"MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLHClZ87lt4\n" -"DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B01GqZ\n" -"NF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq\n" -"KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnV\n" -"vwdVxrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltab\n" -"rNMdjmEPNXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc\n" -"7b3jajWvY9+rGNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8\n" -"GrBQAuUBo2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS\n" -"/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C\n" -"6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yI\n" -"VMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4\n" -"EZw=\n" -"-----END CERTIFICATE-----", - /* NetLock Arany (Class Gold) Főtanúsítvány */ "-----BEGIN CERTIFICATE-----\n" "MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTER\n" @@ -569,27 +537,6 @@ "gwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg==\n" "-----END CERTIFICATE-----", -/* Atos TrustedRoot 2011 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRv\n" -"cyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3\n" -"MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3Qg\n" -"MjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IB\n" -"DwAwggEKAoIBAQCVhTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI41\n" -"9KkM/IL9bcFyYie96mvr54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+\n" -"yj5vdHLqqjAqc2K+SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFs\n" -"Q/H3NYkQ4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L\n" -"cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMB\n" -"AAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/\n" -"MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgGA1UdIAQRMA8wDQYLKwYBBAGw\n" -"LQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4G\n" -"kGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0\n" -"BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQP\n" -"OLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYV\n" -"qL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv\n" -"KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed\n" -"-----END CERTIFICATE-----", - /* QuoVadis Root CA 1 G3 */ "-----BEGIN CERTIFICATE-----\n" "MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDEL\n" @@ -2895,4 +2842,96 @@ "hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZlC9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxH\n" "x1YwlGhza9IuqbnZLBwpvQy6uWWL\n" "-----END CERTIFICATE-----", + +/* SECOM TLS RSA Root CA 2024 */ +"-----BEGIN CERTIFICATE-----\n" +"MIIFmjCCA4KgAwIBAgIJAO6JNNDLgOCyMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAkpQ\n" +"MSYwJAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VD\n" +"T00gVExTIFJTQSBSb290IENBIDIwMjQwHhcNMjQwMTMxMDUxMTU1WhcNNDkwMTE0MDUxMTU1\n" +"WjBaMQswCQYDVQQGEwJKUDEmMCQGA1UEChMdU0VDT00gVHJ1c3QgU3lzdGVtcyBDby4sIEx0\n" +"ZC4xIzAhBgNVBAMTGlNFQ09NIFRMUyBSU0EgUm9vdCBDQSAyMDI0MIICIjANBgkqhkiG9w0B\n" +"AQEFAAOCAg8AMIICCgKCAgEA4TjizUwzxbInq8Tx11gaFYNk5fO+34y7TyM4neh0UgL5JIZb\n" +"JNLTz2x//L/B71+5m6X6nGIr7d4lFJBGtjO677hXOz93zkcWaUTm3VbOAjBlt4YWxlcccBHX\n" +"uZ7o3Q+4R+ormrBdHeJ1CTUEG8ttQbKIl3G7OZYbnH8/pP8cjPub/0kDVNuMzp7xsVRROOis\n" +"Qt53fMoJLlYgoebbuMphOqMCtjkJ7R6efEMfLp8UAVi9ZaLRn76ET/CJkk925nduuufC4Bat\n" +"S4mnXFmxN0vUXb0ij9B8O/D8gixQEsVSD4GK8FWRPh3bVd/6bzdkHGJjy21XI0yejVomZUbR\n" +"rOfNuz0boPGV1pt18fFC39IHQEth3OFqb5NDO3L+A9bNqTgAyUgRmIn4ucgDc/Ri/Km3V51u\n" +"eZjy1/yk0qwJVadAVVrCt56iNeXOyEvzJADGgDQ8E1Pdaqct8Cynz/47ReQM62vFYO08wcQk\n" +"rjmX/tesiko1V1yyaf6EfPzUFzmaGy9xvkCwdbm15EdTolOjE0H2Vb5/APDOyCFEokiYGmXT\n" +"LdAUl0wKZ4IyjkHGzy0jhpaXEXE/GJcEvI6VzEchjaBL03EJ0h9pG4OqeIOycKvAo3A+Tbet\n" +"yfsrgYyHzU0a7/qUjGat1AAq1nVljMpKqpinPTsf/d9H39FTUeJL7TpzzjUCAwEAAaNjMGEw\n" +"HQYDVR0OBBYEFCzrchKOWHdkNRVWNQFXB6l9DTbmMB8GA1UdIwQYMBaAFCzrchKOWHdkNRVW\n" +"NQFXB6l9DTbmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB\n" +"DAUAA4ICAQAVwsvluSafaez5tFPR/hRTBzRxEyMMQF3XJXCVi3yegZyKoec7hmE6jx2ZM8Kg\n" +"M1kn2yJRwFXHX8zUW9nBLEDWc4wuE8LrlZqhGZM9pJQXGmGzResDJV6JgRBna+j4sA1M7yId\n" +"lvL0sAfFXFCTRaWTD4E1V99RLrFzWfTcC+e180hDuNMpqOEo46+lMeW/Wvh7ifOQs+kiK0O2\n" +"gHxQDNxslSavnCs4V7l8HRDJ2La10o70Bo7VLzf1W8MBvv0VTnxB+NjT5qTAbhGFh9Gvp4Ba\n" +"JpmdUf0C5CEP6dbQlfgxWfzYr69yVT6dPQB+GFEaY03IMY+AcBCs+om1fNxrQXt9zoofMBNF\n" +"bLhvpNH/JsXWdGUzfNbO12uswTa5wah8LB18FTQN2/zPHYmvBEoLuyUgZ09VNLJo5YA0kXIt\n" +"VYkLjMe2SixzK4scUHv81IK99I91DWx7FwMVKw2xgFp+ZLYB2dnpQQrqwlW64glHUcK2N9BD\n" +"snjLSxeZ+UPECh9RxH4WAcKiZW+cqaKMmhP2WBfR4IcR7NOL32ml11ds87hhV1CZWWFCJAcC\n" +"idYZz6CZa8exzHojP9SB5RH0/v1KdHAisqhSjtJl/UIAHIQ48elOn8wrTdFap4Yb5aHglmMe\n" +"Nx+fAIhDluWVfxTO7H4dTPU+SFVRMLAh+wwKZfqb94nMeQ==\n" +"-----END CERTIFICATE-----", + +/* SECOM TLS ECC Root CA 2024 */ +"-----BEGIN CERTIFICATE-----\n" +"MIICTDCCAdGgAwIBAgIJAIF6LO+PI3pEMAoGCCqGSM49BAMDMFoxCzAJBgNVBAYTAkpQMSYw\n" +"JAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VDT00g\n" +"VExTIEVDQyBSb290IENBIDIwMjQwHhcNMjQwMTMxMDU1MjM0WhcNNDkwMTE0MDU1MjM0WjBa\n" +"MQswCQYDVQQGEwJKUDEmMCQGA1UEChMdU0VDT00gVHJ1c3QgU3lzdGVtcyBDby4sIEx0ZC4x\n" +"IzAhBgNVBAMTGlNFQ09NIFRMUyBFQ0MgUm9vdCBDQSAyMDI0MHYwEAYHKoZIzj0CAQYFK4EE\n" +"ACIDYgAE7NzFMtu9dzQXSNC12fabk0+GlC5finB3R7XaZonRUd20aFiWObtuNBCLUZSfk6QX\n" +"AE55BjEXsXQ/NG8yUqicXjsu9ksDK3JZBgCwLOVh6+nwJXTvso/dEj/GUYH5mBdoo2MwYTAd\n" +"BgNVHQ4EFgQUO3YReyl04k4GTFaCQNAhL3qzydUwHwYDVR0jBBgwFoAUO3YReyl04k4GTFaC\n" +"QNAhL3qzydUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwMD\n" +"aQAwZgIxAN3ib8fi1pMYtAPjMilB5e5/H+t5CL0xPL+cZ5oTTZuSCjpAn1v7F/VAr8bFxQXA\n" +"owIxAKsBVO1ACFp7skwzPvdv1EUY5a897WGLT4lb+bjxFAWyl8wDcZJdwGZ/pAHxt1AJ1g==\n" +"-----END CERTIFICATE-----", + +/* Telia EC TLS Root CA v3 */ +"-----BEGIN CERTIFICATE-----\n" +"MIICMjCCAbegAwIBAgIPAYvSIlRjTQSLbOVHH9K1MAoGCCqGSM49BAMDMEoxCzAJBgNVBAYT\n" +"AlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMSAwHgYDVQQDDBdUZWxpYSBFQyBUTFMg\n" +"Um9vdCBDQSB2MzAeFw0yMzExMTUwODU1MjZaFw00ODA1MjMxMTAwMDBaMEoxCzAJBgNVBAYT\n" +"AlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMSAwHgYDVQQDDBdUZWxpYSBFQyBUTFMg\n" +"Um9vdCBDQSB2MzB2MBAGByqGSM49AgEGBSuBBAAiA2IABMHIlhVDLbmFKUpW0iK4dpryT6em\n" +"YOeS31JPwWnWPmkWRrAkTbPX40sQfHI9mpR7Rbktu3ngg6W+BBSXSechtMCnBmWXj/EaVlmV\n" +"5cY1jD2HoTfhBQ3AacpCNMLJK4NpZaNjMGEwHwYDVR0jBBgwFoAU1GToQ4g6cy/QGnGCNgte\n" +"hd7H3kMwHQYDVR0OBBYEFNRk6EOIOnMv0BpxgjYLXoXex95DMA4GA1UdDwEB/wQEAwIBBjAP\n" +"BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2kAMGYCMQCXAUdS/9bbJ8A1JYaGf/bWt/s7\n" +"Ta0ot5Ulno8OjSNYRWQIlS4tVWldvTAVA7heOFgCMQCvKr8+Z2Rn+OBr5UHzlgBObpad1Luw\n" +"NTRcdNgUJxIWadcki+UBLEi1/AURKV5md2M=\n" +"-----END CERTIFICATE-----", + +/* Telia RSA TLS Root CA v3 */ +"-----BEGIN CERTIFICATE-----\n" +"MIIFgjCCA2qgAwIBAgIPAYvSUKtCVSxHWr2h3BrFMA0GCSqGSIb3DQEBDAUAMEsxCzAJBgNV\n" +"BAYTAlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMSEwHwYDVQQDDBhUZWxpYSBSU0Eg\n" +"VExTIFJvb3QgQ0EgdjMwHhcNMjMxMTE1MDk0NzQyWhcNNDgwNTIzMTEwMDAwWjBLMQswCQYD\n" +"VQQGEwJTRTEZMBcGA1UECgwQVGVsaWEgQ29tcGFueSBBQjEhMB8GA1UEAwwYVGVsaWEgUlNB\n" +"IFRMUyBSb290IENBIHYzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsV89KG19\n" +"hCf4S1Fvk8D3TyDERhmcvx8F7Kmb4WATx3ije1id3KHxRE0TKmcNCbAQ57bvHFEYa4hR2l20\n" +"VjVadExqOW+2ld99MbEiO+jRVOz+BbxLxJnmGwCqI+BfuTjjVReDxsxjQvjgBsClaO/sm5i7\n" +"0nlZcWGRtIkvWDK3NNkT5RtwXc/O8NTFVpbUqT6cRjIj3olAblR+lRf4Ffy5o+Q9fabjYn9Z\n" +"9S4itruElcEFf9Ljk7fwdTycT/rvJW9w/B3G2a3r0f/zXNOVruIBcqE6pkSospACU2bG42fY\n" +"KrbM/GWnp7u+p9Frz4jaNwpb4YHuEeS8BratNcP8X62jXIvvKHxlsMDJCnb4U8JzFOLsU6mo\n" +"hVY58BdZrvi0Gk9UOuqmgoG6dskHoksjZTlK61D/InzmEoA1yAYJFDVysjRxDUOu9cAwANbq\n" +"mq77WIFL6BpnZgVqPtMfG6wN8BrTKdapvilVsYR59BFgIsAVBMxrGh+W+QcvmJafUpASvlAr\n" +"KvVG2FI4i6PiLjSBT0+6F6EQLrYqefOQF/fBNEXb+njUQ0SUVrAqtH4Y+OjCI/a4/JJQppxe\n" +"emZcQ0SUShgiI5AM5xHO5iyaUrTjYH4zxUz9j+1FEbDH/xpstr1gXBykspup+hRTaJcbA+Ub\n" +"pJqtWZndAPddJmt6YJQ+dU3pDu8CAwEAAaNjMGEwHwYDVR0jBBgwFoAUsMep0t2yKFZzBJSM\n" +"FFxIbzdSkqgwHQYDVR0OBBYEFLDHqdLdsihWcwSUjBRcSG83UpKoMA4GA1UdDwEB/wQEAwIB\n" +"BjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQBdYzFsNGDRk7bR/AgRKq+5\n" +"637YuOW+w6uhpoS0VnKMUpyHCwku86hEvqivakPtfmlm4bFwt++sb/8OXsWBqtfbXMaBNDTZ\n" +"l8XRMJuLWOW2JrbKkRzgG0eBUcvsadG1rrhbmZqYvFXaAZO7o4TdOZzxhBB5GOAWWXB3Iera\n" +"NP4J63zyo9n8Gqw3sJBG44em5hoYjBffP+npibyslnslRi4L6xHsCYj/Pab+OlqbMCB6v+sT\n" +"CLeEIukRVzoR9aQ45pEK7Z1QBnSsbAKQtss0JKD9d/mX143H1xePjPhTXlv5JCkhrcj+SShz\n" +"0P9+EHoWe6m9lyUEOIVn0rp+yVJWNbmyDv3VkwFxHC1ApSQsgSimjGQ4wtr6cSmordYxkV+R\n" +"o8lOIIhRksXPyDk27gW6IjUXCkZKpxFjkL3jiBSc8SkxnwCWtXg8xwNwdFVNBGLCCuJnsneY\n" +"XjJNqzRqUcoGwzsvF3Qi/ZnHUNvISdevlgIAXL4Wvrxaqvoa01wB+GCfs57RTGE4TvAGhKNK\n" +"us8K3hRT1BSpigzMIRzSxtAOrqPN6j//QSmW9f8Jcncri4j2ihSpVrFU0NdNkMhZeAKidTFP\n" +"sxCVFuW4Aniz7jqiw5sWtjbQrlW035izIEU4sYwQoC1Nx0Svy+mMTRai50LqFQ+A1/Hq6xHH\n" +"DNx7CI83d23Erw==\n" +"-----END CERTIFICATE-----", #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/tools/certdata.txt b/tools/certdata.txt index fafc33ddeea5..f2f8edc685ad 100644 --- a/tools/certdata.txt +++ b/tools/certdata.txt @@ -2453,181 +2453,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE -# -# Certificate "ePKI Root Certification Authority" -# -# Issuer: OU=ePKI Root Certification Authority,O="Chunghwa Telecom Co., Ltd.",C=TW -# Serial Number:15:c8:bd:65:47:5c:af:b8:97:00:5e:e4:06:d2:bc:9d -# Subject: OU=ePKI Root Certification Authority,O="Chunghwa Telecom Co., Ltd.",C=TW -# Not Valid Before: Mon Dec 20 02:31:27 2004 -# Not Valid After : Wed Dec 20 02:31:27 2034 -# Fingerprint (SHA-256): C0:A6:F4:DC:63:A2:4B:FD:CF:54:EF:2A:6A:08:2A:0A:72:DE:35:80:3E:2F:F5:FF:52:7A:E5:D8:72:06:DF:D5 -# Fingerprint (SHA1): 67:65:0D:F1:7E:8E:7E:5B:82:40:A4:F4:56:4B:CF:E2:3D:69:C6:F0 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "ePKI Root Certification Authority" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\136\061\013\060\011\006\003\125\004\006\023\002\124\127\061 -\043\060\041\006\003\125\004\012\014\032\103\150\165\156\147\150 -\167\141\040\124\145\154\145\143\157\155\040\103\157\056\054\040 -\114\164\144\056\061\052\060\050\006\003\125\004\013\014\041\145 -\120\113\111\040\122\157\157\164\040\103\145\162\164\151\146\151 -\143\141\164\151\157\156\040\101\165\164\150\157\162\151\164\171 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\136\061\013\060\011\006\003\125\004\006\023\002\124\127\061 -\043\060\041\006\003\125\004\012\014\032\103\150\165\156\147\150 -\167\141\040\124\145\154\145\143\157\155\040\103\157\056\054\040 -\114\164\144\056\061\052\060\050\006\003\125\004\013\014\041\145 -\120\113\111\040\122\157\157\164\040\103\145\162\164\151\146\151 -\143\141\164\151\157\156\040\101\165\164\150\157\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\025\310\275\145\107\134\257\270\227\000\136\344\006\322 -\274\235 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\005\260\060\202\003\230\240\003\002\001\002\002\020\025 -\310\275\145\107\134\257\270\227\000\136\344\006\322\274\235\060 -\015\006\011\052\206\110\206\367\015\001\001\005\005\000\060\136 -\061\013\060\011\006\003\125\004\006\023\002\124\127\061\043\060 -\041\006\003\125\004\012\014\032\103\150\165\156\147\150\167\141 -\040\124\145\154\145\143\157\155\040\103\157\056\054\040\114\164 -\144\056\061\052\060\050\006\003\125\004\013\014\041\145\120\113 -\111\040\122\157\157\164\040\103\145\162\164\151\146\151\143\141 -\164\151\157\156\040\101\165\164\150\157\162\151\164\171\060\036 -\027\015\060\064\061\062\062\060\060\062\063\061\062\067\132\027 -\015\063\064\061\062\062\060\060\062\063\061\062\067\132\060\136 -\061\013\060\011\006\003\125\004\006\023\002\124\127\061\043\060 -\041\006\003\125\004\012\014\032\103\150\165\156\147\150\167\141 -\040\124\145\154\145\143\157\155\040\103\157\056\054\040\114\164 -\144\056\061\052\060\050\006\003\125\004\013\014\041\145\120\113 -\111\040\122\157\157\164\040\103\145\162\164\151\146\151\143\141 -\164\151\157\156\040\101\165\164\150\157\162\151\164\171\060\202 -\002\042\060\015\006\011\052\206\110\206\367\015\001\001\001\005 -\000\003\202\002\017\000\060\202\002\012\002\202\002\001\000\341 -\045\017\356\215\333\210\063\165\147\315\255\037\175\072\116\155 -\235\323\057\024\363\143\164\313\001\041\152\067\352\204\120\007 -\113\046\133\011\103\154\041\236\152\310\325\003\365\140\151\217 -\314\360\042\344\037\347\367\152\042\061\267\054\025\362\340\376 -\000\152\103\377\207\145\306\265\032\301\247\114\155\042\160\041 -\212\061\362\227\164\211\011\022\046\034\236\312\331\022\242\225 -\074\332\351\147\277\010\240\144\343\326\102\267\105\357\227\364 -\366\365\327\265\112\025\002\130\175\230\130\113\140\274\315\327 -\015\232\023\063\123\321\141\371\172\325\327\170\263\232\063\367 -\000\206\316\035\115\224\070\257\250\354\170\121\160\212\134\020 -\203\121\041\367\021\075\064\206\136\345\110\315\227\201\202\065 -\114\031\354\145\366\153\305\005\241\356\107\023\326\263\041\047 -\224\020\012\331\044\073\272\276\104\023\106\060\077\227\074\330 -\327\327\152\356\073\070\343\053\324\227\016\271\033\347\007\111 -\177\067\052\371\167\170\317\124\355\133\106\235\243\200\016\221 -\103\301\326\133\137\024\272\237\246\215\044\107\100\131\277\162 -\070\262\066\154\067\377\231\321\135\016\131\012\253\151\367\300 -\262\004\105\172\124\000\256\276\123\366\265\347\341\370\074\243 -\061\322\251\376\041\122\144\305\246\147\360\165\007\006\224\024 -\201\125\306\047\344\001\217\027\301\152\161\327\276\113\373\224 -\130\175\176\021\063\261\102\367\142\154\030\326\317\011\150\076 -\177\154\366\036\217\142\255\245\143\333\011\247\037\042\102\101 -\036\157\231\212\076\327\371\077\100\172\171\260\245\001\222\322 -\235\075\010\025\245\020\001\055\263\062\166\250\225\015\263\172 -\232\373\007\020\170\021\157\341\217\307\272\017\045\032\164\052 -\345\034\230\101\231\337\041\207\350\225\006\152\012\263\152\107 -\166\145\366\072\317\217\142\027\031\173\012\050\315\032\322\203 -\036\041\307\054\277\276\377\141\150\267\147\033\273\170\115\215 -\316\147\345\344\301\216\267\043\146\342\235\220\165\064\230\251 -\066\053\212\232\224\271\235\354\314\212\261\370\045\211\134\132 -\266\057\214\037\155\171\044\247\122\150\303\204\065\342\146\215 -\143\016\045\115\325\031\262\346\171\067\247\042\235\124\061\002 -\003\001\000\001\243\152\060\150\060\035\006\003\125\035\016\004 -\026\004\024\036\014\367\266\147\362\341\222\046\011\105\300\125 -\071\056\167\077\102\112\242\060\014\006\003\125\035\023\004\005 -\060\003\001\001\377\060\071\006\004\147\052\007\000\004\061\060 -\057\060\055\002\001\000\060\011\006\005\053\016\003\002\032\005 -\000\060\007\006\005\147\052\003\000\000\004\024\105\260\302\307 -\012\126\174\356\133\170\014\225\371\030\123\301\246\034\330\020 -\060\015\006\011\052\206\110\206\367\015\001\001\005\005\000\003 -\202\002\001\000\011\263\203\123\131\001\076\225\111\271\361\201 -\272\371\166\040\043\265\047\140\164\324\152\231\064\136\154\000 -\123\331\237\362\246\261\044\007\104\152\052\306\245\216\170\022 -\350\107\331\130\033\023\052\136\171\233\237\012\052\147\246\045 -\077\006\151\126\163\303\212\146\110\373\051\201\127\164\006\312 -\234\352\050\350\070\147\046\053\361\325\265\077\145\223\370\066 -\135\216\215\215\100\040\207\031\352\357\047\300\075\264\071\017 -\045\173\150\120\164\125\234\014\131\175\132\075\101\224\045\122 -\010\340\107\054\025\061\031\325\277\007\125\306\273\022\265\227 -\364\137\203\205\272\161\301\331\154\201\021\166\012\012\260\277 -\202\227\367\352\075\372\372\354\055\251\050\224\073\126\335\322 -\121\056\256\300\275\010\025\214\167\122\064\226\326\233\254\323 -\035\216\141\017\065\173\233\256\071\151\013\142\140\100\040\066 -\217\257\373\066\356\055\010\112\035\270\277\233\134\370\352\245 -\033\240\163\246\330\370\156\340\063\004\137\150\252\047\207\355 -\331\301\220\234\355\275\343\152\065\257\143\337\253\030\331\272 -\346\351\112\352\120\212\017\141\223\036\342\055\031\342\060\224 -\065\222\135\016\266\007\257\031\200\217\107\220\121\113\056\115 -\335\205\342\322\012\122\012\027\232\374\032\260\120\002\345\001 -\243\143\067\041\114\104\304\233\121\231\021\016\163\234\006\217 -\124\056\247\050\136\104\071\207\126\055\067\275\205\104\224\341 -\014\113\054\234\303\222\205\064\141\313\017\270\233\112\103\122 -\376\064\072\175\270\351\051\334\166\251\310\060\370\024\161\200 -\306\036\066\110\164\042\101\134\207\202\350\030\161\213\101\211 -\104\347\176\130\133\250\270\215\023\351\247\154\303\107\355\263 -\032\235\142\256\215\202\352\224\236\335\131\020\303\255\335\342 -\115\343\061\325\307\354\350\362\260\376\222\036\026\012\032\374 -\331\363\370\047\266\311\276\035\264\154\144\220\177\364\344\304 -\133\327\067\256\102\016\335\244\032\157\174\210\124\305\026\156 -\341\172\150\056\370\072\277\015\244\074\211\073\170\247\116\143 -\203\004\041\010\147\215\362\202\111\320\133\375\261\315\017\203 -\204\324\076\040\205\367\112\075\053\234\375\052\012\011\115\352 -\201\370\021\234 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -# For Server Distrust After: Tue Apr 15 23:59:59 2025 -CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL -\062\065\060\064\061\065\062\063\065\071\065\071\132 -END -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "ePKI Root Certification Authority" -# Issuer: OU=ePKI Root Certification Authority,O="Chunghwa Telecom Co., Ltd.",C=TW -# Serial Number:15:c8:bd:65:47:5c:af:b8:97:00:5e:e4:06:d2:bc:9d -# Subject: OU=ePKI Root Certification Authority,O="Chunghwa Telecom Co., Ltd.",C=TW -# Not Valid Before: Mon Dec 20 02:31:27 2004 -# Not Valid After : Wed Dec 20 02:31:27 2034 -# Fingerprint (SHA-256): C0:A6:F4:DC:63:A2:4B:FD:CF:54:EF:2A:6A:08:2A:0A:72:DE:35:80:3E:2F:F5:FF:52:7A:E5:D8:72:06:DF:D5 -# Fingerprint (SHA1): 67:65:0D:F1:7E:8E:7E:5B:82:40:A4:F4:56:4B:CF:E2:3D:69:C6:F0 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "ePKI Root Certification Authority" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\147\145\015\361\176\216\176\133\202\100\244\364\126\113\317\342 -\075\151\306\360 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\033\056\000\312\046\006\220\075\255\376\157\025\150\323\153\263 -END -CKA_ISSUER MULTILINE_OCTAL -\060\136\061\013\060\011\006\003\125\004\006\023\002\124\127\061 -\043\060\041\006\003\125\004\012\014\032\103\150\165\156\147\150 -\167\141\040\124\145\154\145\143\157\155\040\103\157\056\054\040 -\114\164\144\056\061\052\060\050\006\003\125\004\013\014\041\145 -\120\113\111\040\122\157\157\164\040\103\145\162\164\151\146\151 -\143\141\164\151\157\156\040\101\165\164\150\157\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\025\310\275\145\107\134\257\270\227\000\136\344\006\322 -\274\235 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - # # Certificate "NetLock Arany (Class Gold) Főtanúsítvány" # @@ -5846,7 +5671,7 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\010\134\063\313\142\054\137\263\062 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -24763,3 +24588,1013 @@ CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE + +# +# Certificate "SECOM TLS RSA Root CA 2024" +# +# Issuer: CN=SECOM TLS RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Serial Number:00:ee:89:34:d0:cb:80:e0:b2 +# Subject: CN=SECOM TLS RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Not Valid Before: Wed Jan 31 05:11:55 2024 +# Not Valid After : Thu Jan 14 05:11:55 2049 +# Fingerprint (SHA-256): 14:35:F2:25:C5:D2:52:D7:A2:19:48:CC:3C:E6:2A:EC:FA:88:00:1E:3D:D7:2D:1C:C3:55:51:00:EB:37:2F:93 +# Fingerprint (SHA1): FB:97:96:7C:EF:8D:98:63:06:C0:3B:B6:11:F8:E0:13:97:A2:98:D3 +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "SECOM TLS RSA Root CA 2024" +CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 +CKA_SUBJECT MULTILINE_OCTAL +\060\132\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\043\060\041\006\003\125\004\003 +\023\032\123\105\103\117\115\040\124\114\123\040\122\123\101\040 +\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_ID UTF8 "0" +CKA_ISSUER MULTILINE_OCTAL +\060\132\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\043\060\041\006\003\125\004\003 +\023\032\123\105\103\117\115\040\124\114\123\040\122\123\101\040 +\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\011\000\356\211\064\320\313\200\340\262 +END +CKA_VALUE MULTILINE_OCTAL +\060\202\005\232\060\202\003\202\240\003\002\001\002\002\011\000 +\356\211\064\320\313\200\340\262\060\015\006\011\052\206\110\206 +\367\015\001\001\014\005\000\060\132\061\013\060\011\006\003\125 +\004\006\023\002\112\120\061\046\060\044\006\003\125\004\012\023 +\035\123\105\103\117\115\040\124\162\165\163\164\040\123\171\163 +\164\145\155\163\040\103\157\056\054\040\114\164\144\056\061\043 +\060\041\006\003\125\004\003\023\032\123\105\103\117\115\040\124 +\114\123\040\122\123\101\040\122\157\157\164\040\103\101\040\062 +\060\062\064\060\036\027\015\062\064\060\061\063\061\060\065\061 +\061\065\065\132\027\015\064\071\060\061\061\064\060\065\061\061 +\065\065\132\060\132\061\013\060\011\006\003\125\004\006\023\002 +\112\120\061\046\060\044\006\003\125\004\012\023\035\123\105\103 +\117\115\040\124\162\165\163\164\040\123\171\163\164\145\155\163 +\040\103\157\056\054\040\114\164\144\056\061\043\060\041\006\003 +\125\004\003\023\032\123\105\103\117\115\040\124\114\123\040\122 +\123\101\040\122\157\157\164\040\103\101\040\062\060\062\064\060 +\202\002\042\060\015\006\011\052\206\110\206\367\015\001\001\001 +\005\000\003\202\002\017\000\060\202\002\012\002\202\002\001\000 +\341\070\342\315\114\063\305\262\047\253\304\361\327\130\032\025 +\203\144\345\363\276\337\214\273\117\043\070\235\350\164\122\002 +\371\044\206\133\044\322\323\317\154\177\374\277\301\357\137\271 +\233\245\372\234\142\053\355\336\045\024\220\106\266\063\272\357 +\270\127\073\077\167\316\107\026\151\104\346\335\126\316\002\060 +\145\267\206\026\306\127\034\160\021\327\271\236\350\335\017\270 +\107\352\053\232\260\135\035\342\165\011\065\004\033\313\155\101 +\262\210\227\161\273\071\226\033\234\177\077\244\377\034\214\373 +\233\377\111\003\124\333\214\316\236\361\261\124\121\070\350\254 +\102\336\167\174\312\011\056\126\040\241\346\333\270\312\141\072 +\243\002\266\071\011\355\036\236\174\103\037\056\237\024\001\130 +\275\145\242\321\237\276\204\117\360\211\222\117\166\346\167\156 +\272\347\302\340\026\255\113\211\247\134\131\261\067\113\324\135 +\275\042\217\320\174\073\360\374\202\054\120\022\305\122\017\201 +\212\360\125\221\076\035\333\125\337\372\157\067\144\034\142\143 +\313\155\127\043\114\236\215\132\046\145\106\321\254\347\315\273 +\075\033\240\361\225\326\233\165\361\361\102\337\322\007\100\113 +\141\334\341\152\157\223\103\073\162\376\003\326\315\251\070\000 +\311\110\021\230\211\370\271\310\003\163\364\142\374\251\267\127 +\235\156\171\230\362\327\374\244\322\254\011\125\247\100\125\132 +\302\267\236\242\065\345\316\310\113\363\044\000\306\200\064\074 +\023\123\335\152\247\055\360\054\247\317\376\073\105\344\014\353 +\153\305\140\355\074\301\304\044\256\071\227\376\327\254\212\112 +\065\127\134\262\151\376\204\174\374\324\027\071\232\033\057\161 +\276\100\260\165\271\265\344\107\123\242\123\243\023\101\366\125 +\276\177\000\360\316\310\041\104\242\110\230\032\145\323\055\320 +\024\227\114\012\147\202\062\216\101\306\317\055\043\206\226\227 +\021\161\077\030\227\004\274\216\225\314\107\041\215\240\113\323 +\161\011\322\037\151\033\203\252\170\203\262\160\253\300\243\160 +\076\115\267\255\311\373\053\201\214\207\315\115\032\357\372\224 +\214\146\255\324\000\052\326\165\145\214\312\112\252\230\247\075 +\073\037\375\337\107\337\321\123\121\342\113\355\072\163\316\065 +\002\003\001\000\001\243\143\060\141\060\035\006\003\125\035\016 +\004\026\004\024\054\353\162\022\216\130\167\144\065\025\126\065 +\001\127\007\251\175\015\066\346\060\037\006\003\125\035\043\004 +\030\060\026\200\024\054\353\162\022\216\130\167\144\065\025\126 +\065\001\127\007\251\175\015\066\346\060\016\006\003\125\035\017 +\001\001\377\004\004\003\002\001\006\060\017\006\003\125\035\023 +\001\001\377\004\005\060\003\001\001\377\060\015\006\011\052\206 +\110\206\367\015\001\001\014\005\000\003\202\002\001\000\025\302 +\313\345\271\046\237\151\354\371\264\123\321\376\024\123\007\064 +\161\023\043\014\100\135\327\045\160\225\213\174\236\201\234\212 +\241\347\073\206\141\072\217\035\231\063\302\240\063\131\047\333 +\042\121\300\125\307\137\314\324\133\331\301\054\100\326\163\214 +\056\023\302\353\225\232\241\031\223\075\244\224\027\032\141\263 +\105\353\003\045\136\211\201\020\147\153\350\370\260\015\114\357 +\042\035\226\362\364\260\007\305\134\120\223\105\245\223\017\201 +\065\127\337\121\056\261\163\131\364\334\013\347\265\363\110\103 +\270\323\051\250\341\050\343\257\245\061\345\277\132\370\173\211 +\363\220\263\351\042\053\103\266\200\174\120\014\334\154\225\046 +\257\234\053\070\127\271\174\035\020\311\330\266\265\322\216\364 +\006\216\325\057\067\365\133\303\001\276\375\025\116\174\101\370 +\330\323\346\244\300\156\021\205\207\321\257\247\200\132\046\231 +\235\121\375\002\344\041\017\351\326\320\225\370\061\131\374\330 +\257\257\162\125\076\235\075\000\176\030\121\032\143\115\310\061 +\217\200\160\020\254\372\211\265\174\334\153\101\173\175\316\212 +\037\060\023\105\154\270\157\244\321\377\046\305\326\164\145\063 +\174\326\316\327\153\254\301\066\271\301\250\174\054\035\174\025 +\064\015\333\374\317\035\211\257\004\112\013\273\045\040\147\117 +\125\064\262\150\345\200\064\221\162\055\125\211\013\214\307\266 +\112\054\163\053\213\034\120\173\374\324\202\275\364\217\165\015 +\154\173\027\003\025\053\015\261\200\132\176\144\266\001\331\331 +\351\101\012\352\302\125\272\342\011\107\121\302\266\067\320\103 +\262\170\313\113\027\231\371\103\304\012\037\121\304\176\026\001 +\302\242\145\157\234\251\242\214\232\023\366\130\027\321\340\207 +\021\354\323\213\337\151\245\327\127\154\363\270\141\127\120\231 +\131\141\102\044\007\002\211\326\031\317\240\231\153\307\261\314 +\172\043\077\324\201\345\021\364\376\375\112\164\160\042\262\250 +\122\216\322\145\375\102\000\034\204\070\361\351\116\237\314\053 +\115\321\132\247\206\033\345\241\340\226\143\036\067\037\237\000 +\210\103\226\345\225\177\024\316\354\176\035\114\365\076\110\125 +\121\060\260\041\373\014\012\145\372\233\367\211\314\171 +END +CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE +CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE +CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE + +# Trust for "SECOM TLS RSA Root CA 2024" +# Issuer: CN=SECOM TLS RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Serial Number:00:ee:89:34:d0:cb:80:e0:b2 +# Subject: CN=SECOM TLS RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Not Valid Before: Wed Jan 31 05:11:55 2024 +# Not Valid After : Thu Jan 14 05:11:55 2049 +# Fingerprint (SHA-256): 14:35:F2:25:C5:D2:52:D7:A2:19:48:CC:3C:E6:2A:EC:FA:88:00:1E:3D:D7:2D:1C:C3:55:51:00:EB:37:2F:93 +# Fingerprint (SHA1): FB:97:96:7C:EF:8D:98:63:06:C0:3B:B6:11:F8:E0:13:97:A2:98:D3 +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "SECOM TLS RSA Root CA 2024" +CKA_CERT_SHA1_HASH MULTILINE_OCTAL +\373\227\226\174\357\215\230\143\006\300\073\266\021\370\340\023 +\227\242\230\323 +END +CKA_CERT_MD5_HASH MULTILINE_OCTAL +\320\244\333\062\353\104\230\322\142\013\076\274\115\174\134\351 +END +CKA_ISSUER MULTILINE_OCTAL +\060\132\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\043\060\041\006\003\125\004\003 +\023\032\123\105\103\117\115\040\124\114\123\040\122\123\101\040 +\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\011\000\356\211\064\320\313\200\340\262 +END +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE + +# +# Certificate "SECOM TLS ECC Root CA 2024" +# +# Issuer: CN=SECOM TLS ECC Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Serial Number:00:81:7a:2c:ef:8f:23:7a:44 +# Subject: CN=SECOM TLS ECC Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Not Valid Before: Wed Jan 31 05:52:34 2024 +# Not Valid After : Thu Jan 14 05:52:34 2049 +# Fingerprint (SHA-256): 6A:B2:AB:75:F5:1C:B4:F4:F0:15:62:03:FB:F6:F6:46:23:2F:51:4B:E0:59:F6:28:33:30:8B:82:B4:D7:2D:B1 +# Fingerprint (SHA1): 7A:1F:22:2D:72:B2:C3:19:87:44:DB:61:69:E8:A6:4B:D7:0D:44:0E +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "SECOM TLS ECC Root CA 2024" +CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 +CKA_SUBJECT MULTILINE_OCTAL +\060\132\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\043\060\041\006\003\125\004\003 +\023\032\123\105\103\117\115\040\124\114\123\040\105\103\103\040 +\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_ID UTF8 "0" +CKA_ISSUER MULTILINE_OCTAL +\060\132\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\043\060\041\006\003\125\004\003 +\023\032\123\105\103\117\115\040\124\114\123\040\105\103\103\040 +\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\011\000\201\172\054\357\217\043\172\104 +END +CKA_VALUE MULTILINE_OCTAL +\060\202\002\114\060\202\001\321\240\003\002\001\002\002\011\000 +\201\172\054\357\217\043\172\104\060\012\006\010\052\206\110\316 +\075\004\003\003\060\132\061\013\060\011\006\003\125\004\006\023 +\002\112\120\061\046\060\044\006\003\125\004\012\023\035\123\105 +\103\117\115\040\124\162\165\163\164\040\123\171\163\164\145\155 +\163\040\103\157\056\054\040\114\164\144\056\061\043\060\041\006 +\003\125\004\003\023\032\123\105\103\117\115\040\124\114\123\040 +\105\103\103\040\122\157\157\164\040\103\101\040\062\060\062\064 +\060\036\027\015\062\064\060\061\063\061\060\065\065\062\063\064 +\132\027\015\064\071\060\061\061\064\060\065\065\062\063\064\132 +\060\132\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\043\060\041\006\003\125\004\003 +\023\032\123\105\103\117\115\040\124\114\123\040\105\103\103\040 +\122\157\157\164\040\103\101\040\062\060\062\064\060\166\060\020 +\006\007\052\206\110\316\075\002\001\006\005\053\201\004\000\042 +\003\142\000\004\354\334\305\062\333\275\167\064\027\110\320\265 +\331\366\233\223\117\206\224\056\137\212\160\167\107\265\332\146 +\211\321\121\335\264\150\130\226\071\273\156\064\020\213\121\224 +\237\223\244\027\000\116\171\006\061\027\261\164\077\064\157\062 +\122\250\234\136\073\056\366\113\003\053\162\131\006\000\260\054 +\345\141\353\351\360\045\164\357\262\217\335\022\077\306\121\201 +\371\230\027\150\243\143\060\141\060\035\006\003\125\035\016\004 +\026\004\024\073\166\021\173\051\164\342\116\006\114\126\202\100 +\320\041\057\172\263\311\325\060\037\006\003\125\035\043\004\030 +\060\026\200\024\073\166\021\173\051\164\342\116\006\114\126\202 +\100\320\041\057\172\263\311\325\060\016\006\003\125\035\017\001 +\001\377\004\004\003\002\001\006\060\017\006\003\125\035\023\001 +\001\377\004\005\060\003\001\001\377\060\012\006\010\052\206\110 +\316\075\004\003\003\003\151\000\060\146\002\061\000\335\342\157 +\307\342\326\223\030\264\003\343\062\051\101\345\356\177\037\353 +\171\010\275\061\074\277\234\147\232\023\115\233\222\012\072\100 +\237\133\373\027\365\100\257\306\305\305\005\300\243\002\061\000 +\253\001\124\355\100\010\132\173\262\114\063\076\367\157\324\105 +\030\345\257\075\355\141\213\117\211\133\371\270\361\024\005\262 +\227\314\003\161\222\135\300\146\177\244\001\361\267\120\011\326 +END +CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE +CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE +CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE + +# Trust for "SECOM TLS ECC Root CA 2024" +# Issuer: CN=SECOM TLS ECC Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Serial Number:00:81:7a:2c:ef:8f:23:7a:44 +# Subject: CN=SECOM TLS ECC Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Not Valid Before: Wed Jan 31 05:52:34 2024 +# Not Valid After : Thu Jan 14 05:52:34 2049 +# Fingerprint (SHA-256): 6A:B2:AB:75:F5:1C:B4:F4:F0:15:62:03:FB:F6:F6:46:23:2F:51:4B:E0:59:F6:28:33:30:8B:82:B4:D7:2D:B1 +# Fingerprint (SHA1): 7A:1F:22:2D:72:B2:C3:19:87:44:DB:61:69:E8:A6:4B:D7:0D:44:0E +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "SECOM TLS ECC Root CA 2024" +CKA_CERT_SHA1_HASH MULTILINE_OCTAL +\172\037\042\055\162\262\303\031\207\104\333\141\151\350\246\113 +\327\015\104\016 +END +CKA_CERT_MD5_HASH MULTILINE_OCTAL +\231\323\235\344\322\261\055\360\052\004\147\205\363\337\106\326 +END +CKA_ISSUER MULTILINE_OCTAL +\060\132\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\043\060\041\006\003\125\004\003 +\023\032\123\105\103\117\115\040\124\114\123\040\105\103\103\040 +\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\011\000\201\172\054\357\217\043\172\104 +END +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE + +# +# Certificate "SECOM SMIME RSA Root CA 2024" +# +# Issuer: CN=SECOM SMIME RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Serial Number:00:dd:a9:db:9e:7e:bc:d4:6d +# Subject: CN=SECOM SMIME RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Not Valid Before: Wed Jan 31 06:23:06 2024 +# Not Valid After : Thu Jan 14 06:23:06 2049 +# Fingerprint (SHA-256): 36:29:E7:18:8E:00:A7:CB:32:32:C4:42:6B:C8:49:12:F1:21:8B:1A:9A:E6:76:C0:B0:AB:E1:DB:FE:21:82:B5 +# Fingerprint (SHA1): 90:A5:E1:BD:C5:3F:69:08:C2:E3:73:9F:E7:55:E3:7F:75:F3:84:47 +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "SECOM SMIME RSA Root CA 2024" +CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 +CKA_SUBJECT MULTILINE_OCTAL +\060\134\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\045\060\043\006\003\125\004\003 +\023\034\123\105\103\117\115\040\123\115\111\115\105\040\122\123 +\101\040\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_ID UTF8 "0" +CKA_ISSUER MULTILINE_OCTAL +\060\134\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\045\060\043\006\003\125\004\003 +\023\034\123\105\103\117\115\040\123\115\111\115\105\040\122\123 +\101\040\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\011\000\335\251\333\236\176\274\324\155 +END +CKA_VALUE MULTILINE_OCTAL +\060\202\005\236\060\202\003\206\240\003\002\001\002\002\011\000 +\335\251\333\236\176\274\324\155\060\015\006\011\052\206\110\206 +\367\015\001\001\014\005\000\060\134\061\013\060\011\006\003\125 +\004\006\023\002\112\120\061\046\060\044\006\003\125\004\012\023 +\035\123\105\103\117\115\040\124\162\165\163\164\040\123\171\163 +\164\145\155\163\040\103\157\056\054\040\114\164\144\056\061\045 +\060\043\006\003\125\004\003\023\034\123\105\103\117\115\040\123 +\115\111\115\105\040\122\123\101\040\122\157\157\164\040\103\101 +\040\062\060\062\064\060\036\027\015\062\064\060\061\063\061\060 +\066\062\063\060\066\132\027\015\064\071\060\061\061\064\060\066 +\062\063\060\066\132\060\134\061\013\060\011\006\003\125\004\006 +\023\002\112\120\061\046\060\044\006\003\125\004\012\023\035\123 +\105\103\117\115\040\124\162\165\163\164\040\123\171\163\164\145 +\155\163\040\103\157\056\054\040\114\164\144\056\061\045\060\043 +\006\003\125\004\003\023\034\123\105\103\117\115\040\123\115\111 +\115\105\040\122\123\101\040\122\157\157\164\040\103\101\040\062 +\060\062\064\060\202\002\042\060\015\006\011\052\206\110\206\367 +\015\001\001\001\005\000\003\202\002\017\000\060\202\002\012\002 +\202\002\001\000\301\312\336\306\344\326\154\327\326\170\031\114 +\105\145\246\144\314\126\243\202\175\214\212\325\211\332\015\345 +\357\140\077\143\221\372\360\007\033\147\266\077\301\300\356\202 +\031\226\121\311\053\102\156\376\123\107\212\242\346\206\027\325 +\312\247\266\077\111\010\320\310\375\175\052\072\056\305\013\152 +\226\204\104\114\213\335\010\124\033\263\002\247\103\024\104\135 +\157\062\074\061\202\121\304\342\301\206\375\334\170\172\270\345 +\070\160\233\324\116\037\024\262\057\352\013\032\166\144\050\211 +\047\253\162\171\310\341\135\017\274\026\255\126\312\243\232\040 +\122\120\252\037\062\270\124\133\171\350\374\165\070\240\300\357 +\106\362\313\007\203\121\057\271\172\105\273\221\345\367\034\074 +\302\315\174\307\005\150\324\322\210\306\310\226\231\055\014\005 +\163\314\060\007\220\166\341\003\060\025\165\001\136\320\163\150 +\172\251\020\213\322\106\353\176\057\112\126\145\003\050\213\117 +\031\360\211\175\131\371\370\034\155\177\331\341\331\231\212\304 +\104\226\274\043\044\103\311\161\373\115\151\232\101\045\250\362 +\142\264\235\356\274\020\063\070\067\277\043\013\164\314\276\063 +\060\207\054\032\263\054\200\277\236\264\104\375\316\351\040\130 +\233\031\204\347\150\270\161\177\131\242\322\012\017\252\007\310 +\143\026\334\300\363\014\216\223\321\124\355\201\006\056\115\203 +\015\104\254\115\061\105\356\165\273\114\106\056\255\245\305\037 +\211\242\044\142\223\333\206\072\262\164\242\330\072\103\354\146 +\344\024\221\030\274\014\063\017\214\107\225\011\006\371\320\376 +\227\156\066\062\013\342\140\361\306\163\135\040\367\206\251\033 +\150\361\165\045\131\242\253\276\147\060\247\262\263\303\156\370 +\242\110\163\207\161\216\015\312\134\043\265\221\165\353\257\013 +\263\114\170\360\227\164\351\124\056\336\100\227\252\246\343\173 +\130\366\244\355\016\120\240\371\176\240\056\064\014\127\001\370 +\376\302\370\301\267\254\301\343\363\254\151\305\144\126\262\255 +\320\100\271\333\233\165\046\061\050\367\152\160\123\270\165\022 +\275\025\142\160\036\337\020\107\242\165\147\140\325\137\162\154 +\201\315\344\111\275\156\365\020\013\301\301\135\220\317\323\023 +\155\342\312\103\002\003\001\000\001\243\143\060\141\060\035\006 +\003\125\035\016\004\026\004\024\173\341\234\250\066\034\107\244 +\004\373\001\202\157\272\161\101\065\034\060\260\060\037\006\003 +\125\035\043\004\030\060\026\200\024\173\341\234\250\066\034\107 +\244\004\373\001\202\157\272\161\101\065\034\060\260\060\016\006 +\003\125\035\017\001\001\377\004\004\003\002\001\006\060\017\006 +\003\125\035\023\001\001\377\004\005\060\003\001\001\377\060\015 +\006\011\052\206\110\206\367\015\001\001\014\005\000\003\202\002 +\001\000\264\266\325\373\106\055\356\273\274\174\332\064\357\344 +\260\131\050\312\106\144\274\042\345\010\226\341\032\033\366\044 +\316\331\131\140\142\315\033\252\132\014\174\270\174\164\133\342 +\204\074\044\270\147\167\230\034\156\150\341\152\244\265\265\063 +\222\230\005\171\036\011\272\130\321\352\202\127\124\165\034\144 +\313\101\240\216\241\174\002\104\173\271\250\147\072\172\122\116 +\072\273\201\177\305\145\364\323\032\145\357\120\375\200\237\115 +\011\222\132\001\133\251\247\060\120\200\152\226\177\122\114\034 +\077\146\345\044\224\302\031\252\004\050\237\303\026\060\316\364 +\163\327\264\316\076\317\027\033\072\141\240\166\040\076\012\113 +\063\335\271\330\101\173\224\243\174\266\014\121\244\122\206\233 +\131\116\173\340\332\366\121\304\042\376\045\271\071\152\275\146 +\323\125\236\122\246\141\175\113\054\314\037\256\061\152\364\075 +\230\017\100\003\073\254\146\141\067\337\373\041\223\201\332\324 +\041\027\035\227\021\373\250\216\262\175\202\174\136\326\173\102 +\241\251\033\223\355\042\072\355\224\220\173\367\136\131\073\376 +\165\034\126\205\367\210\113\100\155\251\070\126\041\256\345\025 +\033\102\131\237\377\314\006\073\066\233\121\067\177\130\062\303 +\136\153\230\001\035\301\273\114\022\164\017\152\100\015\117\102 +\062\212\356\072\175\223\250\344\370\041\106\254\140\161\361\115 +\151\046\072\175\365\342\070\102\207\113\372\273\202\124\336\353 +\116\170\310\246\153\332\236\241\254\200\233\111\375\046\276\302 +\027\270\115\250\010\150\031\102\372\305\101\017\030\343\356\010 +\330\037\043\207\020\316\325\361\010\071\103\247\367\353\143\010 +\067\141\077\173\336\203\045\265\134\162\125\357\333\060\072\023 +\325\231\113\107\261\200\276\317\177\200\331\024\014\273\340\234 +\372\011\213\015\001\042\071\154\354\135\304\226\260\055\044\065 +\021\340\005\175\102\151\167\052\101\036\241\305\154\371\275\321 +\046\240\360\255\103\170\213\326\240\134\322\226\001\345\335\354 +\356\310\117\346\140\124\113\014\020\207\207\345\313\167\147\132 +\002\326\221\126\213\356\032\235\346\034\016\366\344\033\000\101 +\240\124\034\056\217\336\322\247\166\147\341\343\237\350\157\007 +\044\244 +END +CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE +CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE +CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE + +# Trust for "SECOM SMIME RSA Root CA 2024" +# Issuer: CN=SECOM SMIME RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Serial Number:00:dd:a9:db:9e:7e:bc:d4:6d +# Subject: CN=SECOM SMIME RSA Root CA 2024,O="SECOM Trust Systems Co., Ltd.",C=JP +# Not Valid Before: Wed Jan 31 06:23:06 2024 +# Not Valid After : Thu Jan 14 06:23:06 2049 +# Fingerprint (SHA-256): 36:29:E7:18:8E:00:A7:CB:32:32:C4:42:6B:C8:49:12:F1:21:8B:1A:9A:E6:76:C0:B0:AB:E1:DB:FE:21:82:B5 +# Fingerprint (SHA1): 90:A5:E1:BD:C5:3F:69:08:C2:E3:73:9F:E7:55:E3:7F:75:F3:84:47 +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "SECOM SMIME RSA Root CA 2024" +CKA_CERT_SHA1_HASH MULTILINE_OCTAL +\220\245\341\275\305\077\151\010\302\343\163\237\347\125\343\177 +\165\363\204\107 +END +CKA_CERT_MD5_HASH MULTILINE_OCTAL +\241\313\205\021\231\325\204\012\342\151\245\257\066\061\142\220 +END +CKA_ISSUER MULTILINE_OCTAL +\060\134\061\013\060\011\006\003\125\004\006\023\002\112\120\061 +\046\060\044\006\003\125\004\012\023\035\123\105\103\117\115\040 +\124\162\165\163\164\040\123\171\163\164\145\155\163\040\103\157 +\056\054\040\114\164\144\056\061\045\060\043\006\003\125\004\003 +\023\034\123\105\103\117\115\040\123\115\111\115\105\040\122\123 +\101\040\122\157\157\164\040\103\101\040\062\060\062\064 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\011\000\335\251\333\236\176\274\324\155 +END +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE + +# +# Certificate "Telia EC Email Root CA v3" +# +# Issuer: CN=Telia EC Email Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:e1:0c:1e:d8:0d:94:03:87:05:11:00:a6 +# Subject: CN=Telia EC Email Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 12:14:14 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): 36:82:22:8D:7D:67:8B:57:14:40:CF:1C:B3:4E:69:FB:41:35:FD:6C:2A:1B:E3:8E:14:16:3B:71:1E:02:AE:01 +# Fingerprint (SHA1): 33:EA:1C:7E:79:CF:31:66:FD:B9:FA:32:47:73:FB:B7:89:01:00:4F +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia EC Email Root CA v3" +CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 +CKA_SUBJECT MULTILINE_OCTAL +\060\114\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\042\060\040\006\003 +\125\004\003\014\031\124\145\154\151\141\040\105\103\040\105\155 +\141\151\154\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_ID UTF8 "0" +CKA_ISSUER MULTILINE_OCTAL +\060\114\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\042\060\040\006\003 +\125\004\003\014\031\124\145\154\151\141\040\105\103\040\105\155 +\141\151\154\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\341\014\036\330\015\224\003\207\005\021\000 +\246 +END +CKA_VALUE MULTILINE_OCTAL +\060\202\002\065\060\202\001\273\240\003\002\001\002\002\017\001 +\213\322\341\014\036\330\015\224\003\207\005\021\000\246\060\012 +\006\010\052\206\110\316\075\004\003\003\060\114\061\013\060\011 +\006\003\125\004\006\023\002\123\105\061\031\060\027\006\003\125 +\004\012\014\020\124\145\154\151\141\040\103\157\155\160\141\156 +\171\040\101\102\061\042\060\040\006\003\125\004\003\014\031\124 +\145\154\151\141\040\105\103\040\105\155\141\151\154\040\122\157 +\157\164\040\103\101\040\166\063\060\036\027\015\062\063\061\061 +\061\065\061\062\061\064\061\064\132\027\015\064\070\060\065\062 +\063\061\061\060\060\060\060\132\060\114\061\013\060\011\006\003 +\125\004\006\023\002\123\105\061\031\060\027\006\003\125\004\012 +\014\020\124\145\154\151\141\040\103\157\155\160\141\156\171\040 +\101\102\061\042\060\040\006\003\125\004\003\014\031\124\145\154 +\151\141\040\105\103\040\105\155\141\151\154\040\122\157\157\164 +\040\103\101\040\166\063\060\166\060\020\006\007\052\206\110\316 +\075\002\001\006\005\053\201\004\000\042\003\142\000\004\224\000 +\346\143\237\026\057\244\370\272\105\045\315\107\053\127\234\131 +\056\207\302\136\363\043\105\231\305\224\256\133\152\302\066\063 +\336\154\267\322\310\274\020\202\351\106\247\016\253\144\015\114 +\056\245\005\347\315\273\064\142\130\251\307\272\334\101\140\052 +\107\031\266\257\036\373\221\221\260\265\234\345\232\127\174\030 +\230\037\222\153\110\262\262\014\011\137\235\341\201\030\243\143 +\060\141\060\037\006\003\125\035\043\004\030\060\026\200\024\056 +\321\321\037\117\251\046\255\246\255\041\230\105\373\023\034\123 +\002\257\346\060\035\006\003\125\035\016\004\026\004\024\056\321 +\321\037\117\251\046\255\246\255\041\230\105\373\023\034\123\002 +\257\346\060\016\006\003\125\035\017\001\001\377\004\004\003\002 +\001\006\060\017\006\003\125\035\023\001\001\377\004\005\060\003 +\001\001\377\060\012\006\010\052\206\110\316\075\004\003\003\003 +\150\000\060\145\002\061\000\353\252\152\365\014\257\201\003\016 +\252\222\077\357\053\033\013\263\264\324\230\330\367\225\244\035 +\252\274\266\277\346\122\230\013\170\134\017\017\243\306\243\151 +\302\275\120\326\157\172\177\002\060\074\035\354\172\365\211\203 +\077\041\351\260\325\170\372\333\120\363\164\007\314\171\247\045 +\362\366\214\070\207\331\266\154\102\364\162\111\114\326\273\135 +\114\256\036\366\372\221\132\052\113 +END +CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE +CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE +CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE + +# Trust for "Telia EC Email Root CA v3" +# Issuer: CN=Telia EC Email Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:e1:0c:1e:d8:0d:94:03:87:05:11:00:a6 +# Subject: CN=Telia EC Email Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 12:14:14 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): 36:82:22:8D:7D:67:8B:57:14:40:CF:1C:B3:4E:69:FB:41:35:FD:6C:2A:1B:E3:8E:14:16:3B:71:1E:02:AE:01 +# Fingerprint (SHA1): 33:EA:1C:7E:79:CF:31:66:FD:B9:FA:32:47:73:FB:B7:89:01:00:4F +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia EC Email Root CA v3" +CKA_CERT_SHA1_HASH MULTILINE_OCTAL +\063\352\034\176\171\317\061\146\375\271\372\062\107\163\373\267 +\211\001\000\117 +END +CKA_CERT_MD5_HASH MULTILINE_OCTAL +\327\203\306\202\162\144\010\353\243\270\127\205\010\236\106\231 +END +CKA_ISSUER MULTILINE_OCTAL +\060\114\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\042\060\040\006\003 +\125\004\003\014\031\124\145\154\151\141\040\105\103\040\105\155 +\141\151\154\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\341\014\036\330\015\224\003\207\005\021\000 +\246 +END +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE + +# +# Certificate "Telia EC TLS Root CA v3" +# +# Issuer: CN=Telia EC TLS Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:22:54:63:4d:04:8b:6c:e5:47:1f:d2:b5 +# Subject: CN=Telia EC TLS Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 08:55:26 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): 09:8E:08:A9:1D:BB:F7:74:78:B9:6C:CE:B8:9B:14:13:A5:DA:37:B7:C8:62:60:6A:95:5D:EB:07:17:9F:43:26 +# Fingerprint (SHA1): B4:D6:07:C2:A5:95:BC:5B:F4:67:4D:C9:DC:6F:6F:0A:00:7A:A5:35 +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia EC TLS Root CA v3" +CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 +CKA_SUBJECT MULTILINE_OCTAL +\060\112\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\040\060\036\006\003 +\125\004\003\014\027\124\145\154\151\141\040\105\103\040\124\114 +\123\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_ID UTF8 "0" +CKA_ISSUER MULTILINE_OCTAL +\060\112\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\040\060\036\006\003 +\125\004\003\014\027\124\145\154\151\141\040\105\103\040\124\114 +\123\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\042\124\143\115\004\213\154\345\107\037\322 +\265 +END +CKA_VALUE MULTILINE_OCTAL +\060\202\002\062\060\202\001\267\240\003\002\001\002\002\017\001 +\213\322\042\124\143\115\004\213\154\345\107\037\322\265\060\012 +\006\010\052\206\110\316\075\004\003\003\060\112\061\013\060\011 +\006\003\125\004\006\023\002\123\105\061\031\060\027\006\003\125 +\004\012\014\020\124\145\154\151\141\040\103\157\155\160\141\156 +\171\040\101\102\061\040\060\036\006\003\125\004\003\014\027\124 +\145\154\151\141\040\105\103\040\124\114\123\040\122\157\157\164 +\040\103\101\040\166\063\060\036\027\015\062\063\061\061\061\065 +\060\070\065\065\062\066\132\027\015\064\070\060\065\062\063\061 +\061\060\060\060\060\132\060\112\061\013\060\011\006\003\125\004 +\006\023\002\123\105\061\031\060\027\006\003\125\004\012\014\020 +\124\145\154\151\141\040\103\157\155\160\141\156\171\040\101\102 +\061\040\060\036\006\003\125\004\003\014\027\124\145\154\151\141 +\040\105\103\040\124\114\123\040\122\157\157\164\040\103\101\040 +\166\063\060\166\060\020\006\007\052\206\110\316\075\002\001\006 +\005\053\201\004\000\042\003\142\000\004\301\310\226\025\103\055 +\271\205\051\112\126\322\042\270\166\232\362\117\247\246\140\347 +\222\337\122\117\301\151\326\076\151\026\106\260\044\115\263\327 +\343\113\020\174\162\075\232\224\173\105\271\055\273\171\340\203 +\245\276\004\024\227\111\347\041\264\300\247\006\145\227\217\361 +\032\126\131\225\345\306\065\214\075\207\241\067\341\005\015\300 +\151\312\102\064\302\311\053\203\151\145\243\143\060\141\060\037 +\006\003\125\035\043\004\030\060\026\200\024\324\144\350\103\210 +\072\163\057\320\032\161\202\066\013\136\205\336\307\336\103\060 +\035\006\003\125\035\016\004\026\004\024\324\144\350\103\210\072 +\163\057\320\032\161\202\066\013\136\205\336\307\336\103\060\016 +\006\003\125\035\017\001\001\377\004\004\003\002\001\006\060\017 +\006\003\125\035\023\001\001\377\004\005\060\003\001\001\377\060 +\012\006\010\052\206\110\316\075\004\003\003\003\151\000\060\146 +\002\061\000\227\001\107\122\377\326\333\047\300\065\045\206\206 +\177\366\326\267\373\073\115\255\050\267\225\045\236\217\016\215 +\043\130\105\144\010\225\056\055\125\151\135\275\060\025\003\270 +\136\070\130\002\061\000\257\052\277\076\147\144\147\370\340\153 +\345\101\363\226\000\116\156\226\235\324\273\260\065\064\134\164 +\330\024\047\022\026\151\327\044\213\345\001\054\110\265\374\005 +\021\051\136\146\167\143 +END +CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE +CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE +CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE + +# Trust for "Telia EC TLS Root CA v3" +# Issuer: CN=Telia EC TLS Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:22:54:63:4d:04:8b:6c:e5:47:1f:d2:b5 +# Subject: CN=Telia EC TLS Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 08:55:26 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): 09:8E:08:A9:1D:BB:F7:74:78:B9:6C:CE:B8:9B:14:13:A5:DA:37:B7:C8:62:60:6A:95:5D:EB:07:17:9F:43:26 +# Fingerprint (SHA1): B4:D6:07:C2:A5:95:BC:5B:F4:67:4D:C9:DC:6F:6F:0A:00:7A:A5:35 +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia EC TLS Root CA v3" +CKA_CERT_SHA1_HASH MULTILINE_OCTAL +\264\326\007\302\245\225\274\133\364\147\115\311\334\157\157\012 +\000\172\245\065 +END +CKA_CERT_MD5_HASH MULTILINE_OCTAL +\266\372\152\134\102\373\305\147\162\300\340\057\162\373\132\104 +END +CKA_ISSUER MULTILINE_OCTAL +\060\112\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\040\060\036\006\003 +\125\004\003\014\027\124\145\154\151\141\040\105\103\040\124\114 +\123\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\042\124\143\115\004\213\154\345\107\037\322 +\265 +END +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE + +# +# Certificate "Telia RSA Email Root CA v3" +# +# Issuer: CN=Telia RSA Email Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:ce:f4:c1:15:78:29:62:4d:79:b2:75:5b +# Subject: CN=Telia RSA Email Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 11:55:02 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): 5B:0C:50:2A:7D:96:3B:A5:52:17:39:6F:DA:9B:3D:C7:81:71:00:0A:EE:FF:42:CE:CC:3A:20:A7:93:81:63:E8 +# Fingerprint (SHA1): AA:6C:3C:AF:F0:96:C6:4D:C3:27:84:BE:9D:8A:3E:3A:7B:B4:4E:C2 +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia RSA Email Root CA v3" +CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 +CKA_SUBJECT MULTILINE_OCTAL +\060\115\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\043\060\041\006\003 +\125\004\003\014\032\124\145\154\151\141\040\122\123\101\040\105 +\155\141\151\154\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_ID UTF8 "0" +CKA_ISSUER MULTILINE_OCTAL +\060\115\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\043\060\041\006\003 +\125\004\003\014\032\124\145\154\151\141\040\122\123\101\040\105 +\155\141\151\154\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\316\364\301\025\170\051\142\115\171\262\165 +\133 +END +CKA_VALUE MULTILINE_OCTAL +\060\202\005\206\060\202\003\156\240\003\002\001\002\002\017\001 +\213\322\316\364\301\025\170\051\142\115\171\262\165\133\060\015 +\006\011\052\206\110\206\367\015\001\001\014\005\000\060\115\061 +\013\060\011\006\003\125\004\006\023\002\123\105\061\031\060\027 +\006\003\125\004\012\014\020\124\145\154\151\141\040\103\157\155 +\160\141\156\171\040\101\102\061\043\060\041\006\003\125\004\003 +\014\032\124\145\154\151\141\040\122\123\101\040\105\155\141\151 +\154\040\122\157\157\164\040\103\101\040\166\063\060\036\027\015 +\062\063\061\061\061\065\061\061\065\065\060\062\132\027\015\064 +\070\060\065\062\063\061\061\060\060\060\060\132\060\115\061\013 +\060\011\006\003\125\004\006\023\002\123\105\061\031\060\027\006 +\003\125\004\012\014\020\124\145\154\151\141\040\103\157\155\160 +\141\156\171\040\101\102\061\043\060\041\006\003\125\004\003\014 +\032\124\145\154\151\141\040\122\123\101\040\105\155\141\151\154 +\040\122\157\157\164\040\103\101\040\166\063\060\202\002\042\060 +\015\006\011\052\206\110\206\367\015\001\001\001\005\000\003\202 +\002\017\000\060\202\002\012\002\202\002\001\000\271\111\073\057 +\133\122\030\311\317\144\254\250\333\366\172\236\076\255\327\201 +\243\354\272\352\201\056\365\275\257\225\321\113\136\210\356\224 +\142\012\313\206\047\011\250\047\321\303\062\243\012\352\356\027 +\145\014\074\023\026\372\337\004\323\153\317\140\212\066\133\367 +\047\226\061\334\333\367\307\156\025\021\272\143\051\273\320\211 +\160\154\343\110\242\064\231\310\372\112\222\217\260\176\222\056 +\354\246\164\371\321\052\234\163\302\162\053\124\217\011\170\173 +\357\046\014\076\362\174\072\021\135\041\007\325\317\276\136\043 +\210\240\053\005\302\214\277\051\277\130\105\074\361\152\310\253 +\364\376\071\376\262\156\025\132\017\247\113\076\151\304\273\073 +\321\222\240\330\137\050\201\302\275\112\226\245\241\106\161\370 +\015\261\021\143\152\246\001\137\305\163\172\330\111\112\056\301 +\064\276\077\145\336\302\152\306\217\040\330\276\046\002\272\307 +\162\042\026\230\231\227\346\144\155\111\070\220\312\324\161\006 +\264\204\042\264\236\063\064\126\312\035\166\251\232\110\335\314 +\365\217\056\211\111\306\172\006\003\217\257\216\354\200\162\025 +\361\331\011\200\130\122\251\302\034\255\076\137\067\061\146\227 +\020\241\330\163\264\335\056\302\063\245\176\247\130\233\201\021 +\153\210\325\374\113\265\055\272\176\374\121\255\347\076\115\256 +\363\316\121\236\345\123\213\257\036\250\102\344\145\271\362\346 +\052\103\347\117\074\365\333\321\334\273\240\337\027\330\341\276 +\122\131\147\076\041\024\072\203\131\176\157\203\331\225\153\061 +\171\143\216\311\135\324\064\215\370\344\332\056\256\331\010\353 +\333\263\034\351\335\225\030\256\142\233\065\200\105\357\322\224 +\240\326\013\340\242\311\040\100\063\265\113\173\230\066\144\064 +\227\324\213\003\267\172\212\233\147\052\225\223\143\263\362\362 +\037\024\054\021\250\321\146\014\332\106\346\014\336\125\272\107 +\043\306\352\017\262\103\135\216\376\017\127\324\257\347\312\070 +\313\326\333\231\273\112\130\266\150\241\324\212\045\162\252\233 +\015\100\072\246\241\243\036\267\132\055\241\347\240\071\217\306 +\104\324\240\166\137\210\074\377\345\045\120\214\356\074\176\022 +\066\075\262\136\045\107\066\151\231\024\041\137\002\003\001\000 +\001\243\143\060\141\060\037\006\003\125\035\043\004\030\060\026 +\200\024\207\271\006\077\106\305\051\024\315\024\136\305\236\043 +\220\266\044\256\146\231\060\035\006\003\125\035\016\004\026\004 +\024\207\271\006\077\106\305\051\024\315\024\136\305\236\043\220 +\266\044\256\146\231\060\016\006\003\125\035\017\001\001\377\004 +\004\003\002\001\006\060\017\006\003\125\035\023\001\001\377\004 +\005\060\003\001\001\377\060\015\006\011\052\206\110\206\367\015 +\001\001\014\005\000\003\202\002\001\000\215\123\131\331\377\073 +\062\217\327\061\076\355\166\235\015\206\215\342\060\120\160\270 +\235\331\237\022\071\313\235\257\265\256\303\207\341\153\304\355 +\203\020\274\105\172\266\231\360\264\171\174\155\065\243\223\220 +\054\060\206\261\377\205\327\214\145\127\130\222\007\110\354\107 +\230\267\347\166\306\167\140\377\107\354\167\100\255\034\055\352 +\337\122\314\222\176\245\333\053\107\365\033\247\107\100\135\161 +\163\110\304\323\375\247\260\043\100\261\043\273\353\332\201\100 +\305\062\007\331\051\311\023\006\266\030\226\127\131\213\140\001 +\257\357\014\230\112\113\246\026\242\241\043\103\254\125\151\114 +\061\137\373\141\274\053\263\305\002\061\265\124\077\165\031\253 +\135\074\076\144\305\343\353\360\177\262\212\272\057\004\062\073 +\362\003\336\052\273\302\011\342\243\045\363\115\056\206\170\126 +\330\074\107\055\144\255\372\001\171\260\330\210\116\353\262\301 +\132\345\113\272\066\304\031\102\353\233\056\014\015\244\370\273 +\332\044\036\000\234\356\112\043\321\241\264\242\317\135\047\252 +\102\123\203\042\204\024\227\050\134\227\171\246\140\274\207\066 +\231\370\303\027\150\342\272\145\363\016\312\066\327\323\044\074 +\301\011\211\356\373\023\271\103\250\051\024\305\273\101\107\264 +\366\112\275\333\107\042\317\367\243\332\060\126\306\175\230\145 +\045\151\120\140\152\365\372\265\277\214\170\033\167\255\271\135 +\316\327\227\151\156\356\011\346\337\226\167\273\006\223\252\123 +\241\234\331\053\273\336\073\231\042\004\251\274\024\366\075\003 +\242\363\065\224\074\116\037\142\276\360\262\347\130\271\323\027 +\051\305\253\016\325\113\145\106\031\133\044\072\142\111\044\005 +\212\300\170\367\024\224\321\257\013\037\067\154\064\005\316\147 +\361\166\300\367\254\144\110\326\127\232\050\363\027\117\232\367 +\003\264\040\021\142\237\377\032\252\151\063\141\050\176\075\366 +\304\150\027\314\063\071\303\316\122\341\366\262\273\125\134\146 +\155\237\341\074\256\246\016\025\115\106\361\326\004\015\205\226 +\263\137\050\024\117\316\326\233\341\372\015\163\154\157\247\375 +\271\351\052\244\267\100\114\160\016\070\117\033\254\307\165\050 +\170\251\167\066\232\150\164\240\327\267 +END +CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE +CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE +CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE + +# Trust for "Telia RSA Email Root CA v3" +# Issuer: CN=Telia RSA Email Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:ce:f4:c1:15:78:29:62:4d:79:b2:75:5b +# Subject: CN=Telia RSA Email Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 11:55:02 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): 5B:0C:50:2A:7D:96:3B:A5:52:17:39:6F:DA:9B:3D:C7:81:71:00:0A:EE:FF:42:CE:CC:3A:20:A7:93:81:63:E8 +# Fingerprint (SHA1): AA:6C:3C:AF:F0:96:C6:4D:C3:27:84:BE:9D:8A:3E:3A:7B:B4:4E:C2 +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia RSA Email Root CA v3" +CKA_CERT_SHA1_HASH MULTILINE_OCTAL +\252\154\074\257\360\226\306\115\303\047\204\276\235\212\076\072 +\173\264\116\302 +END +CKA_CERT_MD5_HASH MULTILINE_OCTAL +\173\170\307\204\252\212\256\263\377\237\272\146\072\007\164\361 +END +CKA_ISSUER MULTILINE_OCTAL +\060\115\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\043\060\041\006\003 +\125\004\003\014\032\124\145\154\151\141\040\122\123\101\040\105 +\155\141\151\154\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\316\364\301\025\170\051\142\115\171\262\165 +\133 +END +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE + +# +# Certificate "Telia RSA TLS Root CA v3" +# +# Issuer: CN=Telia RSA TLS Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:50:ab:42:55:2c:47:5a:bd:a1:dc:1a:c5 +# Subject: CN=Telia RSA TLS Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 09:47:42 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): D1:3D:B1:29:4C:45:EB:C6:FC:86:C6:BB:F6:9F:A2:9B:DF:E6:92:DF:F7:C7:13:C2:43:C7:A9:56:C6:A2:28:4C +# Fingerprint (SHA1): B5:2E:88:4E:40:C1:11:FB:50:C7:E2:4F:AC:18:2B:BD:68:15:D2:34 +CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia RSA TLS Root CA v3" +CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 +CKA_SUBJECT MULTILINE_OCTAL +\060\113\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\041\060\037\006\003 +\125\004\003\014\030\124\145\154\151\141\040\122\123\101\040\124 +\114\123\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_ID UTF8 "0" +CKA_ISSUER MULTILINE_OCTAL +\060\113\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\041\060\037\006\003 +\125\004\003\014\030\124\145\154\151\141\040\122\123\101\040\124 +\114\123\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\120\253\102\125\054\107\132\275\241\334\032 +\305 +END +CKA_VALUE MULTILINE_OCTAL +\060\202\005\202\060\202\003\152\240\003\002\001\002\002\017\001 +\213\322\120\253\102\125\054\107\132\275\241\334\032\305\060\015 +\006\011\052\206\110\206\367\015\001\001\014\005\000\060\113\061 +\013\060\011\006\003\125\004\006\023\002\123\105\061\031\060\027 +\006\003\125\004\012\014\020\124\145\154\151\141\040\103\157\155 +\160\141\156\171\040\101\102\061\041\060\037\006\003\125\004\003 +\014\030\124\145\154\151\141\040\122\123\101\040\124\114\123\040 +\122\157\157\164\040\103\101\040\166\063\060\036\027\015\062\063 +\061\061\061\065\060\071\064\067\064\062\132\027\015\064\070\060 +\065\062\063\061\061\060\060\060\060\132\060\113\061\013\060\011 +\006\003\125\004\006\023\002\123\105\061\031\060\027\006\003\125 +\004\012\014\020\124\145\154\151\141\040\103\157\155\160\141\156 +\171\040\101\102\061\041\060\037\006\003\125\004\003\014\030\124 +\145\154\151\141\040\122\123\101\040\124\114\123\040\122\157\157 +\164\040\103\101\040\166\063\060\202\002\042\060\015\006\011\052 +\206\110\206\367\015\001\001\001\005\000\003\202\002\017\000\060 +\202\002\012\002\202\002\001\000\261\137\075\050\155\175\204\047 +\370\113\121\157\223\300\367\117\040\304\106\031\234\277\037\005 +\354\251\233\341\140\023\307\170\243\173\130\235\334\241\361\104 +\115\023\052\147\015\011\260\020\347\266\357\034\121\030\153\210 +\121\332\135\264\126\065\132\164\114\152\071\157\266\225\337\175 +\061\261\042\073\350\321\124\354\376\005\274\113\304\231\346\033 +\000\252\043\340\137\271\070\343\125\027\203\306\314\143\102\370 +\340\006\300\245\150\357\354\233\230\273\322\171\131\161\141\221 +\264\211\057\130\062\267\064\331\023\345\033\160\135\317\316\360 +\324\305\126\226\324\251\076\234\106\062\043\336\211\100\156\124 +\176\225\027\370\025\374\271\243\344\075\175\246\343\142\177\131 +\365\056\042\266\273\204\225\301\005\177\322\343\223\267\360\165 +\074\234\117\372\357\045\157\160\374\035\306\331\255\353\321\377 +\363\134\323\225\256\342\001\162\241\072\246\104\250\262\220\002 +\123\146\306\343\147\330\052\266\314\374\145\247\247\273\276\247 +\321\153\317\210\332\067\012\133\341\201\356\021\344\274\006\266 +\255\065\303\374\137\255\243\134\213\357\050\174\145\260\300\311 +\012\166\370\123\302\163\024\342\354\123\251\250\205\126\071\360 +\027\131\256\370\264\032\117\124\072\352\246\202\201\272\166\311 +\007\242\113\043\145\071\112\353\120\377\042\174\346\022\200\065 +\310\006\011\024\065\162\262\064\161\015\103\256\365\300\060\000 +\326\352\232\256\373\130\201\113\350\032\147\146\005\152\076\323 +\037\033\254\015\360\032\323\051\326\251\276\051\125\261\204\171 +\364\021\140\042\300\025\004\314\153\032\037\226\371\007\057\230 +\226\237\122\220\022\276\120\053\052\365\106\330\122\070\213\243 +\342\056\064\201\117\117\272\027\241\020\056\266\052\171\363\220 +\027\367\301\064\105\333\372\170\324\103\104\224\126\260\052\264 +\176\030\370\350\302\043\366\270\374\222\120\246\234\136\172\146 +\134\103\104\224\112\030\042\043\220\014\347\021\316\346\054\232 +\122\264\343\140\176\063\305\114\375\217\355\105\021\260\307\377 +\032\154\266\275\140\134\034\244\262\233\251\372\024\123\150\227 +\033\003\345\033\244\232\255\131\231\335\000\367\135\046\153\172 +\140\224\076\165\115\351\016\357\002\003\001\000\001\243\143\060 +\141\060\037\006\003\125\035\043\004\030\060\026\200\024\260\307 +\251\322\335\262\050\126\163\004\224\214\024\134\110\157\067\122 +\222\250\060\035\006\003\125\035\016\004\026\004\024\260\307\251 +\322\335\262\050\126\163\004\224\214\024\134\110\157\067\122\222 +\250\060\016\006\003\125\035\017\001\001\377\004\004\003\002\001 +\006\060\017\006\003\125\035\023\001\001\377\004\005\060\003\001 +\001\377\060\015\006\011\052\206\110\206\367\015\001\001\014\005 +\000\003\202\002\001\000\135\143\061\154\064\140\321\223\266\321 +\374\010\021\052\257\271\353\176\330\270\345\276\303\253\241\246 +\204\264\126\162\214\122\234\207\013\011\056\363\250\104\276\250 +\257\152\103\355\176\151\146\341\261\160\267\357\254\157\377\016 +\136\305\201\252\327\333\134\306\201\064\064\331\227\305\321\060 +\233\213\130\345\266\046\266\312\221\034\340\033\107\201\121\313 +\354\151\321\265\256\270\133\231\232\230\274\125\332\001\223\273 +\243\204\335\071\234\361\204\020\171\030\340\026\131\160\167\041 +\352\332\064\376\011\353\174\362\243\331\374\032\254\067\260\220 +\106\343\207\246\346\032\030\214\027\337\077\351\351\211\274\254 +\226\173\045\106\056\013\353\021\354\011\210\377\075\246\376\072 +\132\233\060\040\172\277\353\023\010\267\204\042\351\021\127\072 +\021\365\244\070\346\221\012\355\235\120\006\164\254\154\002\220 +\266\313\064\044\240\375\167\371\227\327\215\307\327\027\217\214 +\370\123\136\133\371\044\051\041\255\310\376\111\050\163\320\377 +\176\020\172\026\173\251\275\227\045\004\070\205\147\322\272\176 +\311\122\126\065\271\262\016\375\325\223\001\161\034\055\100\245 +\044\054\201\050\246\214\144\070\302\332\372\161\051\250\255\326 +\061\221\137\221\243\311\116\040\210\121\222\305\317\310\071\066 +\356\005\272\042\065\027\012\106\112\247\021\143\220\275\343\210 +\024\234\361\051\061\237\000\226\265\170\074\307\003\160\164\125 +\115\004\142\302\012\342\147\262\167\230\136\062\115\253\064\152 +\121\312\006\303\073\057\027\164\042\375\231\307\120\333\310\111 +\327\257\226\002\000\134\276\026\276\274\132\252\372\032\323\134 +\001\370\140\237\263\236\321\114\141\070\116\360\006\204\243\112 +\272\317\012\336\024\123\324\024\251\212\014\314\041\034\322\306 +\320\016\256\243\315\352\077\377\101\051\226\365\377\011\162\167 +\053\213\210\366\212\024\251\126\261\124\320\327\115\220\310\131 +\170\002\242\165\061\117\263\020\225\026\345\270\002\170\263\356 +\072\242\303\233\026\266\066\320\256\125\264\337\230\263\040\105 +\070\261\214\020\240\055\115\307\104\257\313\351\214\115\026\242 +\347\102\352\025\017\200\327\361\352\353\021\307\014\334\173\010 +\217\067\167\155\304\257 +END +CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE +CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE +CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE + +# Trust for "Telia RSA TLS Root CA v3" +# Issuer: CN=Telia RSA TLS Root CA v3,O=Telia Company AB,C=SE +# Serial Number:01:8b:d2:50:ab:42:55:2c:47:5a:bd:a1:dc:1a:c5 +# Subject: CN=Telia RSA TLS Root CA v3,O=Telia Company AB,C=SE +# Not Valid Before: Wed Nov 15 09:47:42 2023 +# Not Valid After : Sat May 23 11:00:00 2048 +# Fingerprint (SHA-256): D1:3D:B1:29:4C:45:EB:C6:FC:86:C6:BB:F6:9F:A2:9B:DF:E6:92:DF:F7:C7:13:C2:43:C7:A9:56:C6:A2:28:4C +# Fingerprint (SHA1): B5:2E:88:4E:40:C1:11:FB:50:C7:E2:4F:AC:18:2B:BD:68:15:D2:34 +CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST +CKA_TOKEN CK_BBOOL CK_TRUE +CKA_PRIVATE CK_BBOOL CK_FALSE +CKA_MODIFIABLE CK_BBOOL CK_FALSE +CKA_LABEL UTF8 "Telia RSA TLS Root CA v3" +CKA_CERT_SHA1_HASH MULTILINE_OCTAL +\265\056\210\116\100\301\021\373\120\307\342\117\254\030\053\275 +\150\025\322\064 +END +CKA_CERT_MD5_HASH MULTILINE_OCTAL +\364\213\234\363\370\143\317\334\045\217\264\273\242\351\235\342 +END +CKA_ISSUER MULTILINE_OCTAL +\060\113\061\013\060\011\006\003\125\004\006\023\002\123\105\061 +\031\060\027\006\003\125\004\012\014\020\124\145\154\151\141\040 +\103\157\155\160\141\156\171\040\101\102\061\041\060\037\006\003 +\125\004\003\014\030\124\145\154\151\141\040\122\123\101\040\124 +\114\123\040\122\157\157\164\040\103\101\040\166\063 +END +CKA_SERIAL_NUMBER MULTILINE_OCTAL +\002\017\001\213\322\120\253\102\125\054\107\132\275\241\334\032 +\305 +END +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST +CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE From 463bfaed0d97ba1f1922e9938f9677ca661da512 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:52:18 -0700 Subject: [PATCH 94/97] stream: normalize fused stateless transform results Normalize each stateless transform result before passing it to the next transform in a fused run. This ensures that subsequent transforms always receive Uint8Array[] batches in both synchronous and asynchronous pipelines. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/65367 Fixes: https://github.com/nodejs/node/issues/65366 Reviewed-By: James M Snell --- lib/internal/streams/iter/pull.js | 43 ++++++++++++++----- .../test-stream-iter-transform-output.js | 32 ++++++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/lib/internal/streams/iter/pull.js b/lib/internal/streams/iter/pull.js index d75a175443d3..b6c2d9849c42 100644 --- a/lib/internal/streams/iter/pull.js +++ b/lib/internal/streams/iter/pull.js @@ -434,6 +434,16 @@ async function appendTransformResultAsyncSlow(target, result) { } } +function normalizeTransformResultFast(result) { + if (isUint8ArrayBatch(result)) { + return result.length === 0 ? null : result; + } + if (isUint8Array(result)) return [result]; + if (typeof result === 'string') return [toUint8Array(result)]; + if (isAnyArrayBuffer(result)) return [new Uint8Array(result)]; + if (ArrayBufferIsView(result)) return [arrayBufferViewToUint8Array(result)]; +} + // ============================================================================= // Sync Pipeline Implementation // ============================================================================= @@ -457,7 +467,17 @@ function* applyFusedStatelessSyncTransforms(source, run) { current = null; break; } - current = result; + if (i === run.length - 1) { + current = result; + continue; + } + current = normalizeTransformResultFast(result); + if (current === undefined) { + const normalized = []; + appendTransformResultSync(normalized, result); + current = normalized.length === 0 ? null : normalized[0]; + } + if (current === null) break; } if (current === null) continue; // Inline normalization with Uint8Array[] batch as the fast path, @@ -570,21 +590,24 @@ async function* applyFusedStatelessAsyncTransforms(source, run, signal) { for await (const chunks of source) { let current = chunks; for (let i = 0; i < run.length; i++) { - const result = run[i](current, { __proto__: null, signal }); + let result = run[i](current, { __proto__: null, signal }); + if (isPromise(result)) result = await result; if (result === null) { current = null; break; } - if (isPromise(result)) { - const resolved = await result; - if (resolved === null) { - current = null; - break; - } - current = resolved; - } else { + if (i === run.length - 1) { current = result; + continue; + } + current = normalizeTransformResultFast(result); + if (current === undefined) { + const normalized = []; + const pendingResult = appendTransformResultAsync(normalized, result); + if (pendingResult !== undefined) await pendingResult; + current = normalized.length === 0 ? null : normalized[0]; } + if (current === null) break; } if (current === null) continue; // Normalize the final output diff --git a/test/parallel/test-stream-iter-transform-output.js b/test/parallel/test-stream-iter-transform-output.js index d66a20f6e164..90261a33785a 100644 --- a/test/parallel/test-stream-iter-transform-output.js +++ b/test/parallel/test-stream-iter-transform-output.js @@ -59,6 +59,36 @@ async function testSyncTransformReturnsFloat32Array() { assert.strictEqual(data.byteLength, 4); } +// Consecutive stateless transforms normalize intermediate output (async) +async function testConsecutiveTransformsNormalizeIntermediateOutput() { + const first = (chunks) => { + return chunks === null ? null : new Uint8Array([65]); + }; + let receivedBatch = false; + const second = (chunks) => { + if (chunks !== null) receivedBatch = Array.isArray(chunks); + return chunks; + }; + const data = await bytes(pull(from('x'), first, second)); + assert.ok(receivedBatch); + assert.deepStrictEqual(data, new Uint8Array([65])); +} + +// Consecutive stateless transforms normalize intermediate output (sync) +async function testConsecutiveSyncTransformsNormalizeIntermediateOutput() { + const first = (chunks) => { + return chunks === null ? null : new Uint8Array([65]); + }; + let receivedBatch = false; + const second = (chunks) => { + if (chunks !== null) receivedBatch = Array.isArray(chunks); + return chunks; + }; + const data = bytesSync(pullSync(fromSync('x'), first, second)); + assert.ok(receivedBatch); + assert.deepStrictEqual(data, new Uint8Array([65])); +} + // Stateless transform returns a sync generator (iterable) async function testTransformReturnsGenerator() { const tx = (chunks) => { @@ -233,6 +263,8 @@ Promise.all([ testSyncTransformReturnsArrayBuffer(), testTransformReturnsFloat32Array(), testSyncTransformReturnsFloat32Array(), + testConsecutiveTransformsNormalizeIntermediateOutput(), + testConsecutiveSyncTransformsNormalizeIntermediateOutput(), testTransformReturnsGenerator(), testSyncTransformReturnsGenerator(), testTransformReturnsAsyncGenerator(), From 00b338a3f5a7d24aaa6a9377e2e012d2a350298f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=81=C4=85giewka?= Date: Mon, 13 Jul 2026 13:34:40 +0200 Subject: [PATCH 95/97] events: inline createEvent hybrid dispatch closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Originally added in 16b11cd2adaa5f60382a7f205f893f38a5061fff, it first had three callers. Now there's only one branch requireing it. Signed-off-by: Szymon Łągiewka PR-URL: https://github.com/nodejs/node/pull/64473 Reviewed-By: Chemi Atlow --- lib/internal/event_target.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js index 5a4f7c029094..3280bfbc0d2f 100644 --- a/lib/internal/event_target.js +++ b/lib/internal/event_target.js @@ -791,14 +791,6 @@ class EventTarget { } [kHybridDispatch](nodeValue, type, event) { - const createEvent = () => { - if (event === undefined) { - event = this[kCreateEvent](nodeValue, type); - event[kTarget] = this; - event[kIsBeingDispatched] = true; - } - return event; - }; if (event !== undefined) { event[kTarget] = this; event[kIsBeingDispatched] = true; @@ -844,7 +836,12 @@ class EventTarget { if (handler.isNodeStyleListener) { arg = nodeValue; } else { - arg = createEvent(); + if (event === undefined) { + event = this[kCreateEvent](nodeValue, type); + event[kTarget] = this; + event[kIsBeingDispatched] = true; + } + arg = event; } const callback = handler.weak ? handler.callback.deref() : handler.callback; From 067d305cca05b1d4bc9024dd004b3038d9ab2c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=81=C4=85giewka?= Date: Mon, 13 Jul 2026 13:37:33 +0200 Subject: [PATCH 96/97] events: inline iterationCondition hybrid dispatch closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While more readable, the removed closure overhead unlocks 10-20% in eventtarget.js benchmark. Signed-off-by: Szymon Łągiewka PR-URL: https://github.com/nodejs/node/pull/64473 Reviewed-By: Chemi Atlow --- lib/internal/event_target.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js index 3280bfbc0d2f..bfb948bb1ced 100644 --- a/lib/internal/event_target.js +++ b/lib/internal/event_target.js @@ -806,13 +806,8 @@ class EventTarget { let handler = root.next; let next; - const iterationCondition = () => { - if (handler === undefined) { - return false; - } - return root.resistStopPropagation || handler.passive || event?.[kStop] !== true; - }; - while (iterationCondition()) { + while (handler !== undefined && + (root.resistStopPropagation || handler.passive || event?.[kStop] !== true)) { // Cache the next item in case this iteration removes the current one next = handler.next; From a315622e5b2933b64455e4b0de7d80e431e86a8e Mon Sep 17 00:00:00 2001 From: Ruan Gustavo Date: Wed, 2 Sep 2026 22:00:20 -0300 Subject: [PATCH 97/97] deps: V8: backport 0b94a9fd23ba MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original commit message: [leaptiering] Fix BaselineOutOfLinePrologue builtin ... which tried to preserve kJavaScriptCallDispatchHandleRegister even on configurations where it's not used which resulted in a random value on the stack discoverable by GC. This issue triggered only on non-sandbox configuration with enabled leaptiering. Drive-by: fix MacroAssembler::GenerateTailCallToReturnedCode() on riscv port which wasn't preserving dispatch handle as all the other ports do. Bug: 42204201 Fixed: 413769394 Change-Id: If146b0b7a6cf972ed5a881142f40980774f19cba Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6587010 Commit-Queue: Igor Sheludko Reviewed-by: Olivier Flückiger Cr-Commit-Position: refs/heads/main@{#100512} Node.js 24 builds V8 with leaptiering enabled and the sandbox disabled, so V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE is not defined and the JS calling convention does not carry the dispatch handle register (x4 on arm64). BaselineOutOfLinePrologue and GenerateTailCallToReturnedCode still pushed that register as a tagged slot of an INTERNAL frame, so whatever value the caller left there is dereferenced by ClearStaleLeftTrimmedPointerVisitor during mark-compact root scanning and crashes the process with SIGSEGV (seen as jest workers dying). Refs: https://github.com/v8/v8/commit/0b94a9fd23ba5f59b3f675fb04d60a9b0102e27d Fixes: https://github.com/nodejs/node/issues/62393 --- common.gypi | 2 +- deps/v8/src/builtins/arm64/builtins-arm64.cc | 3 ++- .../src/builtins/loong64/builtins-loong64.cc | 5 +++-- deps/v8/src/builtins/mips64/builtins-mips64.cc | 5 +++-- deps/v8/src/builtins/riscv/builtins-riscv.cc | 7 +++++-- deps/v8/src/builtins/x64/builtins-x64.cc | 5 +++-- .../src/codegen/arm64/macro-assembler-arm64.cc | 11 ++++++----- .../codegen/loong64/macro-assembler-loong64.cc | 5 +++-- .../codegen/mips64/macro-assembler-mips64.cc | 5 +++-- .../src/codegen/riscv/macro-assembler-riscv.cc | 18 +++++++++++++++--- deps/v8/src/codegen/x64/macro-assembler-x64.cc | 5 +++-- 11 files changed, 47 insertions(+), 24 deletions(-) diff --git a/common.gypi b/common.gypi index 83e0691d4a2d..cba4b08ec7ba 100644 --- a/common.gypi +++ b/common.gypi @@ -42,7 +42,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.53', + 'v8_embedder_string': '-node.54', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/src/builtins/arm64/builtins-arm64.cc b/deps/v8/src/builtins/arm64/builtins-arm64.cc index 1a2d310bfeed..e99d10aacf9f 100644 --- a/deps/v8/src/builtins/arm64/builtins-arm64.cc +++ b/deps/v8/src/builtins/arm64/builtins-arm64.cc @@ -1293,11 +1293,12 @@ void Builtins::Generate_BaselineOutOfLinePrologue(MacroAssembler* masm) { FrameScope frame_scope(masm, StackFrame::INTERNAL); // Save incoming new target or generator - Register maybe_dispatch_handle = V8_ENABLE_LEAPTIERING_BOOL + Register maybe_dispatch_handle = V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE_BOOL ? kJavaScriptCallDispatchHandleRegister : padreg; // No need to SmiTag as dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + __ AssertSmi(maybe_dispatch_handle); __ Push(maybe_dispatch_handle, new_target); __ SmiTag(frame_size); __ PushArgument(frame_size); diff --git a/deps/v8/src/builtins/loong64/builtins-loong64.cc b/deps/v8/src/builtins/loong64/builtins-loong64.cc index 42f6c346733d..2206ec09182c 100644 --- a/deps/v8/src/builtins/loong64/builtins-loong64.cc +++ b/deps/v8/src/builtins/loong64/builtins-loong64.cc @@ -1103,15 +1103,16 @@ void Builtins::Generate_BaselineOutOfLinePrologue(MacroAssembler* masm) { FrameScope frame_scope(masm, StackFrame::INTERNAL); // Save incoming new target or generator __ Push(kJavaScriptCallNewTargetRegister); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE // No need to SmiTag as dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + __ AssertSmi(kJavaScriptCallDispatchHandleRegister); __ Push(kJavaScriptCallDispatchHandleRegister); #endif __ SmiTag(frame_size); __ Push(frame_size); __ CallRuntime(Runtime::kStackGuardWithGap); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE __ Pop(kJavaScriptCallDispatchHandleRegister); #endif __ Pop(kJavaScriptCallNewTargetRegister); diff --git a/deps/v8/src/builtins/mips64/builtins-mips64.cc b/deps/v8/src/builtins/mips64/builtins-mips64.cc index cf7ee09c52a8..05333d63e972 100644 --- a/deps/v8/src/builtins/mips64/builtins-mips64.cc +++ b/deps/v8/src/builtins/mips64/builtins-mips64.cc @@ -1065,15 +1065,16 @@ void Builtins::Generate_BaselineOutOfLinePrologue(MacroAssembler* masm) { FrameScope frame_scope(masm, StackFrame::INTERNAL); // Save incoming new target or generator __ Push(kJavaScriptCallNewTargetRegister); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE // No need to SmiTag as dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + __ AssertSmi(kJavaScriptCallDispatchHandleRegister); __ Push(kJavaScriptCallDispatchHandleRegister); #endif __ SmiTag(frame_size); __ Push(frame_size); __ CallRuntime(Runtime::kStackGuardWithGap); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE __ Pop(kJavaScriptCallDispatchHandleRegister); #endif __ Pop(kJavaScriptCallNewTargetRegister); diff --git a/deps/v8/src/builtins/riscv/builtins-riscv.cc b/deps/v8/src/builtins/riscv/builtins-riscv.cc index 06649f0a8134..260bd1b22df2 100644 --- a/deps/v8/src/builtins/riscv/builtins-riscv.cc +++ b/deps/v8/src/builtins/riscv/builtins-riscv.cc @@ -1099,15 +1099,18 @@ void Builtins::Generate_BaselineOutOfLinePrologue(MacroAssembler* masm) { FrameScope frame_scope(masm, StackFrame::INTERNAL); // Save incoming new target or generator __ Push(kJavaScriptCallNewTargetRegister); -#if defined(V8_ENABLE_LEAPTIERING) && defined(V8_TARGET_ARCH_RISCV64) +#if defined(V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE) && \ + defined(V8_TARGET_ARCH_RISCV64) // No need to SmiTag as dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + __ AssertSmi(kJavaScriptCallDispatchHandleRegister); __ Push(kJavaScriptCallDispatchHandleRegister); #endif __ SmiTag(frame_size); __ Push(frame_size); __ CallRuntime(Runtime::kStackGuardWithGap); -#if defined(V8_ENABLE_LEAPTIERING) && defined(V8_TARGET_ARCH_RISCV64) +#if defined(V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE) && \ + defined(V8_TARGET_ARCH_RISCV64) __ Pop(kJavaScriptCallDispatchHandleRegister); #endif __ Pop(kJavaScriptCallNewTargetRegister); diff --git a/deps/v8/src/builtins/x64/builtins-x64.cc b/deps/v8/src/builtins/x64/builtins-x64.cc index b212110efb7b..f6edcf8fd086 100644 --- a/deps/v8/src/builtins/x64/builtins-x64.cc +++ b/deps/v8/src/builtins/x64/builtins-x64.cc @@ -2018,15 +2018,16 @@ void Builtins::Generate_BaselineOutOfLinePrologue(MacroAssembler* masm) { FrameScope inner_frame_scope(masm, StackFrame::INTERNAL); // Save incoming new target or generator __ Push(new_target); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE // No need to SmiTag as dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + __ AssertSmi(kJavaScriptCallDispatchHandleRegister); __ Push(kJavaScriptCallDispatchHandleRegister); #endif __ SmiTag(frame_size); __ Push(frame_size); __ CallRuntime(Runtime::kStackGuardWithGap, 1); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE __ Pop(kJavaScriptCallDispatchHandleRegister); #endif __ Pop(new_target); diff --git a/deps/v8/src/codegen/arm64/macro-assembler-arm64.cc b/deps/v8/src/codegen/arm64/macro-assembler-arm64.cc index 18caab8c5b9f..9a14650a1521 100644 --- a/deps/v8/src/codegen/arm64/macro-assembler-arm64.cc +++ b/deps/v8/src/codegen/arm64/macro-assembler-arm64.cc @@ -1518,14 +1518,15 @@ void MacroAssembler::GenerateTailCallToReturnedCode( FrameScope scope(this, StackFrame::INTERNAL); // Push a copy of the target function, the new target, the actual // argument count, and the dispatch handle. - Register lastreg = V8_ENABLE_LEAPTIERING_BOOL - ? kJavaScriptCallDispatchHandleRegister - : padreg; + Register maybe_dispatch_handle = V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE_BOOL + ? kJavaScriptCallDispatchHandleRegister + : padreg; SmiTag(kJavaScriptCallArgCountRegister); // No need to SmiTag the dispatch handle as it always looks like a Smi. static_assert(kJSDispatchHandleShift > 0); + AssertSmi(maybe_dispatch_handle); Push(kJavaScriptCallTargetRegister, kJavaScriptCallNewTargetRegister, - kJavaScriptCallArgCountRegister, lastreg); + kJavaScriptCallArgCountRegister, maybe_dispatch_handle); // Push another copy as a parameter to the runtime call. PushArgument(kJavaScriptCallTargetRegister); @@ -1534,7 +1535,7 @@ void MacroAssembler::GenerateTailCallToReturnedCode( // Restore target function, new target, actual argument count, and dispatch // handle. - Pop(lastreg, kJavaScriptCallArgCountRegister, + Pop(maybe_dispatch_handle, kJavaScriptCallArgCountRegister, kJavaScriptCallNewTargetRegister, kJavaScriptCallTargetRegister); SmiUntag(kJavaScriptCallArgCountRegister); } diff --git a/deps/v8/src/codegen/loong64/macro-assembler-loong64.cc b/deps/v8/src/codegen/loong64/macro-assembler-loong64.cc index 2bb8052af7b7..b227f5757b55 100644 --- a/deps/v8/src/codegen/loong64/macro-assembler-loong64.cc +++ b/deps/v8/src/codegen/loong64/macro-assembler-loong64.cc @@ -5113,9 +5113,10 @@ void MacroAssembler::GenerateTailCallToReturnedCode( SmiTag(kJavaScriptCallArgCountRegister); Push(kJavaScriptCallTargetRegister, kJavaScriptCallNewTargetRegister, kJavaScriptCallArgCountRegister); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE // No need to SmiTag since dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + AssertSmi(kJavaScriptCallDispatchHandleRegister); Push(kJavaScriptCallDispatchHandleRegister); #endif // Function is also the parameter to the runtime call. @@ -5126,7 +5127,7 @@ void MacroAssembler::GenerateTailCallToReturnedCode( // Restore target function, new target, actual argument count and dispatch // handle. -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE Pop(kJavaScriptCallDispatchHandleRegister); #endif Pop(kJavaScriptCallTargetRegister, kJavaScriptCallNewTargetRegister, diff --git a/deps/v8/src/codegen/mips64/macro-assembler-mips64.cc b/deps/v8/src/codegen/mips64/macro-assembler-mips64.cc index 30bf3dbd2055..13fde931d186 100644 --- a/deps/v8/src/codegen/mips64/macro-assembler-mips64.cc +++ b/deps/v8/src/codegen/mips64/macro-assembler-mips64.cc @@ -6578,16 +6578,17 @@ void MacroAssembler::GenerateTailCallToReturnedCode( SmiTag(kJavaScriptCallArgCountRegister); Push(kJavaScriptCallTargetRegister, kJavaScriptCallNewTargetRegister, kJavaScriptCallArgCountRegister); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE // No need to SmiTag since dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + AssertSmi(kJavaScriptCallDispatchHandleRegister); Push(kJavaScriptCallDispatchHandleRegister); #endif // Function is also the parameter to the runtime call. Push(kJavaScriptCallTargetRegister); CallRuntime(function_id, 1); // Restore target function, new target and actual argument count. -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE Pop(kJavaScriptCallDispatchHandleRegister); #endif Pop(kJavaScriptCallTargetRegister, kJavaScriptCallNewTargetRegister, diff --git a/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc b/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc index 203b2bcd61c7..558a6adb1866 100644 --- a/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc +++ b/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc @@ -186,15 +186,23 @@ void MacroAssembler::GenerateTailCallToReturnedCode( // -- a0 : actual argument count // -- a1 : target function (preserved for callee) // -- a3 : new target (preserved for callee) + // -- a4 : dispatch handle (preserved for callee) // ----------------------------------- { FrameScope scope(this, StackFrame::INTERNAL); - // Push a copy of the target function, the new target and the actual - // argument count. + // Push a copy of the target function, the new target, the actual + // argument count, and the dispatch handle. // Push function as parameter to the runtime call. SmiTag(kJavaScriptCallArgCountRegister); +#if defined(V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE) && \ + defined(V8_TARGET_ARCH_RISCV64) + // No need to SmiTag as dispatch handles always look like Smis. + static_assert(kJSDispatchHandleShift > 0); + AssertSmi(kJavaScriptCallDispatchHandleRegister); + Push(kJavaScriptCallDispatchHandleRegister); +#endif Push(kJavaScriptCallTargetRegister, kJavaScriptCallNewTargetRegister, - kJavaScriptCallArgCountRegister, kJavaScriptCallTargetRegister); + kJavaScriptCallArgCountRegister); CallRuntime(function_id, 1); // Use the return value before restoring a0 @@ -202,6 +210,10 @@ void MacroAssembler::GenerateTailCallToReturnedCode( // Restore target function, new target and actual argument count. Pop(kJavaScriptCallTargetRegister, kJavaScriptCallNewTargetRegister, kJavaScriptCallArgCountRegister); +#if defined(V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE) && \ + defined(V8_TARGET_ARCH_RISCV64) + Pop(kJavaScriptCallDispatchHandleRegister); +#endif SmiUntag(kJavaScriptCallArgCountRegister); } diff --git a/deps/v8/src/codegen/x64/macro-assembler-x64.cc b/deps/v8/src/codegen/x64/macro-assembler-x64.cc index 243438b57f3c..46b644fac005 100644 --- a/deps/v8/src/codegen/x64/macro-assembler-x64.cc +++ b/deps/v8/src/codegen/x64/macro-assembler-x64.cc @@ -1247,9 +1247,10 @@ void MacroAssembler::GenerateTailCallToReturnedCode( Push(kJavaScriptCallNewTargetRegister); SmiTag(kJavaScriptCallArgCountRegister); Push(kJavaScriptCallArgCountRegister); -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE // No need to SmiTag since dispatch handles always look like Smis. static_assert(kJSDispatchHandleShift > 0); + AssertSmi(kJavaScriptCallDispatchHandleRegister); Push(kJavaScriptCallDispatchHandleRegister); #endif // Function is also the parameter to the runtime call. @@ -1260,7 +1261,7 @@ void MacroAssembler::GenerateTailCallToReturnedCode( // Restore target function, new target, actual argument count, and dispatch // handle. -#ifdef V8_ENABLE_LEAPTIERING +#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE Pop(kJavaScriptCallDispatchHandleRegister); #endif Pop(kJavaScriptCallArgCountRegister);