From 447bca32d8e0a1cda07878605a6ce298dae60d28 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 12 Nov 2026 18:12:26 -0800 Subject: [PATCH] fix(session): allow large filesync pattern lists Lift Go 1.27's header value count limit for session connections while preserving the header byte limit and compatibility with earlier Go versions. Add regression coverage for large include and exclude pattern lists. Signed-off-by: Tonis Tiigi --- client/client_local_source_test.go | 59 ++++++++++++++++++++++++++++++ client/client_test.go | 1 + session/grpc.go | 2 +- session/http2server_go127.go | 29 +++++++++++++++ session/http2server_legacy.go | 15 ++++++++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 session/http2server_go127.go create mode 100644 session/http2server_legacy.go diff --git a/client/client_local_source_test.go b/client/client_local_source_test.go index 38208b9e52f5..06ab274bd988 100644 --- a/client/client_local_source_test.go +++ b/client/client_local_source_test.go @@ -433,3 +433,62 @@ func testParallelLocalBuilds(t *testing.T, sb integration.Sandbox) { err = eg.Wait() require.NoError(t, err) } + +// testLocalSourceManyPatterns checks that filesync patterns are not limited by +// the HTTP/2 header value count. Every pattern is sent as a separate gRPC +// metadata value and Go 1.27 net/http rejects requests with more than 500 of +// them by default. +func testLocalSourceManyPatterns(t *testing.T, sb integration.Sandbox) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + dir := integration.Tmpdir( + t, + fstest.CreateFile("foo", []byte("foo"), 0600), + fstest.CreateFile("ignored-1", []byte("ignored"), 0600), + ) + + const n = 1000 + excludes := make([]string, 0, n) + includes := make([]string, 0, n+1) + for i := 1; i <= n; i++ { + excludes = append(excludes, fmt.Sprintf("ignored-%d", i)) + includes = append(includes, fmt.Sprintf("included-%d", i)) + } + includes = append(includes, "foo") + + for _, tc := range []struct { + name string + opt llb.LocalOption + }{ + {name: "exclude", opt: llb.ExcludePatterns(excludes)}, + {name: "include", opt: llb.IncludePatterns(includes)}, + } { + t.Run(tc.name, func(t *testing.T) { + def, err := llb.Local("mylocal", tc.opt).Marshal(sb.Context()) + require.NoError(t, err) + + destDir := t.TempDir() + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: destDir, + }, + }, + LocalMounts: map[string]fsutil.FS{ + "mylocal": dir, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(destDir, "foo")) + require.NoError(t, err) + require.Equal(t, "foo", string(dt)) + + _, err = os.Stat(filepath.Join(destDir, "ignored-1")) + require.ErrorIs(t, err, os.ErrNotExist) + }) + } +} diff --git a/client/client_test.go b/client/client_test.go index f38f55a0506a..d48dbaff9ca8 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -192,6 +192,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){ testLocalSymlinkEscape, testMetadataOnlyLocal, testParallelLocalBuilds, + testLocalSourceManyPatterns, // client_mergeop_test.go testMergeOp, diff --git a/session/grpc.go b/session/grpc.go index aa5bd9405ee1..3a47888e6bb4 100644 --- a/session/grpc.go +++ b/session/grpc.go @@ -63,7 +63,7 @@ func serve(ctx context.Context, grpcServer *grpc.Server, conn net.Conn) { conn.Close() }() bklog.G(ctx).Debugf("serving grpc connection") - (&http2.Server{}).ServeConn(conn, &http2.ServeConnOpts{Handler: grpcServer}) //nolint:staticcheck // This existing HTTP/2 connection is already established. + newHTTP2Server(ctx).ServeConn(conn, &http2.ServeConnOpts{Handler: grpcServer}) //nolint:staticcheck // This existing HTTP/2 connection is already established. } func grpcClientConn(ctx context.Context, conn net.Conn, opts map[string][]string) (context.Context, *grpc.ClientConn, error) { diff --git a/session/http2server_go127.go b/session/http2server_go127.go new file mode 100644 index 000000000000..698eba74077e --- /dev/null +++ b/session/http2server_go127.go @@ -0,0 +1,29 @@ +//go:build go1.27 + +package session + +import ( + "context" + "math" + "net/http" + + "github.com/moby/buildkit/util/bklog" + "golang.org/x/net/http2" +) + +// newHTTP2Server returns the HTTP/2 server for session connections. +// +// Since Go 1.27, x/net/http2 is backed by net/http, which limits requests to +// http.DefaultMaxHeaderValueCount (500) header values. Session metadata, such +// as filesync include/exclude patterns, uses one value per item and can exceed +// that. The total header size is still bounded by http.DefaultMaxHeaderBytes. +func newHTTP2Server(ctx context.Context) *http2.Server { //nolint:staticcheck // ServeConn on an established connection needs x/net/http2. + h2 := &http2.Server{} //nolint:staticcheck // ServeConn on an established connection needs x/net/http2. + // h1 only carries config for h2 connections and never serves on its own. + h1 := &http.Server{MaxHeaderValueCount: math.MaxInt} //nolint:gosec // G112: session streams are long-lived, no read timeouts. + if err := http2.ConfigureServer(h1, h2); err != nil { //nolint:staticcheck // h1 is never served, Protocols does not apply. + bklog.G(ctx).WithError(err).Warn("failed to configure session http2 server") + return &http2.Server{} //nolint:staticcheck // ServeConn on an established connection needs x/net/http2. + } + return h2 +} diff --git a/session/http2server_legacy.go b/session/http2server_legacy.go new file mode 100644 index 000000000000..5dd3303ef212 --- /dev/null +++ b/session/http2server_legacy.go @@ -0,0 +1,15 @@ +//go:build !go1.27 + +package session + +import ( + "context" + + "golang.org/x/net/http2" +) + +// newHTTP2Server returns the HTTP/2 server for session connections. Before Go +// 1.27 x/net/http2 has no header value count limit, so no extra setup is needed. +func newHTTP2Server(_ context.Context) *http2.Server { //nolint:staticcheck // ServeConn on an established connection needs x/net/http2. + return &http2.Server{} //nolint:staticcheck // ServeConn on an established connection needs x/net/http2. +}