From cabca4b5e70381f3c670b15b7b1ed7bed11dce8b Mon Sep 17 00:00:00 2001 From: zerone0x Date: Fri, 6 Mar 2026 08:06:05 +0100 Subject: [PATCH 1/2] quic: guard against null impl_ in UpdateDataStats When a QUIC session's handshake fails or the connection is terminated early, UpdateDataStats() can be called before impl_ has been initialized, leading to a null pointer dereference and SIGSEGV. Add an early return when impl_ is nullptr to prevent the crash. Fixes: https://github.com/nodejs/node/issues/62057 Co-Authored-By: Claude --- src/quic/session.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/quic/session.cc b/src/quic/session.cc index 39ffad3e09faa8..fa523ebfc8a5bc 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -2232,6 +2232,7 @@ void Session::ExtendOffset(size_t amount) { } void Session::UpdateDataStats() { + if (!impl_) return; Debug(this, "Updating data stats"); auto& stats_ = impl_->stats_; ngtcp2_conn_info info; From da731418eaceae2c554c25e62be4796978cbef0e Mon Sep 17 00:00:00 2001 From: zerone0x Date: Fri, 6 Mar 2026 13:08:23 +0100 Subject: [PATCH 2/2] test: add regression test for quic UpdateDataStats null-deref Exercises the crash path in Session::SendDataStats() where on_exit fires UpdateDataStats() after Destroy() has already reset impl_ to nullptr. Refs: https://github.com/nodejs/node/pull/62126 --- ...-session-update-data-stats-after-close.mjs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 test/parallel/test-quic-session-update-data-stats-after-close.mjs diff --git a/test/parallel/test-quic-session-update-data-stats-after-close.mjs b/test/parallel/test-quic-session-update-data-stats-after-close.mjs new file mode 100644 index 00000000000000..bba1bb47e2f723 --- /dev/null +++ b/test/parallel/test-quic-session-update-data-stats-after-close.mjs @@ -0,0 +1,54 @@ +// Flags: --experimental-quic --no-warnings +// Regression test for https://github.com/nodejs/node/pull/62126 +// UpdateDataStats() must not crash when called after the session's impl_ has +// been reset to null (i.e. after the session is destroyed). +// +// The crash path is in Session::SendDatagram(): +// auto on_exit = OnScopeLeave([&] { ...; UpdateDataStats(); }); +// If the send encounters an error it calls Close(SILENT) → Destroy() → +// impl_.reset(). The on_exit lambda then fires with impl_ == nullptr. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +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 keys = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const certs = fixtures.readKey('agent1-cert.pem'); + +// Datagrams must be enabled on both sides for sendDatagram() to work. +const kDatagramOptions = { + transportParams: { + maxDatagramFrameSize: 1200n, + }, +}; + +const serverEndpoint = await listen( + mustCall((serverSession) => { + serverSession.opened.then(mustCall(async () => { + // Send a datagram then immediately close. This exercises the + // UpdateDataStats() call that fires via on_exit after SendDatagram — + // the close can race with or precede the stats update, leaving + // impl_ == nullptr. Before the fix this would crash. + serverSession.sendDatagram(Buffer.from('hello')).catch(() => {}); + serverSession.close(); + })); + }), + { keys, certs, ...kDatagramOptions }, +); + +const clientSession = await connect(serverEndpoint.address, kDatagramOptions); +await clientSession.opened; + +// Mirror the race on the client side. +clientSession.sendDatagram(Buffer.from('world')).catch(() => {}); +clientSession.close(); + +await clientSession.closed; +serverEndpoint.close(); +await serverEndpoint.closed;