Repository: moby/buildkit

Issue:
allow space before heredoc name
in bash, this is allowed:

```sh
cat <<       EOF      > /tmp/afile       
  stuff
  more stuff
EOF
```
Dockerfile are quite pedantic about no space between `<<` and name `EOF`, otherwise it gives a very confusing syntax error on next line which leads user to think if heredocs are supported, then the follow up google search makes things more confusing... looks to be a simple case of a missing \s* in heredoc parser.

Discussion:
@thaJeztah: I know the original implementation (see https://github.com/moby/buildkit/pull/2132, https://github.com/moby/moby/issues/34423) was based on https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_07_04 - we should probably read up in that document to see how it defines handling of whitespace here.

If the spec doesn't define whitespace being supported, I'm somewhat leaning towards not allowing it (also in light of efforts in the Dockerfile syntax to provide linting errors for consistency), but we should definitely (where possible) improve errors to make it clear to the user what's wrong (i.e., if we decide based on the above to not allow whitespace, then at least a linter and/or error should indicate that it's because of whitespace).


cc @jedevc @tianon @kasperk81: yes if the error is improved to say "did you mean `<<EOF`" that would suffice too (in case allowing space there is not feasible)@tianon: That section of the spec doesn't spell out the expected whitespace behavior, but all the examples there are tight.  FWIW, BusyBox allows whitespace there too, so I'm sure there's another part of the spec (probably somewhere in some of the token parsing language) that talks about it more generally, and all the other redirection operators are well-known to allow whitespace around them, so it's reasonable to expect the heredoc would too (given it's _also_ a redirection operator).

Another place that allows spaces, and where I know the author has very meticulously read the relevant specs is `mvdan/sh`: https://github.com/mvdan/sh/blob/8a52daabb9b3c31255442161a2eb7a00b365c006/syntax/printer_test.go#L922 :heart:

(So TLDR, I think allowing spaces here is reasonable _and_ consistent with the relevant standards we're emulating, if the parser doesn't get too complex in doing so.)@jedevc: Yup, looking through how `bash` and `zsh` handle this as well, they both handle spaces before the `EOF` terminator, so seems very reasonable to handle it in the dockerfile parser as well.

Solution patch:
diff --git a/.github/workflows/.test.yml b/.github/workflows/.test.yml
index 586fc9b9c..8d872b8c6 100644
--- a/.github/workflows/.test.yml
+++ b/.github/workflows/.test.yml
@@ -31,7 +31,7 @@ on:
         type: string
 
 env:
-  GO_VERSION: "1.23"
+  GO_VERSION: "1.24"
   SETUP_BUILDX_VERSION: "edge"
   SETUP_BUILDKIT_IMAGE: "moby/buildkit:latest"
 
diff --git a/.github/workflows/buildkit.yml b/.github/workflows/buildkit.yml
index 9ab381fff..9694859d7 100644
--- a/.github/workflows/buildkit.yml
+++ b/.github/workflows/buildkit.yml
@@ -25,7 +25,7 @@ on:
       - 'frontend/dockerfile/docs/**'
 
 env:
-  GO_VERSION: "1.23"
+  GO_VERSION: "1.24"
   SETUP_BUILDX_VERSION: "edge"
   SETUP_BUILDKIT_IMAGE: "moby/buildkit:latest"
   SCOUT_VERSION: "1.13.0"
diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
index 559248d56..cac383fef 100644
--- a/.github/workflows/frontend.yml
+++ b/.github/workflows/frontend.yml
@@ -23,7 +23,7 @@ on:
       - 'frontend/dockerfile/docs/**'
 
 env:
-  GO_VERSION: "1.23"
+  GO_VERSION: "1.24"
   SETUP_BUILDX_VERSION: "edge"
   SETUP_BUILDKIT_TAG: "moby/buildkit:latest"
   SCOUT_VERSION: "1.13.0"
diff --git a/.github/workflows/test-os.yml b/.github/workflows/test-os.yml
index 74caf8c7d..f6859912f 100644
--- a/.github/workflows/test-os.yml
+++ b/.github/workflows/test-os.yml
@@ -23,7 +23,7 @@ on:
       - 'frontend/dockerfile/docs/**'
 
 env:
-  GO_VERSION: "1.23"
+  GO_VERSION: "1.24"
   SETUP_BUILDX_VERSION: "edge"
   SETUP_BUILDKIT_IMAGE: "moby/buildkit:latest"
   DESTDIR: "./bin"
diff --git a/Dockerfile b/Dockerfile
index 51bd9179f..d16305c8d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,7 +18,7 @@ ARG AZURITE_VERSION=3.33.0
 ARG GOTESTSUM_VERSION=v1.9.0
 ARG DELVE_VERSION=v1.23.1
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG ALPINE_VERSION=3.21
 ARG XX_VERSION=1.6.1
 ARG BUILDKIT_DEBUG
diff --git a/cache/blobs.go b/cache/blobs.go
index 87cf5f2fb..34a22bc52 100644
--- a/cache/blobs.go
+++ b/cache/blobs.go
@@ -70,7 +70,6 @@ func computeBlobChain(ctx context.Context, sr *immutableRef, createIfNeeded bool
 	switch sr.kind() {
 	case Merge:
 		for _, parent := range sr.mergeParents {
-			parent := parent
 			eg.Go(func() error {
 				return computeBlobChain(ctx, parent, createIfNeeded, comp, s, filter)
 			})
diff --git a/cache/contenthash/checksum.go b/cache/contenthash/checksum.go
index d2c6b0118..786c5f73e 100644
--- a/cache/contenthash/checksum.go
+++ b/cache/contenthash/checksum.go
@@ -569,8 +569,8 @@ func (cc *cacheContext) includedPaths(ctx context.Context, m *mount, p string, o
 		//
 		// When wildcards are enabled, this translation applies to the
 		// portion of 'p' before any wildcards.
-		if strings.HasPrefix(fn, resolvedPrefix) {
-			fn = origPrefix + strings.TrimPrefix(fn, resolvedPrefix)
+		if after, ok := strings.CutPrefix(fn, resolvedPrefix); ok {
+			fn = origPrefix + after
 		}
 
 		for len(parentDirHeaders) != 0 {
diff --git a/cache/contenthash/tarsum.go b/cache/contenthash/tarsum.go
index fb702962c..d1e91dc3c 100644
--- a/cache/contenthash/tarsum.go
+++ b/cache/contenthash/tarsum.go
@@ -49,8 +49,7 @@ func v1TarHeaderSelect(h *tar.Header) (orderedHeaders [][2]string) {
 	// Get extended attributes.
 	xAttrKeys := make([]string, 0, len(h.PAXRecords))
 	for k := range pax {
-		if strings.HasPrefix(k, "SCHILY.xattr.") {
-			k = strings.TrimPrefix(k, "SCHILY.xattr.")
+		if k, ok := strings.CutPrefix(k, "SCHILY.xattr."); ok {
 			if k == "security.capability" || !strings.HasPrefix(k, "security.") && !strings.HasPrefix(k, "system.") {
 				xAttrKeys = append(xAttrKeys, k)
 			}
diff --git a/cache/refs.go b/cache/refs.go
index eeee69dad..209da046f 100644
--- a/cache/refs.go
+++ b/cache/refs.go
@@ -1026,7 +1026,6 @@ func (sr *immutableRef) Extract(ctx context.Context, s session.Group) (rerr erro
 func (sr *immutableRef) withRemoteSnapshotLabelsStargzMode(ctx context.Context, s session.Group, f func()) error {
 	dhs := sr.descHandlers
 	for _, r := range sr.layerChain() {
-		r := r
 		info, err := r.cm.Snapshotter.Stat(ctx, r.getSnapshotID())
 		if err != nil && !cerrdefs.IsNotFound(err) {
 			return err
@@ -1069,7 +1068,6 @@ func (sr *immutableRef) prepareRemoteSnapshotsStargzMode(ctx context.Context, s
 	_, err := g.Do(ctx, sr.ID()+"-prepare-remote-snapshot", func(ctx context.Context) (_ *leaseutil.LeaseRef, rerr error) {
 		dhs := sr.descHandlers
 		for _, r := range sr.layerChain() {
-			r := r
 			snapshotID := r.getSnapshotID()
 			if _, err := r.cm.Snapshotter.Stat(ctx, snapshotID); err == nil {
 				continue
@@ -1635,16 +1633,16 @@ func readonlyOverlay(opt []string) []string {
 	out := make([]string, 0, len(opt))
 	upper := ""
 	for _, o := range opt {
-		if strings.HasPrefix(o, "upperdir=") {
-			upper = strings.TrimPrefix(o, "upperdir=")
+		if after, ok := strings.CutPrefix(o, "upperdir="); ok {
+			upper = after
 		} else if !strings.HasPrefix(o, "workdir=") {
 			out = append(out, o)
 		}
 	}
 	if upper != "" {
 		for i, o := range out {
-			if strings.HasPrefix(o, "lowerdir=") {
-				out[i] = "lowerdir=" + upper + ":" + strings.TrimPrefix(o, "lowerdir=")
+			if after, ok := strings.CutPrefix(o, "lowerdir="); ok {
+				out[i] = "lowerdir=" + upper + ":" + after
 			}
 		}
 	}
diff --git a/cache/remote.go b/cache/remote.go
index 2983eaaa5..c0e3cc6b4 100644
--- a/cache/remote.go
+++ b/cache/remote.go
@@ -121,7 +121,6 @@ func getAvailableBlobs(ctx context.Context, cs content.Store, chain *solver.Remo
 	}
 	var res []*solver.Remote
 	for _, desc := range descs {
-		desc := desc
 		if len(parents) == 0 { // bottommost ref
 			res = append(res, &solver.Remote{
 				Descriptors: []ocispecs.Descriptor{desc},
@@ -277,7 +276,6 @@ func (mp *lazyMultiProvider) Info(ctx context.Context, dgst digest.Digest) (cont
 func (mp *lazyMultiProvider) Unlazy(ctx context.Context) error {
 	eg, egctx := errgroup.WithContext(ctx)
 	for _, p := range mp.plist {
-		p := p
 		eg.Go(func() error {
 			return p.Unlazy(egctx)
 		})
diff --git a/client/client.go b/client/client.go
index 2ca45ca5f..ff3b6669b 100644
--- a/client/client.go
+++ b/client/client.go
@@ -151,7 +151,8 @@ func New(ctx context.Context, address string, opts ...ClientOpt) (*Client, error
 	gopts = append(gopts, grpc.WithStreamInterceptor(grpcerrors.StreamClientInterceptor))
 	gopts = append(gopts, customDialOptions...)
 
-	//nolint:staticcheck // ignore SA1019 NewClient has different behavior and needs to be tested
+	// ignore SA1019 NewClient has different behavior and needs to be tested
+	//nolint:staticcheck
 	conn, err := grpc.DialContext(ctx, address, gopts...)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to dial %q . make sure buildkitd is running", address)
diff --git a/client/llb/exec.go b/client/llb/exec.go
index edcd858a7..cd86f6f7c 100644
--- a/client/llb/exec.go
+++ b/client/llb/exec.go
@@ -170,7 +170,10 @@ func (e *ExecOp) Marshal(ctx context.Context, c *Constraints) (digest.Digest, []
 			} else if e.constraints.Platform != nil {
 				os = e.constraints.Platform.OS
 			}
-			env = env.SetDefault("PATH", system.DefaultPathEnv(os))
+			// don't set PATH on Windows. #5445
+			if os != "windows" {
+				env = env.SetDefault("PATH", system.DefaultPathEnv(os))
+			}
 		} else {
 			addCap(&e.constraints, pb.CapExecMetaSetsDefaultPath)
 		}
diff --git a/client/solve.go b/client/solve.go
index d9a177fd6..603211261 100644
--- a/client/solve.go
+++ b/client/solve.go
@@ -402,8 +402,7 @@ func prepareSyncedFiles(def *llb.Definition, localMounts map[string]fsutil.FS) (
 				return nil, errors.Wrap(err, "failed to parse llb proto op")
 			}
 			if src := op.GetSource(); src != nil {
-				if strings.HasPrefix(src.Identifier, "local://") {
-					name := strings.TrimPrefix(src.Identifier, "local://")
+				if name, ok := strings.CutPrefix(src.Identifier, "local://"); ok {
 					mount, ok := localMounts[name]
 					if !ok {
 						return nil, errors.Errorf("local directory %s not enabled", name)
diff --git a/cmd/buildkitd/config/config.go b/cmd/buildkitd/config/config.go
index a8be23428..156f3f3b5 100644
--- a/cmd/buildkitd/config/config.go
+++ b/cmd/buildkitd/config/config.go
@@ -104,7 +104,7 @@ type NetworkConfig struct {
 type OCIConfig struct {
 	Enabled          *bool             `toml:"enabled"`
 	Labels           map[string]string `toml:"labels"`
-	Platforms        []string          `toml:"platforms"`
+	Platforms        []string          `toml:"platforms,omitempty"`
 	Snapshotter      string            `toml:"snapshotter"`
 	Rootless         bool              `toml:"rootless"`
 	NoProcessSandbox bool              `toml:"noProcessSandbox"`
@@ -138,7 +138,7 @@ type ContainerdConfig struct {
 	Address   string            `toml:"address"`
 	Enabled   *bool             `toml:"enabled"`
 	Labels    map[string]string `toml:"labels"`
-	Platforms []string          `toml:"platforms"`
+	Platforms []string          `toml:"platforms,omitempty"`
 	Namespace string            `toml:"namespace"`
 	Runtime   ContainerdRuntime `toml:"runtime"`
 	GCConfig
diff --git a/cmd/buildkitd/main_oci_worker.go b/cmd/buildkitd/main_oci_worker.go
index f50a9f461..658a334fc 100644
--- a/cmd/buildkitd/main_oci_worker.go
+++ b/cmd/buildkitd/main_oci_worker.go
@@ -366,7 +366,8 @@ func snapshotterFactory(commonRoot string, cfg config.OCIConfig, sm *session.Man
 				grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)),
 				grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)),
 			}
-			//nolint:staticcheck // ignore SA1019 NewClient has different behavior and needs to be tested
+			// ignore SA1019 NewClient has different behavior and needs to be tested
+			//nolint:staticcheck
 			conn, err := grpc.Dial(dialer.DialAddress(address), gopts...)
 			if err != nil {
 				return nil, errors.Wrapf(err, "failed to dial %q", address)
@@ -487,8 +488,8 @@ func sourceWithSession(hosts docker.RegistryHosts, sm *session.Manager) sgzsourc
 		// to the snapshotter API. So, first, get all these IDs
 		var ids []string
 		for k := range labels {
-			if strings.HasPrefix(k, targetRefLabel+".") {
-				ids = append(ids, strings.TrimPrefix(k, targetRefLabel+"."))
+			if after, ok := strings.CutPrefix(k, targetRefLabel+"."); ok {
+				ids = append(ids, after)
 			}
 		}
 
diff --git a/contrib/cdisetup/nvidia/nvidia.go b/contrib/cdisetup/nvidia/nvidia.go
index 88b70b419..00063c338 100644
--- a/contrib/cdisetup/nvidia/nvidia.go
+++ b/contrib/cdisetup/nvidia/nvidia.go
@@ -284,8 +284,7 @@ func getOSID() (string, error) {
 	scanner := bufio.NewScanner(file)
 	for scanner.Scan() {
 		line := scanner.Text()
-		if strings.HasPrefix(line, "ID=") {
-			id := strings.TrimPrefix(line, "ID=")
+		if id, ok := strings.CutPrefix(line, "ID="); ok {
 			return strings.Trim(id, `"`), nil // Remove potential quotes
 		}
 	}
diff --git a/examples/buildkit0/buildkit.go b/examples/buildkit0/buildkit.go
index 2530d660e..1e300c2bf 100644
--- a/examples/buildkit0/buildkit.go
+++ b/examples/buildkit0/buildkit.go
@@ -33,7 +33,7 @@ func main() {
 }
 
 func goBuildBase() llb.State {
-	goAlpine := llb.Image("docker.io/library/golang:1.23-alpine")
+	goAlpine := llb.Image("docker.io/library/golang:1.24-alpine")
 	return goAlpine.
 		AddEnv("PATH", "/usr/local/go/bin:"+system.DefaultPathEnvUnix).
 		AddEnv("GOPATH", "/go").
diff --git a/examples/buildkit1/buildkit.go b/examples/buildkit1/buildkit.go
index f224da0db..44f13bc72 100644
--- a/examples/buildkit1/buildkit.go
+++ b/examples/buildkit1/buildkit.go
@@ -33,7 +33,7 @@ func main() {
 }
 
 func goBuildBase() llb.State {
-	goAlpine := llb.Image("docker.io/library/golang:1.23-alpine")
+	goAlpine := llb.Image("docker.io/library/golang:1.24-alpine")
 	return goAlpine.
 		AddEnv("PATH", "/usr/local/go/bin:"+system.DefaultPathEnvUnix).
 		AddEnv("GOPATH", "/go").
diff --git a/examples/buildkit2/buildkit.go b/examples/buildkit2/buildkit.go
index 7a9254f03..99746f4f0 100644
--- a/examples/buildkit2/buildkit.go
+++ b/examples/buildkit2/buildkit.go
@@ -33,7 +33,7 @@ func main() {
 }
 
 func goBuildBase() llb.State {
-	goAlpine := llb.Image("docker.io/library/golang:1.23-alpine")
+	goAlpine := llb.Image("docker.io/library/golang:1.24-alpine")
 	return goAlpine.
 		AddEnv("PATH", "/usr/local/go/bin:"+system.DefaultPathEnvUnix).
 		AddEnv("GOPATH", "/go").
diff --git a/examples/buildkit3/buildkit.go b/examples/buildkit3/buildkit.go
index 9e1341f08..6d123a84d 100644
--- a/examples/buildkit3/buildkit.go
+++ b/examples/buildkit3/buildkit.go
@@ -34,7 +34,7 @@ func main() {
 }
 
 func goBuildBase() llb.State {
-	goAlpine := llb.Image("docker.io/library/golang:1.23-alpine")
+	goAlpine := llb.Image("docker.io/library/golang:1.24-alpine")
 	return goAlpine.
 		AddEnv("PATH", "/usr/local/go/bin:"+system.DefaultPathEnvUnix).
 		AddEnv("GOPATH", "/go").
diff --git a/examples/buildkit4/buildkit.go b/examples/buildkit4/buildkit.go
index a1aa1a72d..53c16883a 100644
--- a/examples/buildkit4/buildkit.go
+++ b/examples/buildkit4/buildkit.go
@@ -37,7 +37,7 @@ func main() {
 }
 
 func goBuildBase() llb.State {
-	goAlpine := llb.Image("docker.io/library/golang:1.23-alpine")
+	goAlpine := llb.Image("docker.io/library/golang:1.24-alpine")
 	return goAlpine.
 		AddEnv("PATH", "/usr/local/go/bin:"+system.DefaultPathEnvUnix).
 		AddEnv("GOPATH", "/go").
diff --git a/examples/nested-llb/main.go b/examples/nested-llb/main.go
index c9c98c622..a08e68279 100644
--- a/examples/nested-llb/main.go
+++ b/examples/nested-llb/main.go
@@ -32,7 +32,7 @@ func main() {
 }
 
 func goBuildBase() llb.State {
-	goAlpine := llb.Image("docker.io/library/golang:1.23-alpine")
+	goAlpine := llb.Image("docker.io/library/golang:1.24-alpine")
 	return goAlpine.
 		AddEnv("PATH", "/usr/local/go/bin:"+system.DefaultPathEnvUnix).
 		AddEnv("GOPATH", "/go").
diff --git a/exporter/containerimage/export.go b/exporter/containerimage/export.go
index 58d2623b4..0dd8255e2 100644
--- a/exporter/containerimage/export.go
+++ b/exporter/containerimage/export.go
@@ -14,6 +14,7 @@ import (
 	"github.com/containerd/containerd/v2/core/images"
 	"github.com/containerd/containerd/v2/core/leases"
 	"github.com/containerd/containerd/v2/core/remotes/docker"
+	remoteserrors "github.com/containerd/containerd/v2/core/remotes/errors"
 	"github.com/containerd/containerd/v2/pkg/epoch"
 	"github.com/containerd/containerd/v2/pkg/labels"
 	"github.com/containerd/containerd/v2/pkg/rootfs"
@@ -333,7 +334,6 @@ func (e *imageExporterInstance) Export(ctx context.Context, src *exporter.Source
 					}
 					eg, ctx := errgroup.WithContext(ctx)
 					for _, ref := range refs {
-						ref := ref
 						eg.Go(func() error {
 							remotes, err := ref.GetRemotes(ctx, false, e.opts.RefCfg, false, session.NewGroup(sessionID))
 							if err != nil {
@@ -356,6 +356,13 @@ func (e *imageExporterInstance) Export(ctx context.Context, src *exporter.Source
 			if e.push {
 				err = e.pushImage(ctx, src, sessionID, targetName, desc.Digest)
 				if err != nil {
+					var statusErr remoteserrors.ErrUnexpectedStatus
+					if errors.As(err, &statusErr) {
+						var dErr docker.Errors
+						if err1 := json.Unmarshal(statusErr.Body, &dErr); err1 == nil && len(dErr) > 0 {
+							err = &formattedDockerError{dErr: dErr}
+						}
+					}
 					return nil, nil, errors.Wrapf(err, "failed to push %v", targetName)
 				}
 			}
@@ -543,3 +550,36 @@ func (d *descriptorReference) Descriptor() ocispecs.Descriptor {
 func (d *descriptorReference) Release() error {
 	return d.release(context.TODO())
 }
+
+type formattedDockerError struct {
+	dErr docker.Errors
+}
+
+func (e *formattedDockerError) Error() string {
+	format := func(err error) string {
+		out := err.Error()
+		var dErr docker.Error
+		if errors.As(err, &dErr) {
+			if v, ok := dErr.Detail.(string); ok && v != "" {
+				out += " - " + v
+			}
+		}
+		return out
+	}
+	switch len(e.dErr) {
+	case 0:
+		return "<nil>"
+	case 1:
+		return format(e.dErr[0])
+	default:
+		msg := "errors:\n"
+		for _, err := range e.dErr {
+			msg += format(err) + "\n"
+		}
+		return msg
+	}
+}
+
+func (e *formattedDockerError) Unwrap() error {
+	return e.dErr
+}
diff --git a/exporter/containerimage/writer.go b/exporter/containerimage/writer.go
index d3bbd3874..63f390a85 100644
--- a/exporter/containerimage/writer.go
+++ b/exporter/containerimage/writer.go
@@ -683,7 +683,10 @@ func defaultImageConfig() ([]byte, error) {
 	img.Variant = pl.Variant
 	img.RootFS.Type = "layers"
 	img.Config.WorkingDir = "/"
-	img.Config.Env = []string{"PATH=" + system.DefaultPathEnv(pl.OS)}
+	// don't set default PATH on Windows. #5445
+	if pl.OS != "windows" {
+		img.Config.Env = []string{"PATH=" + system.DefaultPathEnv(pl.OS)}
+	}
 	dt, err := json.Marshal(img)
 	return dt, errors.Wrap(err, "failed to create empty image config")
 }
diff --git a/exporter/oci/export.go b/exporter/oci/export.go
index c1bee4e4c..c6650b0a2 100644
--- a/exporter/oci/export.go
+++ b/exporter/oci/export.go
@@ -235,7 +235,6 @@ func (e *imageExporterInstance) Export(ctx context.Context, src *exporter.Source
 	eg, egCtx := errgroup.WithContext(ctx)
 	mprovider := contentutil.NewMultiProvider(e.opt.ImageWriter.ContentStore())
 	for _, ref := range refs {
-		ref := ref
 		eg.Go(func() error {
 			remotes, err := ref.GetRemotes(egCtx, false, e.opts.RefCfg, false, session.NewGroup(sessionID))
 			if err != nil {
diff --git a/exporter/verifier/opts.go b/exporter/verifier/opts.go
index 1e580c57f..b9f13b494 100644
--- a/exporter/verifier/opts.go
+++ b/exporter/verifier/opts.go
@@ -32,8 +32,8 @@ func CaptureFrontendOpts[T comparable](m map[string]string, res *result.Result[T
 
 	req.Labels = map[string]string{}
 	for k, v := range m {
-		if strings.HasPrefix(k, labelsPrefix) {
-			req.Labels[strings.TrimPrefix(k, labelsPrefix)] = v
+		if after, ok := strings.CutPrefix(k, labelsPrefix); ok {
+			req.Labels[after] = v
 		}
 	}
 	req.Request = m[keyRequestID]
diff --git a/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile b/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile
index f5e86234f..9cb911f4f 100644
--- a/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile
+++ b/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile-upstream:master
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG ALPINE_VERSION=3.21
 ARG XX_VERSION=1.6.1
 
diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go
index 043ec22ed..71ef28a3a 100644
--- a/frontend/dockerfile/dockerfile2llb/convert.go
+++ b/frontend/dockerfile/dockerfile2llb/convert.go
@@ -679,7 +679,10 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
 			if d.platform != nil {
 				osName = d.platform.OS
 			}
-			d.image.Config.Env = append(d.image.Config.Env, "PATH="+system.DefaultPathEnv(osName))
+			// except for Windows, leave that to the OS. #5445
+			if osName != "windows" {
+				d.image.Config.Env = append(d.image.Config.Env, "PATH="+system.DefaultPathEnv(osName))
+			}
 		}
 
 		// initialize base metadata from image conf
diff --git a/frontend/dockerfile/dockerfile2llb/image.go b/frontend/dockerfile/dockerfile2llb/image.go
index 3b62aa1d6..ee1c138d2 100644
--- a/frontend/dockerfile/dockerfile2llb/image.go
+++ b/frontend/dockerfile/dockerfile2llb/image.go
@@ -37,6 +37,9 @@ func emptyImage(platform ocispecs.Platform) dockerspec.DockerOCIImage {
 	img.Variant = platform.Variant
 	img.RootFS.Type = "layers"
 	img.Config.WorkingDir = "/"
-	img.Config.Env = []string{"PATH=" + system.DefaultPathEnv(platform.OS)}
+	// don't set path for Windows, leave it to the OS. #5445
+	if platform.OS != "windows" {
+		img.Config.Env = []string{"PATH=" + system.DefaultPathEnv(platform.OS)}
+	}
 	return img
 }
diff --git a/frontend/dockerfile/instructions/parse.go b/frontend/dockerfile/instructions/parse.go
index 01a7c7ecd..b44a6a384 100644
--- a/frontend/dockerfile/instructions/parse.go
+++ b/frontend/dockerfile/instructions/parse.go
@@ -831,8 +831,8 @@ func getComment(comments []string, name string) string {
 		return ""
 	}
 	for _, line := range comments {
-		if strings.HasPrefix(line, name+" ") {
-			return strings.TrimPrefix(line, name+" ")
+		if after, ok := strings.CutPrefix(line, name+" "); ok {
+			return after
 		}
 	}
 	return ""
diff --git a/frontend/dockerfile/parser/parser.go b/frontend/dockerfile/parser/parser.go
index caabe4dc2..2f0da0222 100644
--- a/frontend/dockerfile/parser/parser.go
+++ b/frontend/dockerfile/parser/parser.go
@@ -114,7 +114,7 @@ type Heredoc struct {
 var (
 	dispatch      map[string]func(string, *directives) (*Node, map[string]bool, error)
 	reWhitespace  = regexp.MustCompile(`[\t\v\f\r ]+`)
-	reHeredoc     = regexp.MustCompile(`^(\d*)<<(-?)([^<]*)$`)
+	reHeredoc     = regexp.MustCompile(`^(\d*)<<(-?)\s*([^<]*)$`)
 	reLeadingTabs = regexp.MustCompile(`(?m)^\t+`)
 )
 
diff --git a/frontend/dockerfile/shell/lex.go b/frontend/dockerfile/shell/lex.go
index 18da29334..b8a307a06 100644
--- a/frontend/dockerfile/shell/lex.go
+++ b/frontend/dockerfile/shell/lex.go
@@ -177,6 +177,7 @@ func (sw *shellWord) processStopOn(stopChar rune, rawEscapes bool) (string, []st
 	// no need to initialize all the time
 	var charFuncMapping = map[rune]func() (string, error){
 		'$': sw.processDollar,
+		'<': sw.processPossibleHeredoc,
 	}
 	if !sw.SkipProcessQuotes {
 		charFuncMapping['\''] = sw.processSingleQuote
@@ -512,6 +513,25 @@ func (sw *shellWord) processName() string {
 	return name.String()
 }
 
+func (sw *shellWord) processPossibleHeredoc() (string, error) {
+	sw.scanner.Next()
+	if sw.scanner.Peek() != '<' {
+		return "<", nil // not a heredoc
+	}
+	sw.scanner.Next()
+
+	// heredoc might have whitespace between << and word terminator
+	var space bytes.Buffer
+	nextCh := sw.scanner.Peek()
+	for isWhitespace(nextCh) {
+		space.WriteRune(nextCh)
+		sw.scanner.Next()
+		nextCh = sw.scanner.Peek()
+	}
+	result := "<<" + space.String()
+	return result, nil
+}
+
 // isSpecialParam checks if the provided character is a special parameters,
 // as defined in http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_05_02
 func isSpecialParam(char rune) bool {
@@ -677,3 +697,11 @@ func trimSuffix(pattern, word string, greedy bool) (string, error) {
 	}
 	return reverseString(str), nil
 }
+
+func isWhitespace(r rune) bool {
+	switch r {
+	case '\t', '\r', ' ':
+		return true
+	}
+	return false
+}
diff --git a/frontend/dockerui/attr.go b/frontend/dockerui/attr.go
index 259100b46..e06d70e20 100644
--- a/frontend/dockerui/attr.go
+++ b/frontend/dockerui/attr.go
@@ -128,8 +128,8 @@ func parseSourceDateEpoch(v string) (*time.Time, error) {
 func parseLocalSessionIDs(opt map[string]string) map[string]string {
 	m := map[string]string{}
 	for k, v := range opt {
-		if strings.HasPrefix(k, localSessionIDPrefix) {
-			m[strings.TrimPrefix(k, localSessionIDPrefix)] = v
+		if after, ok := strings.CutPrefix(k, localSessionIDPrefix); ok {
+			m[after] = v
 		}
 	}
 	return m
@@ -138,8 +138,8 @@ func parseLocalSessionIDs(opt map[string]string) map[string]string {
 func filter(opt map[string]string, key string) map[string]string {
 	m := map[string]string{}
 	for k, v := range opt {
-		if strings.HasPrefix(k, key) {
-			m[strings.TrimPrefix(k, key)] = v
+		if after, ok := strings.CutPrefix(k, key); ok {
+			m[after] = v
 		}
 	}
 	return m
diff --git a/frontend/dockerui/build.go b/frontend/dockerui/build.go
index 6ced71d67..233329b61 100644
--- a/frontend/dockerui/build.go
+++ b/frontend/dockerui/build.go
@@ -22,7 +22,6 @@ func (bc *Client) Build(ctx context.Context, fn BuildFunc) (*ResultBuilder, erro
 
 	targets := make([]*ocispecs.Platform, 0, len(bc.TargetPlatforms))
 	for _, p := range bc.TargetPlatforms {
-		p := p
 		targets = append(targets, &p)
 	}
 	if len(targets) == 0 {
@@ -106,7 +105,6 @@ func (rb *ResultBuilder) Finalize() (*client.Result, error) {
 func (rb *ResultBuilder) EachPlatform(ctx context.Context, fn func(ctx context.Context, id string, p ocispecs.Platform) error) error {
 	eg, ctx := errgroup.WithContext(ctx)
 	for _, p := range rb.expPlatforms.Platforms {
-		p := p
 		eg.Go(func() error {
 			return fn(ctx, p.ID, p.Platform)
 		})
diff --git a/frontend/gateway/container/container.go b/frontend/gateway/container/container.go
index 59b447b00..df34d9157 100644
--- a/frontend/gateway/container/container.go
+++ b/frontend/gateway/container/container.go
@@ -105,13 +105,11 @@ func NewContainer(ctx context.Context, cm cache.Manager, exec executor.Executor,
 	ctr.mounts = p.Mounts
 
 	for _, o := range p.OutputRefs {
-		o := o
 		ctr.cleanup = append(ctr.cleanup, func() error {
 			return o.Ref.Release(context.TODO())
 		})
 	}
 	for _, active := range p.Actives {
-		active := active
 		ctr.cleanup = append(ctr.cleanup, func() error {
 			return active.Ref.Release(context.TODO())
 		})
diff --git a/frontend/gateway/gateway.go b/frontend/gateway/gateway.go
index a42fc617e..90ea7dfa5 100644
--- a/frontend/gateway/gateway.go
+++ b/frontend/gateway/gateway.go
@@ -91,8 +91,8 @@ type gatewayFrontend struct {
 func filterPrefix(opts map[string]string, pfx string) map[string]string {
 	m := map[string]string{}
 	for k, v := range opts {
-		if strings.HasPrefix(k, pfx) {
-			m[strings.TrimPrefix(k, pfx)] = v
+		if after, ok := strings.CutPrefix(k, pfx); ok {
+			m[after] = v
 		}
 	}
 	return m
diff --git a/hack/dockerfiles/archutil.Dockerfile b/hack/dockerfiles/archutil.Dockerfile
index 64afaa245..3adc4feab 100644
--- a/hack/dockerfiles/archutil.Dockerfile
+++ b/hack/dockerfiles/archutil.Dockerfile
@@ -1,7 +1,7 @@
 # syntax=docker/dockerfile-upstream:master
 # check=error=true
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG ALPINE_VERSION=3.21
 ARG DEBIAN_VERSION=trixie
 
diff --git a/hack/dockerfiles/docs-dockerfile.Dockerfile b/hack/dockerfiles/docs-dockerfile.Dockerfile
index 2d3d4bfda..18c9563d9 100644
--- a/hack/dockerfiles/docs-dockerfile.Dockerfile
+++ b/hack/dockerfiles/docs-dockerfile.Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile:1
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG ALPINE_VERSION=3.21
 
 FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golatest
diff --git a/hack/dockerfiles/docs.Dockerfile b/hack/dockerfiles/docs.Dockerfile
index 338c3e115..8fd32f75c 100644
--- a/hack/dockerfiles/docs.Dockerfile
+++ b/hack/dockerfiles/docs.Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile:1
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG ALPINE_VERSION=3.21
 
 FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golatest
diff --git a/hack/dockerfiles/generated-files.Dockerfile b/hack/dockerfiles/generated-files.Dockerfile
index 3a6b12788..547b0910b 100644
--- a/hack/dockerfiles/generated-files.Dockerfile
+++ b/hack/dockerfiles/generated-files.Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile-upstream:master
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG DEBIAN_VERSION=bookworm
 ARG PROTOC_VERSION=3.11.4
 ARG PROTOC_GOOGLEAPIS_VERSION=2af421884dd468d565137215c946ebe4e245ae26
diff --git a/hack/dockerfiles/govulncheck.Dockerfile b/hack/dockerfiles/govulncheck.Dockerfile
index 42dfd053e..fc837f866 100644
--- a/hack/dockerfiles/govulncheck.Dockerfile
+++ b/hack/dockerfiles/govulncheck.Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile:1
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG GOVULNCHECK_VERSION=v1.1.3
 ARG FORMAT="text"
 
diff --git a/hack/dockerfiles/lint.Dockerfile b/hack/dockerfiles/lint.Dockerfile
index fd56140fe..6d58cf4d7 100644
--- a/hack/dockerfiles/lint.Dockerfile
+++ b/hack/dockerfiles/lint.Dockerfile
@@ -1,15 +1,14 @@
 # syntax=docker/dockerfile-upstream:master
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG ALPINE_VERSION=3.21
 ARG XX_VERSION=1.6.1
 ARG PROTOLINT_VERSION=0.50.5
 ARG GOLANGCI_LINT_VERSION=v2.1.5
 ARG GOLANGCI_FROM_SOURCE=false
-# v0.31 requires go1.24
-ARG GOPLS_VERSION=v0.30.0
-# GOPLS_ANALYZERS defines gopls analyzers to be run. disabled by default: deprecated unusedvariable simplifyrange
-ARG GOPLS_ANALYZERS="embeddirective fillreturns hostport infertypeargs modernize nonewvars noresultvalues simplifycompositelit simplifyslice unusedparams yield"
+ARG GOPLS_VERSION=v0.33.0
+# GOPLS_ANALYZERS defines gopls analyzers to be run. disabled by default: deprecated simplifyrange unusedfunc unusedvariable
+ARG GOPLS_ANALYZERS="embeddirective fillreturns infertypeargs maprange modernize nonewvars noresultvalues simplifycompositelit simplifyslice unusedparams yield"
 
 FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang-base
 FROM --platform=$BUILDPLATFORM yoheimuta/protolint:${PROTOLINT_VERSION} AS protolint-base
diff --git a/hack/dockerfiles/vendor.Dockerfile b/hack/dockerfiles/vendor.Dockerfile
index 00ac4ac0f..1369eb573 100644
--- a/hack/dockerfiles/vendor.Dockerfile
+++ b/hack/dockerfiles/vendor.Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile-upstream:master
 
-ARG GO_VERSION=1.23
+ARG GO_VERSION=1.24
 ARG ALPINE_VERSION=3.21
 ARG MODOUTDATED_VERSION=v0.9.0
 
diff --git a/session/filesync/filesync.go b/session/filesync/filesync.go
index 2a402e445..230493f16 100644
--- a/session/filesync/filesync.go
+++ b/session/filesync/filesync.go
@@ -336,8 +336,8 @@ func (sp *SyncTarget) DiffCopy(stream FileSend_DiffCopyServer) (err error) {
 	opts, _ := metadata.FromIncomingContext(stream.Context()) // if no metadata continue with empty object
 	md := map[string]string{}
 	for k, v := range opts {
-		if strings.HasPrefix(k, keyExporterMetaPrefix) {
-			md[strings.TrimPrefix(k, keyExporterMetaPrefix)] = strings.Join(v, ",")
+		if after, ok0 := strings.CutPrefix(k, keyExporterMetaPrefix); ok0 {
+			md[after] = strings.Join(v, ",")
 		}
 	}
 	wc, err := f(md)
diff --git a/snapshot/diffapply_linux.go b/snapshot/diffapply_linux.go
index 3bb4216f4..6ddc06c66 100644
--- a/snapshot/diffapply_linux.go
+++ b/snapshot/diffapply_linux.go
@@ -166,10 +166,10 @@ func applierFor(dest Mountable, tryCrossSnapshotLink, userxattr bool) (_ *applie
 
 	if overlay.IsOverlayMountType(mnt) {
 		for _, opt := range mnt.Options {
-			if strings.HasPrefix(opt, "upperdir=") {
-				a.root = strings.TrimPrefix(opt, "upperdir=")
-			} else if strings.HasPrefix(opt, "lowerdir=") {
-				a.lowerdirs = strings.Split(strings.TrimPrefix(opt, "lowerdir="), ":")
+			if after, ok := strings.CutPrefix(opt, "upperdir="); ok {
+				a.root = after
+			} else if after, ok := strings.CutPrefix(opt, "lowerdir="); ok {
+				a.lowerdirs = strings.Split(after, ":")
 			}
 		}
 		if a.root == "" {
diff --git a/solver/combinedcache.go b/solver/combinedcache.go
index db99aaf0d..60d9ae47c 100644
--- a/solver/combinedcache.go
+++ b/solver/combinedcache.go
@@ -131,7 +131,6 @@ func (cm *combinedCacheManager) Records(ctx context.Context, ck *CacheKey) ([]*C
 
 	eg, _ := errgroup.WithContext(context.TODO())
 	for _, c := range cms {
-		c := c
 		eg.Go(func() error {
 			recs, err := c.Records(ctx, ck)
 			if err != nil {
diff --git a/solver/llbsolver/ops/exec.go b/solver/llbsolver/ops/exec.go
index 9bf9a3551..40e32d495 100644
--- a/solver/llbsolver/ops/exec.go
+++ b/solver/llbsolver/ops/exec.go
@@ -462,7 +462,10 @@ func (e *ExecOp) Exec(ctx context.Context, g session.Group, inputs []solver.Resu
 	if e.platform != nil {
 		currentOS = e.platform.OS
 	}
-	meta.Env = addDefaultEnvvar(meta.Env, "PATH", utilsystem.DefaultPathEnv(currentOS))
+	// don't set PATH for Windows. #5445
+	if currentOS != "windows" {
+		meta.Env = addDefaultEnvvar(meta.Env, "PATH", utilsystem.DefaultPathEnv(currentOS))
+	}
 
 	secretEnv, err := e.loadSecretEnv(ctx, g)
 	if err != nil {
diff --git a/util/overlay/overlay_linux.go b/util/overlay/overlay_linux.go
index 223c6655d..076e83479 100644
--- a/util/overlay/overlay_linux.go
+++ b/util/overlay/overlay_linux.go
@@ -86,10 +86,10 @@ func GetOverlayLayers(m mount.Mount) ([]string, error) {
 	var uFound bool
 	var l []string // l[0] = bottommost
 	for _, o := range m.Options {
-		if strings.HasPrefix(o, "upperdir=") {
-			u, uFound = strings.TrimPrefix(o, "upperdir="), true
-		} else if strings.HasPrefix(o, "lowerdir=") {
-			l = strings.Split(strings.TrimPrefix(o, "lowerdir="), ":")
+		if after, ok := strings.CutPrefix(o, "upperdir="); ok {
+			u, uFound = after, true
+		} else if after, ok := strings.CutPrefix(o, "lowerdir="); ok {
+			l = strings.Split(after, ":")
 			for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 {
 				l[i], l[j] = l[j], l[i] // make l[0] = bottommost
 			}
diff --git a/util/winlayers/applier.go b/util/winlayers/applier.go
index 6eebbadef..7bfc3ca7f 100644
--- a/util/winlayers/applier.go
+++ b/util/winlayers/applier.go
@@ -76,8 +76,8 @@ func (s *winApplier) Apply(ctx context.Context, desc ocispecs.Descriptor, mounts
 		}
 
 		rc2, discard := filter(rc, func(hdr *tar.Header) bool {
-			if strings.HasPrefix(hdr.Name, "Files/") {
-				hdr.Name = strings.TrimPrefix(hdr.Name, "Files/")
+			if after, ok := strings.CutPrefix(hdr.Name, "Files/"); ok {
+				hdr.Name = after
 				hdr.Linkname = strings.TrimPrefix(hdr.Linkname, "Files/")
 				// TODO: could convert the windows PAX headers to xattr here to reuse
 				// the original ones in diff for parent directories and file modifications
diff --git a/worker/base/worker.go b/worker/base/worker.go
index 0329c2888..1922b9dc6 100644
--- a/worker/base/worker.go
+++ b/worker/base/worker.go
@@ -495,7 +495,6 @@ func (w *Worker) FromRemote(ctx context.Context, remote *solver.Remote) (ref cac
 	if len(remote.Descriptors) > 0 {
 		var eg errgroup.Group
 		for _, desc := range remote.Descriptors {
-			desc := desc
 			eg.Go(func() error {
 				if _, err := remote.Provider.Info(ctx, desc.Digest); err != nil {
 					return err



Test patch:
diff --git a/cache/contenthash/path_test.go b/cache/contenthash/path_test.go
index a6d895a5f..3af813e47 100644
--- a/cache/contenthash/path_test.go
+++ b/cache/contenthash/path_test.go
@@ -104,7 +104,6 @@ func TestRootPathSymlinks(t *testing.T) {
 		{"link3/link1/../link1/final", true, "/target"},             // link3/target2
 		{"link3/link1/../link1/final", false, "/link1/final"},       // link3/target2
 	} {
-		test := test // capture range variable
 		t.Run(fmt.Sprintf("resolve(%q,followTrailing=%v)", test.path, test.followTrailing), func(t *testing.T) {
 			t.Parallel()
 
diff --git a/cache/manager_test.go b/cache/manager_test.go
index fd03bab81..7d86fff73 100644
--- a/cache/manager_test.go
+++ b/cache/manager_test.go
@@ -1731,7 +1731,6 @@ func TestGetRemotes(t *testing.T) {
 	for _, ir := range refs {
 		ir := ir.(*immutableRef)
 		for _, compressionType := range []compression.Type{compression.Uncompressed, compression.Gzip, compression.EStargz, compression.Zstd} {
-			compressionType := compressionType
 			refCfg := config.RefConfig{Compression: compression.New(compressionType).SetForce(true)}
 			eg.Go(func() error {
 				remotes, err := ir.GetRemotes(egctx, true, refCfg, false, nil)
@@ -1820,7 +1819,6 @@ func TestGetRemotes(t *testing.T) {
 		variantsMapMu.Unlock()
 		require.True(t, ok, ir.ID())
 		for _, compressionType := range []compression.Type{compression.Uncompressed, compression.Gzip, compression.EStargz, compression.Zstd} {
-			compressionType := compressionType
 			refCfg := config.RefConfig{Compression: compression.New(compressionType)}
 			eg.Go(func() error {
 				remotes, err := ir.GetRemotes(egctx, false, refCfg, true, nil)
@@ -1877,7 +1875,6 @@ func checkVariantsCoverage(ctx context.Context, t *testing.T, variants idxToVari
 	// check the lowers as well
 	eg, egctx := errgroup.WithContext(ctx)
 	for _, lowers := range got {
-		lowers := lowers
 		eg.Go(func() error {
 			checkVariantsCoverage(egctx, t, variants, idx-1, lowers, nil) // expect all compression variants
 			return nil
diff --git a/cache/remotecache/gha/gha_test.go b/cache/remotecache/gha/gha_test.go
index fbae1d627..9434f3dd5 100644
--- a/cache/remotecache/gha/gha_test.go
+++ b/cache/remotecache/gha/gha_test.go
@@ -82,10 +82,10 @@ func testBasicGhaCacheImportExportExtraTimeout(t *testing.T, sb integration.Sand
 
 	scope := "buildkit-" + t.Name()
 	if ref := os.Getenv("GITHUB_REF"); ref != "" {
-		if strings.HasPrefix(ref, "refs/heads/") {
-			scope += "-" + strings.TrimPrefix(ref, "refs/heads/")
-		} else if strings.HasPrefix(ref, "refs/tags/") {
-			scope += "-" + strings.TrimPrefix(ref, "refs/tags/")
+		if after, ok := strings.CutPrefix(ref, "refs/heads/"); ok {
+			scope += "-" + after
+		} else if after, ok := strings.CutPrefix(ref, "refs/tags/"); ok {
+			scope += "-" + after
 		} else if strings.HasPrefix(ref, "refs/pull/") {
 			scope += "-pr" + strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(ref, "/head"), "/merge"), "refs/pull/")
 		}
diff --git a/client/build_test.go b/client/build_test.go
index 2198dce55..72055706c 100644
--- a/client/build_test.go
+++ b/client/build_test.go
@@ -1469,7 +1469,6 @@ func testClientGatewayExecError(t *testing.T, sb integration.Sandbox) {
 		}}
 
 		for _, tt := range tests {
-			tt := tt
 			t.Run(tt.Name, func(t *testing.T) {
 				def, err := tt.State.Marshal(ctx)
 				require.NoError(t, err)
@@ -1722,7 +1721,6 @@ func testClientGatewayExecFileActionError(t *testing.T, sb integration.Sandbox)
 		}}
 
 		for _, tt := range tests {
-			tt := tt
 			t.Run(tt.Name, func(t *testing.T) {
 				def, err := tt.State.Marshal(ctx)
 				require.NoError(t, err)
diff --git a/client/client_test.go b/client/client_test.go
index f95055c69..89ae22bad 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -1306,15 +1306,12 @@ func testFrontendImageNaming(t *testing.T, sb integration.Sandbox) {
 
 	// A caller provided name takes precedence over one returned by the frontend. Iterate over both options.
 	for _, winner := range []string{"frontend", "caller"} {
-		winner := winner // capture loop variable.
-
 		// The double layer of `t.Run` here is required so
 		// that the inner-most tests (with the actual
 		// functionality) have definitely completed before the
 		// sandbox and registry cleanups (defered above) are run.
 		t.Run(winner, func(t *testing.T) {
 			for _, exp := range []string{ExporterOCI, ExporterDocker, ExporterImage} {
-				exp := exp // capture loop variable.
 				t.Run(exp, func(t *testing.T) {
 					destDir := t.TempDir()
 
@@ -4796,7 +4793,6 @@ func testPullZstdImage(t *testing.T, sb integration.Sandbox) {
 	integration.SkipOnPlatform(t, "windows")
 	workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
 	for _, ociMediaTypes := range []bool{true, false} {
-		ociMediaTypes := ociMediaTypes
 		t.Run(t.Name()+fmt.Sprintf("/ociMediaTypes=%t", ociMediaTypes), func(t *testing.T) {
 			c, err := New(sb.Context(), sb.Address())
 			require.NoError(t, err)
@@ -5888,7 +5884,6 @@ func testCacheExportIgnoreError(t *testing.T, sb integration.Sandbox) {
 	for _, ignoreError := range ignoreErrorValues {
 		ignoreErrStr := strconv.FormatBool(ignoreError)
 		for n, test := range tests {
-			n := n
 			require.Equal(t, 1, len(test.Exports))
 			require.Equal(t, 1, len(test.CacheExports))
 			require.NotEmpty(t, test.CacheExports[0].Attrs)
@@ -6464,9 +6459,7 @@ func testRegistryEmptyCacheExport(t *testing.T, sb integration.Sandbox) {
 	)
 
 	for _, ociMediaTypes := range []bool{true, false} {
-		ociMediaTypes := ociMediaTypes
 		for _, imageManifest := range []bool{true, false} {
-			imageManifest := imageManifest
 			if imageManifest && !ociMediaTypes {
 				// invalid configuration for remote cache
 				continue
@@ -11296,7 +11289,6 @@ func testSourcePolicy(t *testing.T, sb integration.Sandbox) {
 		},
 	}
 	for i, tc := range testCases {
-		tc := tc
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
 			_, err = c.Build(sb.Context(), SolveOpt{SourcePolicy: tc.srcPol}, "", frontend, nil)
 			if tc.expectedErr == "" {
diff --git a/client/llb/definition_test.go b/client/llb/definition_test.go
index 4e8bed333..ef71bd194 100644
--- a/client/llb/definition_test.go
+++ b/client/llb/definition_test.go
@@ -27,7 +27,6 @@ func TestDefinitionEquivalence(t *testing.T) {
 		{"platform constraint", Image("ref", LinuxArm64)},
 		{"mount", Image("busybox").Run(Shlex(`sh -c "echo foo > /out/foo"`)).AddMount("/out", Scratch())},
 	} {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
@@ -144,7 +143,6 @@ func TestDefinitionNil(t *testing.T) {
 func testParallelWalk(ctx context.Context, out Output) error {
 	eg, egCtx := errgroup.WithContext(ctx)
 	for _, o := range out.Vertex(ctx, nil).Inputs() {
-		o := o
 		eg.Go(func() error {
 			return testParallelWalk(egCtx, o)
 		})
diff --git a/client/llb/llbtest/platform_test.go b/client/llb/llbtest/platform_test.go
index 8fbae50b9..5b2366e5d 100644
--- a/client/llb/llbtest/platform_test.go
+++ b/client/llb/llbtest/platform_test.go
@@ -229,8 +229,8 @@ func getenv(e solver.Edge, k string) (string, bool) {
 	env := toOp(e).GetExec().Meta.Env
 	k = k + "="
 	for _, e := range env {
-		if strings.HasPrefix(e, k) {
-			return strings.TrimPrefix(e, k), true
+		if after, ok := strings.CutPrefix(e, k); ok {
+			return after, true
 		}
 	}
 	return "", false
diff --git a/cmd/buildctl/buildctl_test.go b/cmd/buildctl/buildctl_test.go
index fce72a981..c3c6779df 100644
--- a/cmd/buildctl/buildctl_test.go
+++ b/cmd/buildctl/buildctl_test.go
@@ -115,7 +115,6 @@ func TestWriteMetadataFile(t *testing.T) {
 	}
 
 	for _, tt := range cases {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			fname := path.Join(tmpdir, "metadata_"+tt.name)
 			require.NoError(t, writeMetadataFile(fname, tt.exporterResponse))
diff --git a/executor/oci/resolvconf_test.go b/executor/oci/resolvconf_test.go
index 5de1f12c7..a88606716 100644
--- a/executor/oci/resolvconf_test.go
+++ b/executor/oci/resolvconf_test.go
@@ -103,7 +103,6 @@ func TestResolvConf(t *testing.T) {
 	}
 
 	for _, tt := range cases {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			ctx := context.Background()
 			tempDir := t.TempDir()
diff --git a/frontend/attestations/parse.go b/frontend/attestations/parse.go
index a3d5ff5db..2b2ed2499 100644
--- a/frontend/attestations/parse.go
+++ b/frontend/attestations/parse.go
@@ -43,12 +43,12 @@ func Validate(values map[string]map[string]string) (map[string]map[string]string
 func Parse(values map[string]string) (map[string]map[string]string, error) {
 	attests := make(map[string]string)
 	for k, v := range values {
-		if strings.HasPrefix(k, "attest:") {
-			attests[strings.ToLower(strings.TrimPrefix(k, "attest:"))] = v
+		if after, ok := strings.CutPrefix(k, "attest:"); ok {
+			attests[strings.ToLower(after)] = v
 			continue
 		}
-		if strings.HasPrefix(k, "build-arg:BUILDKIT_ATTEST_") {
-			attests[strings.ToLower(strings.TrimPrefix(k, "build-arg:BUILDKIT_ATTEST_"))] = v
+		if after, ok := strings.CutPrefix(k, "build-arg:BUILDKIT_ATTEST_"); ok {
+			attests[strings.ToLower(after)] = v
 			continue
 		}
 	}
diff --git a/frontend/dockerfile/dockerfile_lint_test.go b/frontend/dockerfile/dockerfile_lint_test.go
index 01d5f6197..eb79d6e2b 100644
--- a/frontend/dockerfile/dockerfile_lint_test.go
+++ b/frontend/dockerfile/dockerfile_lint_test.go
@@ -1086,6 +1086,8 @@ FROM %s
 ENV PATH=$PAHT:/tmp/bin
 		`,
 		baseImg)
+	// not hint on Windows since default PATH is not set
+	hintStr := integration.UnixOrWindows(" (did you mean $PATH?)", "")
 	checkLinterWarnings(t, sb, &lintTestParams{
 		Dockerfile: dockerfile,
 		Warnings: []expectedLintWarning{
@@ -1093,7 +1095,7 @@ ENV PATH=$PAHT:/tmp/bin
 				RuleName:    "UndefinedVar",
 				Description: "Variables should be defined before their use",
 				URL:         "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
-				Detail:      "Usage of undefined variable '$PAHT' (did you mean $PATH?)",
+				Detail:      fmt.Sprintf("Usage of undefined variable '$PAHT'%s", hintStr),
 				Level:       1,
 				Line:        3,
 			},
diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go
index ac47a37f4..1293044f3 100644
--- a/frontend/dockerfile/dockerfile_test.go
+++ b/frontend/dockerfile/dockerfile_test.go
@@ -215,7 +215,7 @@ var allTests = integration.TestFuncs(
 	testLocalCustomSessionID,
 	testTargetStageNameArg,
 	testStepNames,
-	testPowershellInDefaultPathOnWindows,
+	testDefaultPathEnvOnWindows,
 	testOCILayoutMultiname,
 	testPlatformWithOSVersion,
 	testMaintainBaseOSVersion,
@@ -1943,7 +1943,8 @@ COPY Dockerfile .
 		entrypoint []string
 		env        []string
 	}{
-		{p: "windows/amd64", entrypoint: []string{"cmd", "/S", "/C", "foo bar"}, env: []string{"PATH=c:\\Windows\\System32;c:\\Windows;C:\\Windows\\System32\\WindowsPowerShell\\v1.0"}},
+		// we don't set PATH on Windows. #5445
+		{p: "windows/amd64", entrypoint: []string{"cmd", "/S", "/C", "foo bar"}, env: []string(nil)},
 		{p: "linux/amd64", entrypoint: []string{"/bin/sh", "-c", "foo bar"}, env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}},
 	} {
 		t.Run(exp.p, func(t *testing.T) {
@@ -2097,14 +2098,11 @@ COPY --from=base /out/ /
 	require.Equal(t, fmt.Sprintf("value:final%s", lineEnd), string(dt))
 }
 
-func testPowershellInDefaultPathOnWindows(t *testing.T, sb integration.Sandbox) {
+func testDefaultPathEnvOnWindows(t *testing.T, sb integration.Sandbox) {
 	integration.SkipOnPlatform(t, "!windows")
 
 	f := getFrontend(t, sb)
 
-	// just testing that the powershell path is in PATH
-	// but not testing powershell itself since it will need
-	// servercore image that is too bulky for just one single test.
 	dockerfile := []byte(`
 FROM nanoserver
 USER ContainerAdministrator
@@ -2137,7 +2135,7 @@ RUN echo %PATH% > env_path.txt
 	require.NoError(t, err)
 
 	envPath := string(dt)
-	require.Contains(t, envPath, "C:\\Windows\\System32\\WindowsPowerShell\\v1.0")
+	require.Contains(t, envPath, "C:\\Windows")
 }
 
 func testExportMultiPlatform(t *testing.T, sb integration.Sandbox) {
@@ -6993,7 +6991,6 @@ RUN  reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Param
 		},
 	}
 	for _, tt := range cases {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			_, err = f.Solve(sb.Context(), c, client.SolveOpt{
 				FrontendAttrs: tt.attrs,
diff --git a/frontend/dockerfile/instructions/parse_test.go b/frontend/dockerfile/instructions/parse_test.go
index e1adaa6d7..d3ae1e282 100644
--- a/frontend/dockerfile/instructions/parse_test.go
+++ b/frontend/dockerfile/instructions/parse_test.go
@@ -153,7 +153,6 @@ func TestParseOptInterval(t *testing.T) {
 
 func TestNilLinter(t *testing.T) {
 	for cmd := range command.Commands {
-		cmd := cmd
 		t.Run(cmd, func(t *testing.T) {
 			t.Parallel()
 
diff --git a/frontend/dockerfile/parser/parser_heredoc_test.go b/frontend/dockerfile/parser/parser_heredoc_test.go
index c158bcba1..461cf5d93 100644
--- a/frontend/dockerfile/parser/parser_heredoc_test.go
+++ b/frontend/dockerfile/parser/parser_heredoc_test.go
@@ -107,6 +107,12 @@ one two
 
 RUN <<$EOF
 $EOF
+
+RUN <<  EOF
+EOF
+
+RUN <<  EOF  > foo
+EOF
 	`)
 
 	tests := [][]Heredoc{
@@ -290,6 +296,22 @@ $EOF
 				Expand:  true,
 			},
 		},
+		{
+			// RUN <<  EOF
+			{
+				Name:    "EOF",
+				Content: "",
+				Expand:  true,
+			},
+		},
+		{
+			// RUN <<  EOF  /foo
+			{
+				Name:    "EOF",
+				Content: "",
+				Expand:  true,
+			},
+		},
 	}
 
 	result, err := Parse(dockerfile)
@@ -333,6 +355,8 @@ func TestParseHeredocHelpers(t *testing.T) {
 		"<<-'EOF'",
 		`<<-"EOF"`,
 		`<<EO"F"`,
+		"<< EOF",
+		"<<- EOF",
 	}
 	invalidHeredocs := []string{
 		"<<'EOF",
@@ -378,6 +402,18 @@ func TestHeredocsFromLine(t *testing.T) {
 			line:         "RUN <<-EOF",
 			heredocNames: []string{"EOF"},
 		},
+		{
+			line:         "RUN <<EOF",
+			heredocNames: []string{"EOF"},
+		},
+		{
+			line:         "RUN <<- EOF",
+			heredocNames: []string{"EOF"},
+		},
+		{
+			line:         "RUN << -EOF",
+			heredocNames: []string{"-EOF"},
+		},
 		{
 			line:         "RUN <<'EOF'",
 			heredocNames: []string{"EOF"},
diff --git a/frontend/dockerfile/shell/lex_test.go b/frontend/dockerfile/shell/lex_test.go
index cb8ffda4a..aa18532fa 100644
--- a/frontend/dockerfile/shell/lex_test.go
+++ b/frontend/dockerfile/shell/lex_test.go
@@ -585,7 +585,6 @@ func TestProcessWithMatches(t *testing.T) {
 	}
 
 	for _, c := range tc {
-		c := c
 		t.Run(c.input, func(t *testing.T) {
 			result, err := shlex.ProcessWordWithMatches(c.input, envsFromMap(c.envs))
 			w := result.Result
diff --git a/snapshot/snapshotter_test.go b/snapshot/snapshotter_test.go
index a9cb00816..21c4dc6bd 100644
--- a/snapshot/snapshotter_test.go
+++ b/snapshot/snapshotter_test.go
@@ -106,7 +106,6 @@ func newSnapshotter(ctx context.Context, t *testing.T, snapshotterName string) (
 
 func TestMerge(t *testing.T) {
 	for _, snName := range []string{"overlayfs", "native", "native-nohardlink"} {
-		snName := snName
 		t.Run(snName, func(t *testing.T) {
 			t.Parallel()
 			if snName == "overlayfs" {
@@ -310,7 +309,6 @@ func TestMerge(t *testing.T) {
 
 func TestHardlinks(t *testing.T) {
 	for _, snName := range []string{"overlayfs", "native"} {
-		snName := snName
 		t.Run(snName, func(t *testing.T) {
 			t.Parallel()
 			if snName == "overlayfs" {
@@ -376,7 +374,6 @@ func TestHardlinks(t *testing.T) {
 func TestMergeFileCapabilities(t *testing.T) {
 	requireRoot(t)
 	for _, snName := range []string{"overlayfs", "native", "native-nohardlink"} {
-		snName := snName
 		t.Run(snName, func(t *testing.T) {
 			t.Parallel()
 
@@ -410,7 +407,6 @@ func TestMergeFileCapabilities(t *testing.T) {
 
 func TestUsage(t *testing.T) {
 	for _, snName := range []string{"overlayfs", "native", "native-nohardlink"} {
-		snName := snName
 		t.Run(snName, func(t *testing.T) {
 			t.Parallel()
 			if snName == "overlayfs" {
diff --git a/solver/llbsolver/ops/exec_test.go b/solver/llbsolver/ops/exec_test.go
index 629f8a2dc..e0a5af9a5 100644
--- a/solver/llbsolver/ops/exec_test.go
+++ b/solver/llbsolver/ops/exec_test.go
@@ -107,7 +107,6 @@ func TestExecOpCacheMap(t *testing.T) {
 
 	ctx := context.Background()
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
@@ -192,7 +191,6 @@ func TestExecOpContentCache(t *testing.T) {
 
 	ctx := context.Background()
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
diff --git a/solver/scheduler_test.go b/solver/scheduler_test.go
index c5ddceec8..823ff99a1 100644
--- a/solver/scheduler_test.go
+++ b/solver/scheduler_test.go
@@ -736,7 +736,7 @@ func TestHugeGraph(t *testing.T) {
 	nodes := 1000
 
 	g, v := generateSubGraph(nodes)
-	// printGraph(g, "")
+	// PrintGraph(g, "")
 	g.Vertex.(*vertexSum).setupCallCounters()
 
 	res, err := j0.Build(ctx, g)
@@ -3925,12 +3925,11 @@ func (v *vertexSubBuild) Acquire(ctx context.Context) (ReleaseFunc, error) {
 	return func() {}, nil
 }
 
-//nolint:unused
-func printGraph(e Edge, pfx string) {
+func PrintGraph(e Edge, pfx string) {
 	name := e.Vertex.Name()
 	fmt.Printf("%s %d %s\n", pfx, e.Index, name)
 	for _, inp := range e.Vertex.Inputs() {
-		printGraph(inp, pfx+"-->")
+		PrintGraph(inp, pfx+"-->")
 	}
 }
 
diff --git a/source/git/identifier_test.go b/source/git/identifier_test.go
index 6f9bc7e55..c272f581e 100644
--- a/source/git/identifier_test.go
+++ b/source/git/identifier_test.go
@@ -110,7 +110,6 @@ func TestNewGitIdentifier(t *testing.T) {
 		},
 	}
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.url, func(t *testing.T) {
 			gi, err := NewGitIdentifier(tt.url)
 			require.NoError(t, err)
diff --git a/sourcepolicy/matcher_test.go b/sourcepolicy/matcher_test.go
index 7951f3e0c..7df31cf7c 100644
--- a/sourcepolicy/matcher_test.go
+++ b/sourcepolicy/matcher_test.go
@@ -318,7 +318,6 @@ func TestMatch(t *testing.T) {
 	}
 
 	for _, tc := range cases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			matches, err := match(&selectorCache{Selector: tc.src}, tc.ref, tc.attrs)
 			if !tc.xErr {
diff --git a/util/gitutil/git_ref_test.go b/util/gitutil/git_ref_test.go
index fe4d094b6..d470a09fc 100644
--- a/util/gitutil/git_ref_test.go
+++ b/util/gitutil/git_ref_test.go
@@ -140,7 +140,6 @@ func TestParseGitRef(t *testing.T) {
 		},
 	}
 	for _, tt := range cases {
-		tt := tt
 		t.Run(tt.ref, func(t *testing.T) {
 			got, err := ParseGitRef(tt.ref)
 			if tt.expected == nil {
diff --git a/util/purl/image_test.go b/util/purl/image_test.go
index ea94bc9da..58ae6f96e 100644
--- a/util/purl/image_test.go
+++ b/util/purl/image_test.go
@@ -73,7 +73,6 @@ func TestRefToPURL(t *testing.T) {
 	}
 
 	for _, tc := range tcases {
-		tc := tc
 		t.Run(tc.ref, func(t *testing.T) {
 			purl, err := RefToPURL(packageurl.TypeDocker, tc.ref, tc.platform)
 			if tc.err {
@@ -138,7 +137,6 @@ func TestPURLToRef(t *testing.T) {
 	}
 
 	for _, tc := range tcases {
-		tc := tc
 		t.Run(tc.purl, func(t *testing.T) {
 			ref, platform, err := PURLToRef(tc.purl)
 			if tc.err {
diff --git a/util/testutil/integration/run.go b/util/testutil/integration/run.go
index 5067ad389..754527a97 100644
--- a/util/testutil/integration/run.go
+++ b/util/testutil/integration/run.go
@@ -168,8 +168,8 @@ func Run(t *testing.T, testCases []Test, opt ...TestOpt) {
 		parts := strings.Split(filter, "/")
 		if len(parts) >= 2 {
 			const prefix = "slice="
-			if strings.HasPrefix(parts[1], prefix) {
-				conf := strings.TrimPrefix(parts[1], prefix)
+			if after, ok0 := strings.CutPrefix(parts[1], prefix); ok0 {
+				conf := after
 				offsetS, totalS, ok := strings.Cut(conf, "-")
 				if !ok {
 					t.Fatalf("invalid slice=%q", conf)
@@ -514,8 +514,8 @@ func prepareValueMatrix(tc testConf) []matrixValue {
 func SkipOnPlatform(t *testing.T, goos string) {
 	skip := false
 	// support for negation
-	if strings.HasPrefix(goos, "!") {
-		goos = strings.TrimPrefix(goos, "!")
+	if after, ok := strings.CutPrefix(goos, "!"); ok {
+		goos = after
 		skip = runtime.GOOS != goos
 	} else {
 		skip = runtime.GOOS == goos
@@ -539,8 +539,8 @@ func UnixOrWindows[T any](unix, windows T) T {
 func lookupTestFilter() (string, bool) {
 	const prefix = "-test.run="
 	for _, arg := range os.Args {
-		if strings.HasPrefix(arg, prefix) {
-			return strings.TrimPrefix(arg, prefix), true
+		if after, ok := strings.CutPrefix(arg, prefix); ok {
+			return after, true
 		}
 	}
 	return "", false
diff --git a/util/urlutil/redact_test.go b/util/urlutil/redact_test.go
index 6a3ea7cec..1b3143c96 100644
--- a/util/urlutil/redact_test.go
+++ b/util/urlutil/redact_test.go
@@ -35,7 +35,6 @@ func TestRedactCredentials(t *testing.T) {
 		},
 	}
 	for _, tt := range cases {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			if g, w := RedactCredentials(tt.url), tt.want; g != w {
 				t.Fatalf("got: %q\nwant: %q", g, w)
diff --git a/version/ua_test.go b/version/ua_test.go
index ce0c15c67..65bf27f1b 100644
--- a/version/ua_test.go
+++ b/version/ua_test.go
@@ -34,7 +34,6 @@ func TestUserAgent(t *testing.T) {
 		},
 	}
 	for _, tt := range cases {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			Version = tt.version
 			for pname, pver := range tt.products {