This brings in CL 773940, which should reduce flakes for HTTP/3 tests.

For #79104
For #78737

Change-Id: I4875570b3a615faba0ea686a71621d4c6a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/774500
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Nicholas S. Husin 2026-05-05 14:16:45 -04:00 committed by Nicholas Husin
parent 0b54a75319
commit 19f8047c26
10 changed files with 146 additions and 11 deletions

View file

@ -4,7 +4,7 @@ go 1.27
require (
golang.org/x/crypto v0.50.0
golang.org/x/net v0.53.1-0.20260420212600-f70faeaf29ed
golang.org/x/net v0.53.1-0.20260505181449-5e11a5ab891c
)
require (

View file

@ -1,7 +1,7 @@
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/net v0.53.1-0.20260420212600-f70faeaf29ed h1:7TjurTHs0hDGI2ZSxkEJy0ToTN3VoUsPAZ6MYUEvl48=
golang.org/x/net v0.53.1-0.20260420212600-f70faeaf29ed/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.53.1-0.20260505181449-5e11a5ab891c h1:wrF5RobEIw4Wj2wvtEac1FCXJLWmFGe+ZolWSwFYN5s=
golang.org/x/net v0.53.1-0.20260505181449-5e11a5ab891c/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=

View file

@ -188,6 +188,7 @@ func (r *bodyReader) Read(p []byte) (n int, err error) {
}
var dec qpackDecoder
if err := dec.decode(r.st, func(_ indexType, name, value string) error {
name = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(name))
if _, ok := r.trailer[name]; ok {
r.trailer.Add(name, value)
}

View file

@ -8,6 +8,7 @@ import (
"errors"
"io"
"golang.org/x/net/http/httpguts"
"golang.org/x/net/http2/hpack"
)
@ -330,3 +331,26 @@ func appendPrefixedString(b []byte, firstByte byte, prefixLen uint8, s string) [
}
return b
}
// validWireHeaderFieldName reports whether v is a valid header field
// name (key). See httpguts.ValidHeaderFieldName for the base rules.
//
// Further, http3 says:
// "A request or response containing uppercase characters in field names MUST
// be treated as malformed."
//
// This function does not validate whether a pseudo-header field name is valid.
func validWireHeaderFieldName(v string) bool {
if len(v) == 0 {
return false
}
for _, r := range v {
if !httpguts.IsTokenRune(r) {
return false
}
if 'A' <= r && r <= 'Z' {
return false
}
}
return true
}

View file

@ -4,6 +4,10 @@
package http3
import (
"golang.org/x/net/internal/httpcommon"
)
type qpackEncoder struct {
// The encoder has no state for now,
// but that'll change once we add dynamic table support.
@ -28,6 +32,18 @@ func (qe *qpackEncoder) encode(headers func(func(itype indexType, name, value st
b = appendPrefixedInt(b, 0, 7, 0) // Delta Base
headers(func(itype indexType, name, value string) {
// Technically, it is the responsibility of the protocol using HTTP/3
// to ensure that all field names are already in lowercase. However,
// this QPACK implementation is solely used by and live in the http3
// package. So, we might as well do the lowercasing here to make sure
// we do not miss any callsites or need to create yet another struct
// wrapping the qpackEncoder.
name, ascii := httpcommon.LowerHeader(name)
// Skip writing invalid headers. Per RFC 9114 section 4.2: "Field
// names are strings containing a subset of ASCII characters."
if !ascii {
return
}
if itype == mayIndex {
if i, ok := staticTableByNameValue[tableEntry{name, value}]; ok {
b = appendIndexedFieldLine(b, staticTable, i)

View file

@ -345,6 +345,9 @@ func (cc *clientConn) handleHeaders(st *stream) (statusCode int, h http.Header,
// Issue #71374: Consider tracking the never-indexed status of headers
// with the N bit set in their QPACK encoding.
err = cc.dec.decode(st, func(_ indexType, name, value string) error {
if !httpguts.ValidHeaderFieldValue(value) {
return &streamError{errH3MessageError, "invalid field value"}
}
switch {
case name == ":status":
if haveStatus {
@ -372,6 +375,9 @@ func (cc *clientConn) handleHeaders(st *stream) (statusCode int, h http.Header,
cookie += "; " + value
}
default:
if !validWireHeaderFieldName(name) {
return &streamError{errH3MessageError, "invalid field name"}
}
if h == nil {
h = make(http.Header)
}

View file

@ -324,6 +324,35 @@ func (sc *serverConn) handlePushStream(*stream) error {
}
}
// hasDisallowedConnectionHeader reports whether h contains connnection headers
// that are not allowed in HTTP/3:
//
// "An endpoint MUST NOT generate an HTTP/3 field section containing
// connection-specific fields; any message containing connection-specific
// fields MUST be treated as malformed."
//
// "The only exception to this is the TE header field, which MAY be present in
// an HTTP/3 request header; when it is, it MUST NOT contain any value other
// than "trailers"."
func hasDisallowedConnectionHeader(h http.Header) bool {
neverAllowed := []string{
"Connection",
"Keep-Alive",
"Proxy-Connection",
"Transfer-Encoding",
"Upgrade",
}
for _, k := range neverAllowed {
if _, ok := h[k]; ok {
return true
}
}
if te, ok := h["Te"]; ok && (len(te) != 1 || te[0] != "trailers") {
return true
}
return false
}
type pseudoHeader struct {
method string
scheme string
@ -337,22 +366,45 @@ func (sc *serverConn) parseHeader(st *stream) (http.Header, pseudoHeader, error)
return nil, pseudoHeader{}, err
}
if ftype != frameTypeHeaders {
return nil, pseudoHeader{}, err
return nil, pseudoHeader{}, &streamError{errH3MessageError, "received other frames when expecting HEADERS"}
}
header := make(http.Header)
var pHeader pseudoHeader
var dec qpackDecoder
var hasMethod, hasScheme, hasPath, hasAuthority bool
if err := dec.decode(st, func(_ indexType, name, value string) error {
if !httpguts.ValidHeaderFieldValue(value) {
return &streamError{errH3MessageError, "invalid field value"}
}
switch name {
case ":method":
if hasMethod {
return &streamError{errH3MessageError, "duplicate :method"}
}
hasMethod = true
pHeader.method = value
case ":scheme":
if hasScheme {
return &streamError{errH3MessageError, "duplicate :scheme"}
}
hasScheme = true
pHeader.scheme = value
case ":path":
if hasPath {
return &streamError{errH3MessageError, "duplicate :path"}
}
hasPath = true
pHeader.path = value
case ":authority":
if hasAuthority {
return &streamError{errH3MessageError, "duplicate :authority"}
}
hasAuthority = true
pHeader.authority = value
default:
if !validWireHeaderFieldName(name) {
return &streamError{errH3MessageError, "invalid field name"}
}
header.Add(name, value)
}
return nil
@ -362,6 +414,29 @@ func (sc *serverConn) parseHeader(st *stream) (http.Header, pseudoHeader, error)
if err := st.endFrame(); err != nil {
return nil, pseudoHeader{}, err
}
if hasDisallowedConnectionHeader(header) {
return nil, pseudoHeader{}, &streamError{errH3MessageError, "invalid connection-related header"}
}
// "All HTTP/3 requests MUST include exactly one value for the :method,
// :scheme, and :path pseudo-header fields, unless the request is a CONNECT
// request"
//
// "A CONNECT request MUST be constructed as follows:
// - The :method pseudo-header field is set to "CONNECT"
// - The :scheme and :path pseudo-header fields are omitted
// - The :authority pseudo-header field contains the host and port to connect to"
if !hasMethod {
return nil, pseudoHeader{}, &streamError{errH3MessageError, "missing :method"}
}
if pHeader.method != "CONNECT" && (!hasScheme || !hasPath) {
return nil, pseudoHeader{}, &streamError{errH3MessageError, "missing :scheme or :path for non-CONNECT requests"}
}
if pHeader.method == "CONNECT" && (hasScheme || hasPath || !hasAuthority) {
return nil, pseudoHeader{}, &streamError{
errH3MessageError, "CONNECT request must only have :method and :authority pseudo-headers",
}
}
return header, pHeader, nil
}

View file

@ -163,8 +163,8 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType)
num := id.num()
styp := id.streamType()
if id.initiator() == c.side {
if num < c.streams.localLimit[styp].opened {
// This stream was created by us, and has been closed.
// This stream was created by us, and has been closed.
if c.streams.localLimit[styp].wasOpened(num) {
return nil
}
// Received a frame for a stream that should be originated by us,

View file

@ -32,28 +32,41 @@ func (lim *localStreamLimits) open(ctx context.Context, c *Conn) (num int64, err
if err := lim.gate.waitAndLock(ctx); err != nil {
return 0, err
}
defer lim.unlock()
if lim.opened < 0 {
lim.gate.unlock(true)
return 0, errConnClosed
}
num = lim.opened
lim.opened++
lim.gate.unlock(lim.opened < lim.max)
return num, nil
}
// unlock is a wrapper around lim.gate.unlock. This should be used in lieu of
// lim.gate.unlock directly so that out gate-state-setting logic is consistent
// across multiple calls.
func (lim *localStreamLimits) unlock() {
lim.gate.unlock(lim.opened < lim.max)
}
// wasOpened reports whether the given stream was opened by us.
func (lim *localStreamLimits) wasOpened(num int64) bool {
lim.gate.lock()
defer lim.unlock()
return num < lim.opened
}
// connHasClosed indicates the connection has been closed, locally or by the peer.
func (lim *localStreamLimits) connHasClosed() {
lim.gate.lock()
lim.opened = -1
lim.gate.unlock(true)
lim.unlock()
}
// setMax sets the MAX_STREAMS provided by the peer.
func (lim *localStreamLimits) setMax(maxStreams int64) {
lim.gate.lock()
lim.max = max(lim.max, maxStreams)
lim.gate.unlock(lim.opened < lim.max)
lim.unlock()
}
// remoteStreamLimits are limits on the number of open streams created by the peer.

View file

@ -7,7 +7,7 @@ golang.org/x/crypto/cryptobyte/asn1
golang.org/x/crypto/hkdf
golang.org/x/crypto/internal/alias
golang.org/x/crypto/internal/poly1305
# golang.org/x/net v0.53.1-0.20260420212600-f70faeaf29ed
# golang.org/x/net v0.53.1-0.20260505181449-5e11a5ab891c
## explicit; go 1.25.0
golang.org/x/net/dns/dnsmessage
golang.org/x/net/http/httpguts