From 2fac5afde6a18c602772a5601be4dc6d55e00b45 Mon Sep 17 00:00:00 2001 From: arian ahmidi Date: Tue, 13 Oct 2026 18:38:21 +0330 Subject: [PATCH] Fix UQUICConn.Close deadlock and report handshake errors in NextEvent UQUICConn mirrors QUICConn, but missed two changes from the Go 1.26 QUIC rework: - Close: the handshake goroutine now always sends on signalc before checking for cancellation, and QUICConn.Close receives it before draining blockedc. UQUICConn.Close didn't, so closing during a handshake hung forever. - NextEvent: QUICConn reports a failed handshake once as QUICErrorEvent; UQUICConn never did, so a TLS failure looked like no events. Both now match QUICConn. Adds UQUICConn versions of TestQUICCanceledWaitingForData and TestQUICHandshakeError, for HelloGolang and HelloCustom; both fail without this change. Fixes #421 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KqYot5fp6kvHireReN6Lu7 --- u_quic.go | 12 ++++- u_quic_test.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 u_quic_test.go diff --git a/u_quic.go b/u_quic.go index 6493ddbdb3..4d2aeefaf1 100644 --- a/u_quic.go +++ b/u_quic.go @@ -73,6 +73,15 @@ func (q *UQUICConn) NextEvent() QUICEvent { <-qs.signalc <-qs.blockedc } + if err := q.conn.handshakeErr; err != nil { + if qs.errorReturned { + return QUICEvent{Kind: QUICNoEvent} + } + qs.errorReturned = true + qs.events = nil + qs.nextEvent = 0 + return QUICEvent{Kind: QUICErrorEvent, Err: q.conn.handshakeErr} + } if qs.nextEvent >= len(qs.events) { qs.events = qs.events[:0] qs.nextEvent = 0 @@ -86,10 +95,11 @@ func (q *UQUICConn) NextEvent() QUICEvent { // Close closes the connection and stops any in-progress handshake. func (q *UQUICConn) Close() error { - if q.conn.quic.cancel == nil { + if q.conn.quic.ctx == nil { return nil // never started } q.conn.quic.cancel() + <-q.conn.quic.signalc for range q.conn.quic.blockedc { // Wait for the handshake goroutine to return. } diff --git a/u_quic_test.go b/u_quic_test.go new file mode 100644 index 0000000000..46866cea42 --- /dev/null +++ b/u_quic_test.go @@ -0,0 +1,129 @@ +// Copyright 2026 The uTLS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tls + +import ( + "context" + "errors" + "testing" + "time" +) + +// newTestUQUICClient returns a UQUICConn for helloID, with a QUIC-capable preset for +// HelloCustom. +func newTestUQUICClient(t *testing.T, config *Config, helloID ClientHelloID) *UQUICConn { + t.Helper() + config.MinVersion = VersionTLS13 + config.NextProtos = []string{"h3"} + client := UQUICClient(&QUICConfig{TLSConfig: config}, helloID) + if helloID == HelloCustom { + preset := go127ClientHelloSpec([]SignatureScheme{ECDSAWithP256AndSHA256}) + preset.Extensions = append(preset.Extensions, + &ALPNExtension{AlpnProtocols: []string{"h3"}}, + &QUICTransportParametersExtension{}, + ) + if err := client.ApplyPreset(preset); err != nil { + t.Fatal(err) + } + } + client.SetTransportParameters(nil) + return client +} + +// Like TestQUICCanceledWaitingForData, for UQUICConn: Close while the handshake waits for +// the server must return instead of deadlocking. +func TestUQUICCanceledWaitingForData(t *testing.T) { + for _, helloID := range []ClientHelloID{HelloGolang, HelloCustom} { + t.Run(helloID.Str(), func(t *testing.T) { + client := newTestUQUICClient(t, testConfigClient.Clone(), helloID) + if err := client.Start(context.Background()); err != nil { + t.Fatal(err) + } + for client.NextEvent().Kind != QUICNoEvent { + } + closed := make(chan error, 1) + go func() { closed <- client.Close() }() + select { + case err := <-closed: + if !errors.Is(err, alertCloseNotify) { + t.Errorf("Close() = %v, want alertCloseNotify", err) + } + case <-time.After(5 * time.Second): + t.Fatal("Close() deadlocked") + } + }) + } +} + +// Like TestQUICHandshakeError, for UQUICConn: a failed handshake is reported once by +// NextEvent as a QUICErrorEvent. +func TestUQUICHandshakeError(t *testing.T) { + for _, helloID := range []ClientHelloID{HelloGolang, HelloCustom} { + t.Run(helloID.Str(), func(t *testing.T) { + clientConfig := testConfigClient.Clone() + clientConfig.InsecureSkipVerify = false + clientConfig.ServerName = "name" + client := newTestUQUICClient(t, clientConfig, helloID) + defer client.Close() + + serverConfig := testConfigServer.Clone() + serverConfig.MinVersion = VersionTLS13 + serverConfig.NextProtos = []string{"h3"} + server := QUICServer(&QUICConfig{TLSConfig: serverConfig}) + defer server.Close() + server.SetTransportParameters(nil) + + if err := client.Start(context.Background()); err != nil { + t.Fatal(err) + } + if err := server.Start(context.Background()); err != nil { + t.Fatal(err) + } + var handshakeErr error + for handshakeErr == nil { + ev := client.NextEvent() + if ev.Kind == QUICErrorEvent { + t.Fatalf("QUICErrorEvent before the handshake failed: %v", ev.Err) + } + if ev.Kind == QUICWriteData { + if err := server.HandleData(ev.Level, ev.Data); err != nil { + t.Fatal(err) + } + continue + } + if ev.Kind != QUICNoEvent { + continue + } + progressed := false + for sev := server.NextEvent(); sev.Kind != QUICNoEvent; sev = server.NextEvent() { + if sev.Kind == QUICWriteData { + progressed = true + if err := client.HandleData(sev.Level, sev.Data); err != nil { + handshakeErr = err + break + } + } + } + if !progressed && handshakeErr == nil { + t.Fatal("handshake stalled without an error") + } + } + if !errors.Is(handshakeErr, AlertError(alertBadCertificate)) { + t.Fatalf("handshake error %v, want alertBadCertificate", handshakeErr) + } + + ev := client.NextEvent() + if ev.Kind != QUICErrorEvent { + t.Fatalf("NextEvent() = %v, want QUICErrorEvent", ev.Kind) + } + if !errors.Is(ev.Err, AlertError(alertBadCertificate)) { + t.Errorf("QUICErrorEvent.Err = %v, want alertBadCertificate", ev.Err) + } + if ev := client.NextEvent(); ev.Kind != QUICNoEvent { + t.Errorf("second NextEvent() = %v, want QUICNoEvent", ev.Kind) + } + }) + } +}