-
Notifications
You must be signed in to change notification settings - Fork 30.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
src: fix HTTP2 mem leak on premature close and ERR_PROTO
This commit fixes a memory leak when the socket is suddenly closed by the peer (without GOAWAY notification) and when invalid header (by nghttp2) is identified and the connection is terminated by peer. Refs: https://hackerone.com/reports/2841362 PR-URL: nodejs-private/node-private#650 Reviewed-By: James M Snell <[email protected]> CVE-ID: CVE-2025-23085
- Loading branch information
Showing
7 changed files
with
220 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// Flags: --expose-internals | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
if (!common.hasCrypto) common.skip('missing crypto'); | ||
|
||
const h2 = require('http2'); | ||
const net = require('net'); | ||
const assert = require('assert'); | ||
const { ServerHttp2Session } = require('internal/http2/core'); | ||
|
||
async function sendInvalidLastStreamId(server) { | ||
const client = new net.Socket(); | ||
|
||
const address = server.address(); | ||
if (!common.hasIPv6 && address.family === 'IPv6') { | ||
// Necessary to pass CI running inside containers. | ||
client.connect(address.port); | ||
} else { | ||
client.connect(address); | ||
} | ||
|
||
client.on('connect', common.mustCall(function() { | ||
// HTTP/2 preface | ||
client.write(Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'utf8')); | ||
|
||
// Empty SETTINGS frame | ||
client.write(Buffer.from([0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00])); | ||
|
||
// GOAWAY frame with custom debug message | ||
const goAwayFrame = [ | ||
0x00, 0x00, 0x21, // Length: 33 bytes | ||
0x07, // Type: GOAWAY | ||
0x00, // Flags | ||
0x00, 0x00, 0x00, 0x00, // Stream ID: 0 | ||
0x00, 0x00, 0x00, 0x01, // Last Stream ID: 1 | ||
0x00, 0x00, 0x00, 0x00, // Error Code: 0 (No error) | ||
]; | ||
|
||
// Add the debug message | ||
const debugMessage = 'client transport shutdown'; | ||
const goAwayBuffer = Buffer.concat([ | ||
Buffer.from(goAwayFrame), | ||
Buffer.from(debugMessage, 'utf8'), | ||
]); | ||
|
||
client.write(goAwayBuffer); | ||
client.destroy(); | ||
})); | ||
} | ||
|
||
const server = h2.createServer(); | ||
|
||
server.on('error', common.mustNotCall()); | ||
|
||
server.on( | ||
'sessionError', | ||
common.mustCall((err, session) => { | ||
// When destroying the session, on Windows, we would get ECONNRESET | ||
// errors, make sure we take those into account in our tests. | ||
if (err.code !== 'ECONNRESET') { | ||
assert.strictEqual(err.code, 'ERR_HTTP2_ERROR'); | ||
assert.strictEqual(err.name, 'Error'); | ||
assert.strictEqual(err.message, 'Protocol error'); | ||
assert.strictEqual(session instanceof ServerHttp2Session, true); | ||
} | ||
session.close(); | ||
server.close(); | ||
}), | ||
); | ||
|
||
server.listen( | ||
0, | ||
common.mustCall(async () => { | ||
await sendInvalidLastStreamId(server); | ||
}), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
// Flags: --expose-internals | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
if (!common.hasCrypto) common.skip('missing crypto'); | ||
|
||
const h2 = require('http2'); | ||
const net = require('net'); | ||
|
||
async function requestAndClose(server) { | ||
const client = new net.Socket(); | ||
|
||
const address = server.address(); | ||
if (!common.hasIPv6 && address.family === 'IPv6') { | ||
// Necessary to pass CI running inside containers. | ||
client.connect(address.port); | ||
} else { | ||
client.connect(address); | ||
} | ||
|
||
client.on('connect', common.mustCall(function() { | ||
// Send HTTP/2 Preface | ||
client.write(Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'utf8')); | ||
|
||
// Send a SETTINGS frame (empty payload) | ||
client.write(Buffer.from([0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00])); | ||
|
||
const streamId = 1; | ||
// Send a valid HEADERS frame | ||
const headersFrame = Buffer.concat([ | ||
Buffer.from([ | ||
0x00, 0x00, 0x0c, // Length: 12 bytes | ||
0x01, // Type: HEADERS | ||
0x05, // Flags: END_HEADERS + END_STREAM | ||
(streamId >> 24) & 0xFF, // Stream ID: high byte | ||
(streamId >> 16) & 0xFF, | ||
(streamId >> 8) & 0xFF, | ||
streamId & 0xFF, // Stream ID: low byte | ||
]), | ||
Buffer.from([ | ||
0x82, // Indexed Header Field Representation (Predefined ":method: GET") | ||
0x84, // Indexed Header Field Representation (Predefined ":path: /") | ||
0x86, // Indexed Header Field Representation (Predefined ":scheme: http") | ||
0x44, 0x0a, // Custom ":authority: localhost" | ||
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, | ||
]), | ||
]); | ||
client.write(headersFrame); | ||
|
||
// Send a valid DATA frame | ||
const dataFrame = Buffer.concat([ | ||
Buffer.from([ | ||
0x00, 0x00, 0x05, // Length: 5 bytes | ||
0x00, // Type: DATA | ||
0x00, // Flags: No flags | ||
(streamId >> 24) & 0xFF, // Stream ID: high byte | ||
(streamId >> 16) & 0xFF, | ||
(streamId >> 8) & 0xFF, | ||
streamId & 0xFF, // Stream ID: low byte | ||
]), | ||
Buffer.from('Hello', 'utf8'), // Data payload | ||
]); | ||
client.write(dataFrame); | ||
|
||
// Does not wait for server to reply. Shutdown the socket | ||
client.end(); | ||
})); | ||
} | ||
|
||
const server = h2.createServer(); | ||
|
||
server.on('error', common.mustNotCall()); | ||
|
||
server.on( | ||
'session', | ||
common.mustCall((session) => { | ||
session.on('close', common.mustCall(() => { | ||
server.close(); | ||
})); | ||
}), | ||
); | ||
|
||
server.listen( | ||
0, | ||
common.mustCall(async () => { | ||
await requestAndClose(server); | ||
}), | ||
); |