From b2e5ff8060be399eef3c317a36b683f789d90a41 Mon Sep 17 00:00:00 2001 From: StarProxima <34741787+StarProxima@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:36:43 +0300 Subject: [PATCH] Fix UQUICConn Close after cancellation and preset transport parameters UQUICConn.Close did not wait on signalc after canceling the handshake, as QUICConn.Close does since crypto/tls fixed golang/go#77274 (23b5a72). Closing a UQUICConn whose context was canceled during the handshake could block forever. SetTransportParameters only set quic.transportParams, which a ClientHello built from a preset does not use: ApplyPreset clones the spec, so the QUICTransportParametersExtension that is marshaled is the clone. The parameters never reached the peer. Store them in that extension. TestUQUICPresetTransportParameters and TestUQUICContextCancelation fail without the change: the server receives empty transport parameters, and the cancellation test hangs. --- u_quic.go | 17 ++++--- u_quic_preset_test.go | 110 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 u_quic_preset_test.go diff --git a/u_quic.go b/u_quic.go index 6493ddbdb3..618f5782f1 100644 --- a/u_quic.go +++ b/u_quic.go @@ -90,6 +90,7 @@ func (q *UQUICConn) Close() error { 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. } @@ -184,14 +185,16 @@ func (q *UQUICConn) SetTransportParameters(params []byte) { if params == nil { params = []byte{} } - q.conn.quic.transportParams = params // this won't be used for building ClientHello when using a preset + q.conn.quic.transportParams = params - // // instead, we set the transport parameters hold by the ClientHello - // for _, ext := range q.conn.Extensions { - // if qtp, ok := ext.(*QUICTransportParametersExtension); ok { - // qtp.TransportParametersExtData = params - // } - // } + // A ClientHello built from a preset marshals its own + // QUICTransportParametersExtension, which ApplyPreset cloned from the + // caller's spec. Put the parameters there, so they reach the peer. + for _, ext := range q.conn.Extensions { + if qtp, ok := ext.(*QUICTransportParametersExtension); ok { + qtp.marshalResult = append([]byte{}, params...) + } + } if q.conn.quic.started { <-q.conn.quic.signalc diff --git a/u_quic_preset_test.go b/u_quic_preset_test.go new file mode 100644 index 0000000000..427ee53337 --- /dev/null +++ b/u_quic_preset_test.go @@ -0,0 +1,110 @@ +// 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 ( + "bytes" + "context" + "sync" + "testing" +) + +func newUQUICPresetClient(t *testing.T) *UQUICConn { + t.Helper() + clientConfig := testConfigClient.Clone() + clientConfig.NextProtos = []string{"h3"} + client := UQUICClient(&QUICConfig{TLSConfig: clientConfig}, 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) + } + return client +} + +func newUQUICTestServer() *QUICConn { + serverConfig := testConfigServer.Clone() + serverConfig.NextProtos = []string{"h3"} + return QUICServer(&QUICConfig{TLSConfig: serverConfig}) +} + +type uquicTestEndpoint interface { + Start(context.Context) error + NextEvent() QUICEvent + HandleData(QUICEncryptionLevel, []byte) error +} + +// runUQUICHandshake pumps events between the endpoints until both are idle +// or one fails, and returns the transport parameters each side received. +func runUQUICHandshake(ctx context.Context, client, server uquicTestEndpoint) (got [2][]byte, err error) { + endpoints := []uquicTestEndpoint{client, server} + for _, endpoint := range endpoints { + if err := endpoint.Start(ctx); err != nil { + return got, err + } + } + for current, idle := 0, 0; idle < 2; { + event := endpoints[current].NextEvent() + if event.Kind == QUICNoEvent { + idle++ + current = 1 - current + continue + } + idle = 0 + switch event.Kind { + case QUICWriteData: + if err := endpoints[1-current].HandleData(event.Level, event.Data); err != nil { + return got, err + } + case QUICTransportParameters: + got[current] = append([]byte{}, event.Data...) + } + } + return got, nil +} + +// SetTransportParameters must reach the ClientHello when a preset is applied: +// ApplyPreset clones the spec, so the extension the caller passed is no longer +// the one that is marshaled. +func TestUQUICPresetTransportParameters(t *testing.T) { + params := []byte{0x04, 0x04, 0x80, 0x10, 0x00, 0x00} // initial_max_data + client := newUQUICPresetClient(t) + defer client.Close() + client.SetTransportParameters(params) + server := newUQUICTestServer() + defer server.Close() + server.SetTransportParameters(nil) + + got, err := runUQUICHandshake(t.Context(), client, server) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got[1], params) { + t.Errorf("server received transport parameters %x, want %x", got[1], params) + } +} + +// Canceling the context concurrently with a handshake must not race with +// UQUICConn methods, as for QUICConn. See https://go.dev/issue/77274. +func TestUQUICContextCancelation(t *testing.T) { + for range 20 { + ctx, cancel := context.WithCancel(context.Background()) + client := newUQUICPresetClient(t) + client.SetTransportParameters(nil) + server := newUQUICTestServer() + server.SetTransportParameters(nil) + var wg sync.WaitGroup + wg.Go(func() { + _, _ = runUQUICHandshake(ctx, client, server) + client.Close() + server.Close() + }) + wg.Go(cancel) + wg.Wait() + } +}