Skip to content

Commit bf30af2

Browse files
committed
quic: fix crash in early handshake failure
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6
1 parent c2d032f commit bf30af2

2 files changed

Lines changed: 64 additions & 2 deletions

File tree

src/quic/http3.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,17 @@ class Http3ApplicationImpl final : public Session::Application {
262262
}
263263

264264
void BeginShutdown() override {
265-
if (conn_) nghttp3_conn_submit_shutdown_notice(*this);
265+
// Only submit a shutdown notice if the H3 connection was fully
266+
// started (control streams bound). If the TLS handshake failed
267+
// before Start() was called, conn_ exists but its control streams
268+
// are unbound, and nghttp3_conn_submit_shutdown_notice would crash.
269+
if (conn_ && started_) nghttp3_conn_submit_shutdown_notice(*this);
266270
}
267271

268272
void CompleteShutdown() override {
269-
if (conn_) nghttp3_conn_shutdown(*this);
273+
// Same guard as BeginShutdown — nghttp3_conn_shutdown asserts
274+
// that the control stream is bound (conn->tx.ctrl != NULL).
275+
if (conn_ && started_) nghttp3_conn_shutdown(*this);
270276
}
271277

272278
bool ReceiveStreamData(stream_id id,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Flags: --experimental-quic --no-warnings
2+
3+
// Regression test: HTTP/3 server must not crash when a session is closed
4+
// before the H3 application is fully started (control streams bound).
5+
// Previously, closing such a session would call nghttp3_conn_shutdown on
6+
// an H3 connection whose control streams were never bound, causing an
7+
// assertion failure in nghttp3 (conn->tx.ctrl != NULL).
8+
//
9+
// The test creates an H3 server and a client that immediately closes the
10+
// session before the handshake completes. The server creates the H3
11+
// application during ALPN negotiation, but Start() (which binds control
12+
// streams) hasn't been called yet when the session is torn down.
13+
// The server must handle this gracefully without crashing.
14+
15+
import { hasQuic, skip, mustNotCall } from '../common/index.mjs';
16+
import { setTimeout } from 'node:timers/promises';
17+
import * as fixtures from '../common/fixtures.mjs';
18+
19+
const { readKey } = fixtures;
20+
21+
if (!hasQuic) {
22+
skip('QUIC is not enabled');
23+
}
24+
25+
const { listen, connect } = await import('node:quic');
26+
const { createPrivateKey } = await import('node:crypto');
27+
28+
const key = createPrivateKey(readKey('agent1-key.pem'));
29+
const cert = readKey('agent1-cert.pem');
30+
31+
const serverEndpoint = await listen(async (serverSession) => {
32+
await serverSession.closed;
33+
}, {
34+
sni: { '*': { keys: [key], certs: [cert] } },
35+
onheaders: mustNotCall(),
36+
});
37+
38+
// Connect then immediately close the session before the handshake completes.
39+
// This exercises the H3 shutdown path on the server while the H3 application
40+
// exists but hasn't started (control streams not yet bound).
41+
const clientSession = await connect(serverEndpoint.address, {
42+
servername: 'localhost',
43+
// h3 ALPN — must match the server so the H3 application is selected
44+
// on the server side before we tear it down.
45+
});
46+
47+
// Close immediately — don't wait for handshake.
48+
await clientSession.close();
49+
50+
// Give the server time to process the close and tear down the session.
51+
await setTimeout(500);
52+
53+
// The critical assertion: reaching this point without a crash means the
54+
// server correctly handled the H3 shutdown before control streams were
55+
// bound. Verify the endpoint is still alive by closing it gracefully.
56+
await serverEndpoint.close();

0 commit comments

Comments
 (0)