From 41d2ee13be2cc1cd3f6b980f89dd0647c0e2c47c Mon Sep 17 00:00:00 2001 From: Richard Lau Date: Tue, 16 Jun 2026 12:28:06 +0000 Subject: [PATCH 1/3] build: switch coverage-windows to `windows-2022` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub has updated the `windows-2025` runner to Visual Studio 2026. Since Node.js 22.x does not support compilation on VS2026, switch the workflow to `windows-2022` which still has the earlier version of Visual Studio. Signed-off-by: Richard Lau PR-URL: https://github.com/nodejs/node/pull/63940 Refs: https://github.blog/changelog/2026-05-14-github-actions-upcoming-image-migrations/#windows-2025-visual-studio-2026-image-migration Reviewed-By: Michaël Zasso Reviewed-By: Luigi Pinca Reviewed-By: Juan José Arboleda --- .github/workflows/coverage-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage-windows.yml b/.github/workflows/coverage-windows.yml index fa0a52b925f90c..fe3cc7179c2c45 100644 --- a/.github/workflows/coverage-windows.yml +++ b/.github/workflows/coverage-windows.yml @@ -43,7 +43,7 @@ permissions: jobs: coverage-windows: if: github.event.pull_request.draft == false - runs-on: windows-2025 + runs-on: windows-2022 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: From eaa292549e8ef30e5bd45c8e7bfc90e2cee214da Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 20 Jun 2026 16:58:11 +0200 Subject: [PATCH 2/3] http: avoid stream listeners on idle agent sockets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/64004 Fixes: https://github.com/nodejs/node/issues/63989 Reviewed-By: René Reviewed-By: Yagiz Nizipli Reviewed-By: James M Snell Reviewed-By: Robert Nagy --- lib/_http_agent.js | 66 +++++++++++++++++-- .../test-http-agent-free-socket-data-guard.js | 9 +-- test/parallel/test-http-agent-keepalive.js | 5 +- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 2d23f3be63e98a..95498c86de5a08 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -43,12 +43,22 @@ const { filterEnvForProxies, } = require('internal/http'); const { AsyncResource } = require('async_hooks'); -const { async_id_symbol } = require('internal/async_hooks').symbols; +const { + async_id_symbol, + owner_symbol, +} = require('internal/async_hooks').symbols; const { getLazy, kEmptyObject, once, } = require('internal/util'); +const { + onStreamRead, +} = require('internal/stream_base_commons'); +const { + kReadBytesOrError, + streamBaseState, +} = internalBinding('stream_wrap'); const { validateNumber, validateOneOf, @@ -60,6 +70,7 @@ const { getOptionValue } = require('internal/options'); const kOnKeylog = Symbol('onkeylog'); const kRequestOptions = Symbol('requestOptions'); const kRequestAsyncResource = Symbol('requestAsyncResource'); +const kFreeSocketDataGuard = Symbol('freeSocketDataGuard'); // New Agent code. @@ -92,9 +103,51 @@ function freeSocketErrorListener(err) { // in the TCP buffer and be silently consumed as the response for the // *next* request that reuses the socket (response-queue poisoning). // See: https://hackerone.com/reports/3582376 -function freeSocketDataGuard() { - debug('DATA on FREE socket - destroying poisoned socket'); - this.destroy(); +function freeSocketOnReadGuard() { + const nread = streamBaseState[kReadBytesOrError]; + if (nread === 0) return; + + debug('READ on FREE socket - destroying poisoned socket'); + this[owner_symbol].destroy(); +} + +function installFreeSocketDataGuard(socket) { + if (socket.readableLength > 0) { + debug('BUFFERED DATA on FREE socket - destroying poisoned socket'); + socket.destroy(); + return; + } + + if (socket.connecting) { + socket[kFreeSocketDataGuard] = function onConnect() { + socket[kFreeSocketDataGuard] = null; + installFreeSocketDataGuard(socket); + }; + socket.once('connect', socket[kFreeSocketDataGuard]); + return; + } + + const handle = socket._handle; + if (handle) { + handle.onread = freeSocketOnReadGuard; + if (!handle.reading) { + handle.reading = true; + const err = handle.readStart(); + if (err) socket.destroy(); + } + } +} + +function removeFreeSocketDataGuard(socket) { + if (socket[kFreeSocketDataGuard]) { + socket.removeListener('connect', socket[kFreeSocketDataGuard]); + socket[kFreeSocketDataGuard] = null; + } + + const handle = socket._handle; + if (handle?.onread === freeSocketOnReadGuard) { + handle.onread = onStreamRead; + } } function Agent(options) { @@ -206,8 +259,7 @@ function Agent(options) { this.removeSocket(socket, options); socket.once('error', freeSocketErrorListener); - socket.on('data', freeSocketDataGuard); - socket.resume(); + installFreeSocketDataGuard(socket); freeSockets.push(socket); }); @@ -599,7 +651,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { Agent.prototype.reuseSocket = function reuseSocket(socket, req) { debug('have free socket'); socket.removeListener('error', freeSocketErrorListener); - socket.removeListener('data', freeSocketDataGuard); + removeFreeSocketDataGuard(socket); req.reusedSocket = true; socket.ref(); }; diff --git a/test/parallel/test-http-agent-free-socket-data-guard.js b/test/parallel/test-http-agent-free-socket-data-guard.js index 9c1a526aaebb38..2cded0838ce1a4 100644 --- a/test/parallel/test-http-agent-free-socket-data-guard.js +++ b/test/parallel/test-http-agent-free-socket-data-guard.js @@ -8,8 +8,8 @@ // writes a full HTTP response during this window, it is consumed as the // response for the *next* request — poisoning the response queue. // -// The fix attaches a data guard listener + resume() on idle sockets so -// that unsolicited data causes the socket to be destroyed. +// The fix installs a read guard on idle sockets so that unsolicited data +// causes the socket to be destroyed without adding public stream listeners. const common = require('../common'); const assert = require('assert'); @@ -48,8 +48,9 @@ server.listen(0, common.mustCall(() => { assert.strictEqual(agent.freeSockets[name]?.length, 1); const freeSocket = agent.freeSockets[name][0]; assert.strictEqual(freeSocket.parser, null); - // With the fix, a data guard listener is attached - assert.strictEqual(freeSocket.listenerCount('data'), 1); + // With the fix, no public stream listeners are added. + assert.strictEqual(freeSocket.listenerCount('data'), 0); + assert.strictEqual(freeSocket.listenerCount('readable'), 0); // Step 2: Server injects a poisoned response while socket is idle serverSocket.write( diff --git a/test/parallel/test-http-agent-keepalive.js b/test/parallel/test-http-agent-keepalive.js index e4f5c09de2dbde..1cf5a597d0af0b 100644 --- a/test/parallel/test-http-agent-keepalive.js +++ b/test/parallel/test-http-agent-keepalive.js @@ -149,8 +149,9 @@ server.listen(0, common.mustCall(() => { function checkListeners(socket) { const callback = common.mustCall(() => { if (!socket.destroyed) { - // Sockets have freeSocketDataGuard while in the free pool. - assert.strictEqual(socket.listenerCount('data'), 1); + // Sockets have no public stream guard listeners while in the free pool. + assert.strictEqual(socket.listenerCount('readable'), 0); + assert.strictEqual(socket.listenerCount('data'), 0); assert.strictEqual(socket.listenerCount('drain'), 0); // Sockets have freeSocketErrorListener. assert.strictEqual(socket.listenerCount('error'), 1); From bd96dfbf0361576724b65322046e2ca9f9609cb9 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Mon, 22 Jun 2026 11:27:03 -0400 Subject: [PATCH 3/3] 2026-06-23, Version 22.23.1 'Jod' (LTS) PR-URL: https://github.com/nodejs/node/pull/64067 Signed-off-by: RafaelGSS --- CHANGELOG.md | 3 ++- doc/changelogs/CHANGELOG_V22.md | 13 +++++++++++++ src/node_version.h | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ebb1b136a42a..ae538a2dbe5bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,8 @@ release. -22.23.0
+22.23.1
+22.23.0
22.22.3
22.22.2
22.22.1
diff --git a/doc/changelogs/CHANGELOG_V22.md b/doc/changelogs/CHANGELOG_V22.md index bb7bd04770ae66..986a302c1b08e7 100644 --- a/doc/changelogs/CHANGELOG_V22.md +++ b/doc/changelogs/CHANGELOG_V22.md @@ -9,6 +9,7 @@ +22.23.1
22.23.0
22.22.3
22.22.2
@@ -72,6 +73,18 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2026-06-23, Version 22.23.1 'Jod' (LTS), @RafaelGSS + +This release includes a fix for an unexpected behavior introduced +by the recent security release (22.23.0). + +### Commits + +* \[[`41d2ee13be`](https://github.com/nodejs/node/commit/41d2ee13be)] - **build**: switch coverage-windows to `windows-2022` (Richard Lau) [#63940](https://github.com/nodejs/node/pull/63940) +* \[[`eaa292549e`](https://github.com/nodejs/node/commit/eaa292549e)] - **http**: avoid stream listeners on idle agent sockets (Matteo Collina) [#64004](https://github.com/nodejs/node/pull/64004) + ## 2026-06-18, Version 22.23.0 'Jod' (LTS), @aduh95 diff --git a/src/node_version.h b/src/node_version.h index 71b13f9e211d54..98edc11fc493fe 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -29,7 +29,7 @@ #define NODE_VERSION_IS_LTS 1 #define NODE_VERSION_LTS_CODENAME "Jod" -#define NODE_VERSION_IS_RELEASE 0 +#define NODE_VERSION_IS_RELEASE 1 #ifndef NODE_STRINGIFY #define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n)