Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions client/client_local_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
1 change: 1 addition & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testLocalSymlinkEscape,
testMetadataOnlyLocal,
testParallelLocalBuilds,
testLocalSourceManyPatterns,

// client_mergeop_test.go
testMergeOp,
Expand Down
2 changes: 1 addition & 1 deletion session/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 29 additions & 0 deletions session/http2server_go127.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions session/http2server_legacy.go
Original file line number Diff line number Diff line change
@@ -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.
}
Loading