Repository: moby/buildkit

Issue:
adding SBOM fails with trying to send message larger than max (25286341 vs. 16777216): unknown
I have a quite specific use case:

I'm building a container which has a bunch of tools installed which is supposed to be used for CI runs.

When trying to add a SBOM during building (`docker buildx build --sbom=true`) the build succeeds but the `exporting to image` step fails with
```
------
 > exporting to image:
------
ERROR: failed to solve: error writing data blob sha256:7b5af02ce619d513c5fed786b397d5d2365ce1f3736e4f3379a27058004b0b43: failed to copy: failed to send write: trying to send message larger than max (25286341 vs. 16777216): unknown
```

My best guess is, that this is happening due to the SBOM being too large.

Discussion:
@tonistiigi: Do you have example reproduction steps?@tonistiigi: Also, please post all buildkit version information as something like this was already fixed quite a long time ago.@ifalatiksetlog: I haven't had the chance to build a reproduction Dockerfile, as the image I'm building is quite large, and includes some of our internal tools, I'll have to somehow work around those.
Regarding version I can only give you this:
```
> docker build ...
level=info msg="starting syft scanner for buildkit v1.4.0"

> docker version
Client:
 Version:           27.2.0
 API version:       1.47
 Go version:        go1.21.13
 Git commit:        3ab4256
 Built:             Tue Aug 27 14:17:17 2024
 OS/Arch:           windows/amd64
 Context:           desktop-linux

Server: Docker Desktop 4.34.1 (166053)
 Engine:
  Version:          27.2.0
  API version:      1.47 (minimum version 1.24)
  Go version:       go1.21.13
  Git commit:       3ab5c7d
  Built:            Tue Aug 27 14:15:15 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.7.20
  GitCommit:        8fc6bcff51318944179630522a095cc9dbf9f353
 runc:
  Version:          1.1.13
  GitCommit:        v1.1.13-0-g58aa920
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```

If there's a way to get the BuildKit version specifically I'd need you to point me in the right direction there.@ifalatiksetlog: Here's the reproduction:
PowerShell invocation:
```pwsh
& docker buildx build --no-cache -t buildkit-sbom:test --sbom=true -f .\Dockerfile --progress=plain . 2>&1 | Out-File docker-build.log
```
Dockerfile:
```Dockerfile
FROM ubuntu@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab63ee
# (24.04)

ENV DEBIAN_FRONTEND=noninteractive

# misc tools
RUN apt-get update && apt-get install -y --no-install-recommends \
    apt-utils \
    openssh-client \
    ca-certificates \
    build-essential libz-dev zlib1g-dev \
    git \
    wget \
    curl \
    nano \
    colorized-logs \
    asciidoctor \
    httpie \
    unzip \
    zip \
    gpg \
    lsb-release \
    xz-utils \
    rsync \
    gettext-base \
    genisoimage \
    jq \
    pip \
    bridge-utils \
    dhcping \
    osslsigncode \
    file\
    iftop \
    iperf3 \
    iproute2 \
    iptables \
    iptraf-ng \
    mtr \
    mysql-client \
    netcat-openbsd \
    nftables \
    ngrep \
    nmap \
    strace \
    tcpdump \
    iputils-ping \
    python-is-python3 \
    libxml2-utils \
    python3-jmespath \
    && apt-get -y autoremove

# python
RUN apt-get update && apt-get install -y --no-install-recommends \
    python3.12 \
    python3-pip \
    python3.12-venv \
    && rm -rf /var/lib/apt/lists/*

# java
RUN mkdir -p /tmp/app && cd /tmp/app
RUN APP_DIR="/usr/local/bin/jdk-8" && \
    APP_URL="https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u412-b08/OpenJDK8U-jdk_x64_linux_hotspot_8u412b08.tar.gz" && \
    echo "b9884a96f78543276a6399c3eb8c2fd8a80e6b432ea50e87d3d12d495d1d2808  app.tar.gz" > SHA256SUMS && \
    wget --progress=bar:force:noscroll "${APP_URL}" -O app.tar.gz && \
    sha256sum -c SHA256SUMS && \
    mkdir -p "${APP_DIR}" && \
    tar -x --strip-components 1 --directory "${APP_DIR}" -f app.tar.gz
RUN APP_DIR="/usr/local/bin/jdk-21" && \
    APP_URL="https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.3%2B9/OpenJDK21U-jdk_x64_linux_hotspot_21.0.3_9.tar.gz" && \
    echo "fffa52c22d797b715a962e6c8d11ec7d79b90dd819b5bc51d62137ea4b22a340  app.tar.gz" > SHA256SUMS && \
    wget --progress=bar:force:noscroll "${APP_URL}" -O app.tar.gz && \
    sha256sum -c SHA256SUMS && \
    mkdir -p "${APP_DIR}" && \
    tar -x --strip-components 1 --directory "${APP_DIR}" -f app.tar.gz
RUN APP_DIR="/usr/local/bin/jdk-graalvm"  && \
    APP_URL="https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz" && \
    DIGEST_URL="https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz.sha256" && \
    sha256sum -c SHA256SUMS && \
    wget --progress=bar:force:noscroll "${APP_URL}" -O app.tar.gz && \
    wget --progress=bar:force:noscroll "${DIGEST_URL}" -O SHA256SUMS && echo " app.tar.gz" >> SHA256SUMS && \
    sha256sum -c SHA256SUMS && \
    mkdir -p "${APP_DIR}" && \
    tar -x --strip-components 1 --directory "${APP_DIR}" -f app.tar.gz
RUN rm /usr/local/bin/jdk*/src.zip || true && \
    rm /usr/local/bin/jdk*/lib/src.zip || true && \
    rm -r /tmp/app

# go
RUN mkdir -p /tmp/app && cd /tmp/app
RUN APP_DIR="/usr/local/bin/go-1.23.1" && \
    APP_URL="https://go.dev/dl/go1.23.1.linux-amd64.tar.gz" && \
    echo "49bbb517cfa9eee677e1e7897f7cf9cfdbcf49e05f61984a2789136de359f9bd  app.tar.gz" > SHA256SUMS && \
    wget --progress=bar:force:noscroll "${APP_URL}" -O app.tar.gz && \
    sha256sum -c SHA256SUMS && \
    mkdir -p "${APP_DIR}" && \
    tar -x --strip-components 1 --directory "${APP_DIR}" -f app.tar.gz && \
    rm -rf ${APP_DIR}/api && \
    ln -s ${APP_DIR} /usr/local/bin/go-latest && \
    curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /root/go/bin v1.61.0 && \
    export PATH=$PATH:/root/go/bin && \
    golangci-lint --version && \
    rm -r /tmp/app

# node
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
    NODE_MAJOR=18 && \
    echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
    apt-get update && apt-get install --install-recommends -y nodejs

# cleanup
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    apt-get clean && \
    apt-get autoremove -y --purge

```
the full output log: [docker-build.log](https://github.com/user-attachments/files/16991659/docker-build.log).@dreamworlds1234: > I have a quite specific use case:
> 
> I'm building a container which has a bunch of tools installed which is supposed to be used for CI runs.
> 
> When trying to add a SBOM during building (`docker buildx build --sbom=true`) the build succeeds but the `exporting to image` step fails with
> 
> ```
> ------
>  > exporting to image:
> ------
> ERROR: failed to solve: error writing data blob sha256:7b5af02ce619d513c5fed786b397d5d2365ce1f3736e4f3379a27058004b0b43: failed to copy: failed to send write: trying to send message larger than max (25286341 vs. 16777216): unknown
> ```
> 
> My best guess is, that this is happening due to the SBOM being too large.

I am having the same error after I enabling the "--sbom=true -o type=local,dest=out"@tonistiigi: @dreamworlds1234 Are you enabling containerd image store in Docker for `--sbom=true` to work or creating a builder instance with `buildx create` ?

I couldn't reproduce on mac, I get some warnings on sbom generation but otherwise seems to work fine:

```
#20 [linux/arm64] generating sbom using docker.io/docker/buildkit-syft-scanner:stable-1
#20 0.069 time="2024-09-13T22:28:29Z" level=info msg="starting syft scanner for buildkit v1.4.0"
#20 6.074 [0006]  WARN cataloger failed cataloger=java-archive-cataloger error=unable to read files from java archive: unable to open zip archive (/tmp/syft-archive-contents-1423641467/archive-test-badbase.zip): unable to open ZipReadCloser @ "/tmp/syft-archive-contents-1423641467/archive-test-badbase.zip": zip: not a valid zip file location=/usr/local/bin/go-1.23.1/src/archive/zip/testdata/test-badbase.zip
#20 6.074 [0006]  WARN cataloger failed cataloger=java-archive-cataloger error=unable to read files from java archive: unable to open zip archive (/tmp/syft-archive-contents-2888149883/archive-test-baddirsz.zip): unable to open ZipReadCloser @ "/tmp/syft-archive-contents-2888149883/archive-test-baddirsz.zip": zip: not a valid zip file location=/usr/local/bin/go-1.23.1/src/archive/zip/testdata/test-baddirsz.zip
#20 7.433 [0007]  WARN cataloger failed cataloger=linux-kernel-cataloger error=unable to get magic type for file: EOF location=/usr/local/bin/go-1.23.1/src/debug/pe/testdata/vmlinuz-4.15.0-47-generic
#20 8.777 [0008]  WARN unable to process executable "/usr/local/bin/go-1.23.1/src/debug/pe/testdata/vmlinuz-4.15.0-47-generic" error=unable to determine executable kind: unable to read first sector of file: EOF
#20 DONE 11.0s
```@ifalatik: @tonistiigi yeah those errors occur for me too. So I guess it's environment specific? What could it depend on?@dreamworlds1234: > @dreamworlds1234 Are you enabling containerd image store in Docker for `--sbom=true` to work or creating a builder instance with `buildx create` ?
> 
> I couldn't reproduce on mac, I get some warnings on sbom generation but otherwise seems to work fine:
> 
> ```
> #20 [linux/arm64] generating sbom using docker.io/docker/buildkit-syft-scanner:stable-1
> #20 0.069 time="2024-09-13T22:28:29Z" level=info msg="starting syft scanner for buildkit v1.4.0"
> #20 6.074 [0006]  WARN cataloger failed cataloger=java-archive-cataloger error=unable to read files from java archive: unable to open zip archive (/tmp/syft-archive-contents-1423641467/archive-test-badbase.zip): unable to open ZipReadCloser @ "/tmp/syft-archive-contents-1423641467/archive-test-badbase.zip": zip: not a valid zip file location=/usr/local/bin/go-1.23.1/src/archive/zip/testdata/test-badbase.zip
> #20 6.074 [0006]  WARN cataloger failed cataloger=java-archive-cataloger error=unable to read files from java archive: unable to open zip archive (/tmp/syft-archive-contents-2888149883/archive-test-baddirsz.zip): unable to open ZipReadCloser @ "/tmp/syft-archive-contents-2888149883/archive-test-baddirsz.zip": zip: not a valid zip file location=/usr/local/bin/go-1.23.1/src/archive/zip/testdata/test-baddirsz.zip
> #20 7.433 [0007]  WARN cataloger failed cataloger=linux-kernel-cataloger error=unable to get magic type for file: EOF location=/usr/local/bin/go-1.23.1/src/debug/pe/testdata/vmlinuz-4.15.0-47-generic
> #20 8.777 [0008]  WARN unable to process executable "/usr/local/bin/go-1.23.1/src/debug/pe/testdata/vmlinuz-4.15.0-47-generic" error=unable to determine executable kind: unable to read first sector of file: EOF
> #20 DONE 11.0s
> ```

I created the builder (builderx) for cache purpose, and using it like this:

docker buildx build --sbom=true -o type=local,dest=out -f config/docker/Dockerfile.prod . --tag testbuild --iidfile testbuildiidfile

However, I found the error is related to the "containerd max message size", which can increase in the configuration for send and receive max size.

However, what I don't understand is why sbom needs such large size, is it a bug in the docker/nerdcli?

@tonistiigi: > However, I found the error is related to the "containerd max message size", which can increase in the configuration for send and receive max size.

We don't have such configuration options in buildkitd config https://github.com/moby/buildkit/blob/master/cmd/buildkitd/config/config.go . If you mean containerd config then don't see how this is related to the buildx builder. You said you created builder instance with `buildx create` and that instance does not rely on containerd daemon.

I see you are using `-o type=local,dest=out`. I don't think this can cause the issue but there is a known issue exporting big images with local exporter https://github.com/grpc/grpc-go/issues/4722  that can cause deadlocks. Local exporter is for exporting individual files directly to client (eg. if you are building a release binary in Dockerfile). It does not make sense to export full image rootfs like this as the image would not be runnable anyway (you will not have layers, the ownership of files would be changed to your local user, special files can't be written). If you want to export an image without layers you should use `-o type=tar` that preserves the file ownerships, or `-o type=image`/`-o type=oci` if you want to export image.@ifalatiksetlog: @tonistiigi if it helps I'm using the containerd docker desktop registry.@alexander-e-f-smith: I have same issue. Has this been resolved by anyone?@tonistiigi: @alexander-e-f-smith Are there reproduction steps? The steps above do not independently reproduce as mentioned in previous comments.@NiklasBr: ERROR: failed to solve: error writing data blob sha256:c9392e26d30701c1ba6551f8efb736e53372e899158a5351446e4aa0a2f190ec: failed to copy: failed to send write: trying to send message larger than max (25118494 vs. 16777216): unknown

View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/gyvldem5l3npd7himaq9b7isp
@NiklasBr: The above error appears to happen when using 
`docker build --progress=plain --platform=linux/amd64,linux/arm64 --load --attest type=sbom --provenance=true .`

Skipping attest and provenance works:

`docker build --progress=plain --platform=linux/amd64,linux/arm64 --load .`@tonistiigi: https://github.com/containerd/containerd/pull/11441 should fix this and #5785 is mitigation

Solution patch:
diff --git a/.github/workflows/test-os.yml b/.github/workflows/test-os.yml
index 42e92c8b2..bed5901af 100644
--- a/.github/workflows/test-os.yml
+++ b/.github/workflows/test-os.yml
@@ -89,7 +89,18 @@ jobs:
           - ./worker/containerd
           - ./solver
           - ./frontend
-          - ./frontend/dockerfile
+          - ./frontend/dockerfile#1-12
+          - ./frontend/dockerfile#2-12
+          - ./frontend/dockerfile#3-12
+          - ./frontend/dockerfile#4-12
+          - ./frontend/dockerfile#5-12
+          - ./frontend/dockerfile#6-12
+          - ./frontend/dockerfile#7-12
+          - ./frontend/dockerfile#8-12
+          - ./frontend/dockerfile#9-12
+          - ./frontend/dockerfile#10-12
+          - ./frontend/dockerfile#11-12
+          - ./frontend/dockerfile#12-12
         include:
           - os: windows-2022
             worker: containerd
@@ -103,10 +114,18 @@ jobs:
       -
         name: Prepare
         run: |
+          TESTPKG=$(echo "${{ matrix.pkg }}" | awk '-F#' '{print $1}')
+          TESTSLICE=$(echo "${{ matrix.pkg }}" | awk '-F#' '{print $2}')
+          echo "TESTPKG=$TESTPKG" >> $GITHUB_ENV
           echo "TEST_REPORT_NAME=${{ github.job }}-$(echo "${{ matrix.pkg }}-${{ matrix.skip-integration-tests }}-${{ matrix.worker }}" | tr -dc '[:alnum:]-\n\r' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
           testFlags="${{ env.TESTFLAGS }}"
+          testSliceOffset=""
+          if [ -n "$TESTSLICE" ]; then
+            echo "TESTSLICE=$TESTSLICE" >> $GITHUB_ENV
+            testSliceOffset="slice=$TESTSLICE/"
+          fi
           if [ -n "${{ matrix.worker }}" ]; then
-            testFlags="${testFlags} --run=TestIntegration/.*/worker=${{ matrix.worker }}"
+            testFlags="${testFlags} --run=TestIntegration/$testSliceOffset.*/worker=${{ matrix.worker }}"
           fi
           echo "TESTFLAGS=${testFlags}" >> $GITHUB_ENV
         shell: bash
@@ -142,7 +161,7 @@ jobs:
           gotestsum \
             --jsonfile="./bin/testreports/go-test-report-${{ env.TEST_REPORT_NAME }}.json" \
             --junitfile="./bin/testreports/junit-report-${{ env.TEST_REPORT_NAME }}.xml" \
-            --packages="${{ matrix.pkg }}" \
+            --packages="${{ env.TESTPKG }}" \
             -- \
               "-mod=vendor" \
               "-coverprofile" "./bin/testreports/coverage-${{ env.TEST_REPORT_NAME }}.txt" \
@@ -220,7 +239,7 @@ jobs:
           vagrant up --no-tty
       -
         name: Smoke test
-        uses: nick-fields/retry@c97818ca39074beaea45180dba704f92496a0082  # v3.0.1
+        uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08  # v3.0.2
         with:
           timeout_minutes: 20
           max_attempts: 5
diff --git a/Dockerfile b/Dockerfile
index 6775afc65..50a21cdb2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -162,8 +162,8 @@ FROM scratch AS cni-plugins-export-squashed
 COPY --from=cni-plugins-export / /
 
 FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS binfmt-filter
-# built from https://github.com/tonistiigi/binfmt/releases/tag/buildkit%2Fv9.2.0-50
-COPY --link --from=tonistiigi/binfmt:buildkit-v9.2.0-50@sha256:ff21b00e7238dce3bbd74fbe25591f7213837a77861b47b2df5e019540ec33fa / /out/
+# built from https://github.com/tonistiigi/binfmt/releases/tag/buildkit%2Fv9.2.2-54
+COPY --link --from=tonistiigi/binfmt:buildkit-v9.2.2-54@sha256:e60fbf01e26c75efa816224f4de31c2ef63c5486b20c3e8fa1e5da2aff368ba9 / /out/
 WORKDIR /out/
 RUN rm buildkit-qemu-loongarch64 buildkit-qemu-mips64 buildkit-qemu-mips64el
 
diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go
index 77c91c15d..fe98b708b 100644
--- a/frontend/dockerfile/dockerfile2llb/convert.go
+++ b/frontend/dockerfile/dockerfile2llb/convert.go
@@ -1600,7 +1600,7 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error {
 		commitMessage.WriteString(" <<" + src.Path)
 
 		data := src.Data
-		f, err := system.CheckSystemDriveAndRemoveDriveLetter(src.Path, d.platform.OS)
+		f, err := system.CheckSystemDriveAndRemoveDriveLetter(src.Path, d.platform.OS, false)
 		if err != nil {
 			return errors.Wrap(err, "removing drive letter")
 		}
@@ -1868,7 +1868,7 @@ func pathRelativeToWorkingDir(s llb.State, p string, platform ocispecs.Platform)
 		return "", err
 	}
 
-	p, err = system.CheckSystemDriveAndRemoveDriveLetter(p, platform.OS)
+	p, err = system.CheckSystemDriveAndRemoveDriveLetter(p, platform.OS, true)
 	if err != nil {
 		return "", errors.Wrap(err, "removing drive letter")
 	}
diff --git a/go.mod b/go.mod
index 479b9c30c..887c02163 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2
 	github.com/containerd/console v1.0.4
 	github.com/containerd/containerd/api v1.8.0
-	github.com/containerd/containerd/v2 v2.0.2
+	github.com/containerd/containerd/v2 v2.0.3
 	github.com/containerd/continuity v0.4.5
 	github.com/containerd/errdefs v1.0.0
 	github.com/containerd/fuse-overlayfs-snapshotter/v2 v2.1.1
@@ -73,7 +73,7 @@ require (
 	github.com/stretchr/testify v1.10.0
 	github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205
 	github.com/tonistiigi/fsutil v0.0.0-20250227023350-13604323a291
-	github.com/tonistiigi/go-actions-cache v0.0.0-20250219102945-1a5174abd055
+	github.com/tonistiigi/go-actions-cache v0.0.0-20250228231703-3e9a6642607f
 	github.com/tonistiigi/go-archvariant v1.0.0
 	github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4
 	github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea
diff --git a/go.sum b/go.sum
index 98d55401a..d74a5262a 100644
--- a/go.sum
+++ b/go.sum
@@ -95,8 +95,8 @@ github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn
 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0=
 github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc=
-github.com/containerd/containerd/v2 v2.0.2 h1:GmH/tRBlTvrXOLwSpWE2vNAm8+MqI6nmxKpKBNKY8Wc=
-github.com/containerd/containerd/v2 v2.0.2/go.mod h1:wIqEvQ/6cyPFUGJ5yMFanspPabMLor+bF865OHvNTTI=
+github.com/containerd/containerd/v2 v2.0.3 h1:zBKgwgZsuu+LPCMzCLgA4sC4MiZzZ59ZT31XkmiISQM=
+github.com/containerd/containerd/v2 v2.0.3/go.mod h1:5j9QUUaV/cy9ZeAx4S+8n9ffpf+iYnEj4jiExgcbuLY=
 github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
 github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -397,8 +397,8 @@ github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 h1:eUk79E1
 github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
 github.com/tonistiigi/fsutil v0.0.0-20250227023350-13604323a291 h1:1WRvmYdTkwjgFIlnOwo6Y15BMwQBUQT423y1zHad4Eo=
 github.com/tonistiigi/fsutil v0.0.0-20250227023350-13604323a291/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw=
-github.com/tonistiigi/go-actions-cache v0.0.0-20250219102945-1a5174abd055 h1:ocuqZe/ImPTYgDocnHTDOTBachfQ9m0JxvP4uGeDtBE=
-github.com/tonistiigi/go-actions-cache v0.0.0-20250219102945-1a5174abd055/go.mod h1:h0oRlVs3NoFIHysRQ4rU1+RG4QmU0M2JVSwTYrB4igk=
+github.com/tonistiigi/go-actions-cache v0.0.0-20250228231703-3e9a6642607f h1:q/SWz3Bz0KtAsqaBo73CHVXjaz5O8PDnmD2JHVhgYnE=
+github.com/tonistiigi/go-actions-cache v0.0.0-20250228231703-3e9a6642607f/go.mod h1:h0oRlVs3NoFIHysRQ4rU1+RG4QmU0M2JVSwTYrB4igk=
 github.com/tonistiigi/go-archvariant v1.0.0 h1:5LC1eDWiBNflnTF1prCiX09yfNHIxDC/aukdhCdTyb0=
 github.com/tonistiigi/go-archvariant v1.0.0/go.mod h1:TxFmO5VS6vMq2kvs3ht04iPXtu2rUT/erOnGFYfk5Ho=
 github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
diff --git a/solver/llbsolver/file/backend.go b/solver/llbsolver/file/backend.go
index 465cfe67a..ad7ac8fd5 100644
--- a/solver/llbsolver/file/backend.go
+++ b/solver/llbsolver/file/backend.go
@@ -5,7 +5,6 @@ import (
 	"log"
 	"os"
 	"path/filepath"
-	"runtime"
 	"strings"
 	"time"
 
@@ -14,7 +13,6 @@ import (
 	"github.com/moby/buildkit/snapshot"
 	"github.com/moby/buildkit/solver/llbsolver/ops/fileoptypes"
 	"github.com/moby/buildkit/solver/pb"
-	"github.com/moby/buildkit/util/system"
 	"github.com/pkg/errors"
 	copy "github.com/tonistiigi/fsutil/copy"
 )
@@ -145,10 +143,7 @@ func rm(d string, action *pb.FileActionRm) (err error) {
 	}()
 
 	if action.AllowWildcard {
-		src, err := cleanPath(action.Path)
-		if err != nil {
-			return errors.Wrap(err, "cleaning path")
-		}
+		src := cleanPath(action.Path)
 		m, err := copy.ResolveWildcards(d, src, false)
 		if err != nil {
 			return errors.WithStack(err)
@@ -190,14 +185,9 @@ func rmPath(root, src string, allowNotFound bool) error {
 }
 
 func docopy(ctx context.Context, src, dest string, action *pb.FileActionCopy, u *copy.User, idmap *idtools.IdentityMapping) (err error) {
-	srcPath, err := cleanPath(action.Src)
-	if err != nil {
-		return errors.Wrap(err, "cleaning source path")
-	}
-	destPath, err := cleanPath(action.Dest)
-	if err != nil {
-		return errors.Wrap(err, "cleaning destination path")
-	}
+	srcPath := cleanPath(action.Src)
+	destPath := cleanPath(action.Dest)
+
 	if !action.CreateDestPath {
 		p, err := fs.RootPath(dest, filepath.Join("/", action.Dest))
 		if err != nil {
@@ -434,11 +424,7 @@ func (fb *Backend) readUserWrapper(owner *pb.ChownOpt, user, group fileoptypes.M
 	return u, nil
 }
 
-func cleanPath(s string) (string, error) {
-	s, err := system.CheckSystemDriveAndRemoveDriveLetter(s, runtime.GOOS)
-	if err != nil {
-		return "", errors.Wrap(err, "removing drive letter")
-	}
+func cleanPath(s string) string {
 	s = filepath.FromSlash(s)
 	s2 := filepath.Join("/", s)
 	if strings.HasSuffix(s, string(filepath.Separator)+".") {
@@ -449,5 +435,5 @@ func cleanPath(s string) (string, error) {
 	} else if strings.HasSuffix(s, string(filepath.Separator)) && s2 != string(filepath.Separator) {
 		s2 += string(filepath.Separator)
 	}
-	return s2, nil
+	return s2
 }
diff --git a/util/system/path.go b/util/system/path.go
index 14f02b495..fdd410386 100644
--- a/util/system/path.go
+++ b/util/system/path.go
@@ -2,7 +2,6 @@ package system
 
 import (
 	"path"
-	"path/filepath"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -46,7 +45,7 @@ func NormalizePath(parent, newPath, inputOS string, keepSlash bool) (string, err
 	}
 
 	var err error
-	parent, err = CheckSystemDriveAndRemoveDriveLetter(parent, inputOS)
+	parent, err = CheckSystemDriveAndRemoveDriveLetter(parent, inputOS, keepSlash)
 	if err != nil {
 		return "", errors.Wrap(err, "removing drive letter")
 	}
@@ -61,7 +60,7 @@ func NormalizePath(parent, newPath, inputOS string, keepSlash bool) (string, err
 		newPath = parent
 	}
 
-	newPath, err = CheckSystemDriveAndRemoveDriveLetter(newPath, inputOS)
+	newPath, err = CheckSystemDriveAndRemoveDriveLetter(newPath, inputOS, keepSlash)
 	if err != nil {
 		return "", errors.Wrap(err, "removing drive letter")
 	}
@@ -137,7 +136,7 @@ func IsAbs(pth, inputOS string) bool {
 	if inputOS == "" {
 		inputOS = "linux"
 	}
-	cleanedPath, err := CheckSystemDriveAndRemoveDriveLetter(pth, inputOS)
+	cleanedPath, err := CheckSystemDriveAndRemoveDriveLetter(pth, inputOS, false)
 	if err != nil {
 		return false
 	}
@@ -174,7 +173,7 @@ func IsAbs(pth, inputOS string) bool {
 // There is no sane way to support this without adding a lot of complexity
 // which I am not sure is worth it.
 // \\.\C$\a     --> Fail
-func CheckSystemDriveAndRemoveDriveLetter(path string, inputOS string) (string, error) {
+func CheckSystemDriveAndRemoveDriveLetter(path string, inputOS string, keepSlash bool) (string, error) {
 	if inputOS == "" {
 		inputOS = "linux"
 	}
@@ -193,9 +192,10 @@ func CheckSystemDriveAndRemoveDriveLetter(path string, inputOS string) (string,
 	}
 
 	parts := strings.SplitN(path, ":", 2)
+
 	// Path does not have a drive letter. Just return it.
 	if len(parts) < 2 {
-		return ToSlash(filepath.Clean(path), inputOS), nil
+		return ToSlash(cleanPath(path, inputOS, keepSlash), inputOS), nil
 	}
 
 	// We expect all paths to be in C:
@@ -220,5 +220,30 @@ func CheckSystemDriveAndRemoveDriveLetter(path string, inputOS string) (string,
 	//
 	// We must return the second element of the split path, as is, without attempting to convert
 	// it to an absolute path. We have no knowledge of the CWD; that is treated elsewhere.
-	return ToSlash(filepath.Clean(parts[1]), inputOS), nil
+	return ToSlash(cleanPath(parts[1], inputOS, keepSlash), inputOS), nil
+}
+
+// An adaptation of filepath.Clean to allow an option to
+// retain the trailing slash, on either of the platforms.
+// See https://github.com/moby/buildkit/issues/5249
+func cleanPath(origPath, inputOS string, keepSlash bool) string {
+	// so as to handle cases like \\a\\b\\..\\c\\
+	// on Linux, when inputOS is Windows
+	origPath = ToSlash(origPath, inputOS)
+
+	if !keepSlash {
+		return path.Clean(origPath)
+	}
+
+	cleanedPath := path.Clean(origPath)
+	// Windows supports both \\ and / as path separator.
+	hasTrailingSlash := strings.HasSuffix(origPath, "/")
+	if inputOS == "windows" {
+		hasTrailingSlash = hasTrailingSlash || strings.HasSuffix(origPath, "\\")
+	}
+
+	if len(cleanedPath) > 1 && hasTrailingSlash {
+		return cleanedPath + "/"
+	}
+	return cleanedPath
 }
diff --git a/vendor/github.com/containerd/containerd/v2/core/content/proxy/content_writer.go b/vendor/github.com/containerd/containerd/v2/core/content/proxy/content_writer.go
index 214a0a335..98818f234 100644
--- a/vendor/github.com/containerd/containerd/v2/core/content/proxy/content_writer.go
+++ b/vendor/github.com/containerd/containerd/v2/core/content/proxy/content_writer.go
@@ -26,6 +26,7 @@ import (
 	digest "github.com/opencontainers/go-digest"
 
 	"github.com/containerd/containerd/v2/core/content"
+	"github.com/containerd/containerd/v2/defaults"
 	"github.com/containerd/containerd/v2/pkg/protobuf"
 )
 
@@ -76,27 +77,37 @@ func (rw *remoteWriter) Digest() digest.Digest {
 }
 
 func (rw *remoteWriter) Write(p []byte) (n int, err error) {
-	offset := rw.offset
+	const maxBufferSize = defaults.DefaultMaxSendMsgSize >> 1
+	for i := 0; i < len(p); i += maxBufferSize {
+		offset := rw.offset
 
-	resp, err := rw.send(&contentapi.WriteContentRequest{
-		Action: contentapi.WriteAction_WRITE,
-		Offset: offset,
-		Data:   p,
-	})
-	if err != nil {
-		return 0, fmt.Errorf("failed to send write: %w", errgrpc.ToNative(err))
-	}
+		end := i + maxBufferSize
+		if end > len(p) {
+			end = len(p)
+		}
+		data := p[i:end]
+
+		resp, err := rw.send(&contentapi.WriteContentRequest{
+			Action: contentapi.WriteAction_WRITE,
+			Offset: offset,
+			Data:   data,
+		})
+		if err != nil {
+			return 0, fmt.Errorf("failed to send write: %w", errgrpc.ToNative(err))
+		}
 
-	n = int(resp.Offset - offset)
-	if n < len(p) {
-		err = io.ErrShortWrite
-	}
+		written := int(resp.Offset - offset)
+		rw.offset += int64(written)
+		if resp.Digest != "" {
+			rw.digest = digest.Digest(resp.Digest)
+		}
+		n += written
 
-	rw.offset += int64(n)
-	if resp.Digest != "" {
-		rw.digest = digest.Digest(resp.Digest)
+		if written < len(data) {
+			return n, io.ErrShortWrite
+		}
 	}
-	return
+	return n, nil
 }
 
 func (rw *remoteWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) (err error) {
diff --git a/vendor/github.com/containerd/containerd/v2/pkg/oci/spec_opts.go b/vendor/github.com/containerd/containerd/v2/pkg/oci/spec_opts.go
index 5101c63bb..3b85d764a 100644
--- a/vendor/github.com/containerd/containerd/v2/pkg/oci/spec_opts.go
+++ b/vendor/github.com/containerd/containerd/v2/pkg/oci/spec_opts.go
@@ -28,18 +28,17 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/containerd/containerd/v2/core/containers"
-	"github.com/containerd/containerd/v2/core/content"
-	"github.com/containerd/containerd/v2/core/images"
-	"github.com/containerd/containerd/v2/core/mount"
-	"github.com/containerd/containerd/v2/pkg/namespaces"
 	"github.com/containerd/continuity/fs"
-	"github.com/containerd/log"
 	"github.com/containerd/platforms"
 	"github.com/moby/sys/user"
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/opencontainers/runtime-spec/specs-go"
-	"tags.cncf.io/container-device-interface/pkg/cdi"
+
+	"github.com/containerd/containerd/v2/core/containers"
+	"github.com/containerd/containerd/v2/core/content"
+	"github.com/containerd/containerd/v2/core/images"
+	"github.com/containerd/containerd/v2/core/mount"
+	"github.com/containerd/containerd/v2/pkg/namespaces"
 )
 
 // SpecOpts sets spec specific information to a newly generated OCI spec
@@ -1643,31 +1642,3 @@ func WithWindowsNetworkNamespace(ns string) SpecOpts {
 		return nil
 	}
 }
-
-// WithCDIDevices injects the requested CDI devices into the OCI specification.
-func WithCDIDevices(devices ...string) SpecOpts {
-	return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
-		if len(devices) == 0 {
-			return nil
-		}
-
-		if err := cdi.Refresh(); err != nil {
-			// We don't consider registry refresh failure a fatal error.
-			// For instance, a dynamically generated invalid CDI Spec file for
-			// any particular vendor shouldn't prevent injection of devices of
-			// different vendors. CDI itself knows better and it will fail the
-			// injection if necessary.
-			log.G(ctx).Warnf("CDI registry refresh failed: %v", err)
-		}
-
-		if _, err := cdi.InjectDevices(s, devices...); err != nil {
-			return fmt.Errorf("CDI device injection failed: %w", err)
-		}
-
-		// One crucial thing to keep in mind is that CDI device injection
-		// might add OCI Spec environment variables, hooks, and mounts as
-		// well. Therefore it is important that none of the corresponding
-		// OCI Spec fields are reset up in the call stack once we return.
-		return nil
-	}
-}
diff --git a/vendor/github.com/containerd/containerd/v2/version/version.go b/vendor/github.com/containerd/containerd/v2/version/version.go
index 28f46ed09..d0749a6cf 100644
--- a/vendor/github.com/containerd/containerd/v2/version/version.go
+++ b/vendor/github.com/containerd/containerd/v2/version/version.go
@@ -23,7 +23,7 @@ var (
 	Package = "github.com/containerd/containerd/v2"
 
 	// Version holds the complete version number. Filled in at linking time.
-	Version = "2.0.2+unknown"
+	Version = "2.0.3+unknown"
 
 	// Revision is filled with the VCS (e.g. git) revision being used to build
 	// the program at linking time.
diff --git a/vendor/github.com/tonistiigi/go-actions-cache/cache_v2.go b/vendor/github.com/tonistiigi/go-actions-cache/cache_v2.go
index adbcda30e..6bf7ec3c0 100644
--- a/vendor/github.com/tonistiigi/go-actions-cache/cache_v2.go
+++ b/vendor/github.com/tonistiigi/go-actions-cache/cache_v2.go
@@ -5,7 +5,10 @@ import (
 	"context"
 	"encoding/json"
 	"io"
+	"time"
 
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
 	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
 	"github.com/pkg/errors"
@@ -45,8 +48,18 @@ func (c *Cache) reserveV2(ctx context.Context, key string) (string, error) {
 	return cr.SignedUploadURL, nil
 }
 
+var azureOptions = &blockblob.ClientOptions{
+	ClientOptions: azcore.ClientOptions{
+		Retry: policy.RetryOptions{
+			MaxRetries:    10,
+			MaxRetryDelay: 2 * time.Minute,
+			RetryDelay:    10 * time.Second,
+		},
+	},
+}
+
 func (c *Cache) uploadV2(ctx context.Context, url string, b Blob) error {
-	client, err := blockblob.NewClientWithNoCredential(url, nil)
+	client, err := blockblob.NewClientWithNoCredential(url, azureOptions)
 	if err != nil {
 		return errors.WithStack(err)
 	}
@@ -62,7 +75,7 @@ func (c *Cache) uploadV2(ctx context.Context, url string, b Blob) error {
 
 func (ce *Entry) downloadV2(ctx context.Context) ReaderAtCloser {
 	return toReaderAtCloser(func(offset int64) (io.ReadCloser, error) {
-		client, err := blockblob.NewClientWithNoCredential(ce.URL, nil)
+		client, err := blockblob.NewClientWithNoCredential(ce.URL, azureOptions)
 		if err != nil {
 			return nil, errors.WithStack(err)
 		}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 1508768c2..7d0d29fe0 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -301,7 +301,7 @@ github.com/containerd/containerd/api/types/runc/options
 github.com/containerd/containerd/api/types/runtimeoptions/v1
 github.com/containerd/containerd/api/types/task
 github.com/containerd/containerd/api/types/transfer
-# github.com/containerd/containerd/v2 v2.0.2
+# github.com/containerd/containerd/v2 v2.0.3
 ## explicit; go 1.22.0
 github.com/containerd/containerd/v2/client
 github.com/containerd/containerd/v2/core/containers
@@ -785,7 +785,7 @@ github.com/tonistiigi/dchapes-mode
 github.com/tonistiigi/fsutil
 github.com/tonistiigi/fsutil/copy
 github.com/tonistiigi/fsutil/types
-# github.com/tonistiigi/go-actions-cache v0.0.0-20250219102945-1a5174abd055
+# github.com/tonistiigi/go-actions-cache v0.0.0-20250228231703-3e9a6642607f
 ## explicit; go 1.22
 github.com/tonistiigi/go-actions-cache
 # github.com/tonistiigi/go-archvariant v1.0.0



Test patch:
diff --git a/cache/remotecache/gha/gha_test.go b/cache/remotecache/gha/gha_test.go
index 6da0e85da..6b80d18f5 100644
--- a/cache/remotecache/gha/gha_test.go
+++ b/cache/remotecache/gha/gha_test.go
@@ -127,6 +127,10 @@ func testBasicGhaCacheImportExportExtraTimeout(t *testing.T, sb integration.Sand
 	}
 	maps.Copy(cacheImportAttrs, cacheAttrs)
 
+	// Github Cache Service v2 has a problem of not being immediately consistent
+	// so we need to wait for the changes to propagate.
+	time.Sleep(3 * time.Second)
+
 	_, err = c.Solve(sb.Context(), def, client.SolveOpt{
 		Exports: []client.ExportEntry{
 			{
diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go
index ce9f3231d..0e18b9b1d 100644
--- a/frontend/dockerfile/dockerfile_test.go
+++ b/frontend/dockerfile/dockerfile_test.go
@@ -159,6 +159,7 @@ var allTests = integration.TestFuncs(
 	testNamedMultiplatformInputContext,
 	testNamedFilteredContext,
 	testEmptyDestDir,
+	testPreserveDestDirSlash,
 	testCopyLinkDotDestDir,
 	testCopyLinkEmptyDestDir,
 	testCopyChownCreateDest,
@@ -562,6 +563,42 @@ RUN cmd /V:on /C "set /p tfcontent=<testfile \
 	require.NoError(t, err)
 }
 
+func testPreserveDestDirSlash(t *testing.T, sb integration.Sandbox) {
+	f := getFrontend(t, sb)
+
+	dockerfile := []byte(integration.UnixOrWindows(
+		`
+FROM busybox
+COPY testfile /sample/
+RUN [ "$(cat /sample/testfile)" == "contents0" ]
+`,
+		`
+FROM nanoserver
+COPY testfile /sample/
+RUN cmd /V:on /C "set /p tfcontent=<\sample\testfile \
+	& if !tfcontent! NEQ contents0 (exit 1)"
+`,
+	))
+
+	dir := integration.Tmpdir(
+		t,
+		fstest.CreateFile("Dockerfile", dockerfile, 0600),
+		fstest.CreateFile("testfile", []byte("contents0"), 0600),
+	)
+
+	c, err := client.New(sb.Context(), sb.Address())
+	require.NoError(t, err)
+	defer c.Close()
+
+	_, err = f.Solve(sb.Context(), c, client.SolveOpt{
+		LocalMounts: map[string]fsutil.FS{
+			dockerui.DefaultLocalNameDockerfile: dir,
+			dockerui.DefaultLocalNameContext:    dir,
+		},
+	}, nil)
+	require.NoError(t, err)
+}
+
 func testCopyLinkDotDestDir(t *testing.T, sb integration.Sandbox) {
 	integration.SkipOnPlatform(t, "windows")
 	f := getFrontend(t, sb)
diff --git a/util/system/path_test.go b/util/system/path_test.go
index 086c63a34..da42472b6 100644
--- a/util/system/path_test.go
+++ b/util/system/path_test.go
@@ -87,8 +87,9 @@ func TestNormalizeWorkdir(t *testing.T) {
 
 // TestCheckSystemDriveAndRemoveDriveLetter tests CheckSystemDriveAndRemoveDriveLetter
 func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
+	keepSlash := false
 	// Fails if not C drive.
-	_, err := CheckSystemDriveAndRemoveDriveLetter(`d:\`, "windows")
+	_, err := CheckSystemDriveAndRemoveDriveLetter(`d:\`, "windows", keepSlash)
 	if err == nil || err.Error() != "The specified path is not on the system drive (C:)" {
 		t.Fatalf("Expected error for d:")
 	}
@@ -96,7 +97,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	var path string
 
 	// Single character is unchanged
-	if path, err = CheckSystemDriveAndRemoveDriveLetter("z", "windows"); err != nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter("z", "windows", keepSlash); err != nil {
 		t.Fatalf("Single character should pass")
 	}
 	if path != "z" {
@@ -104,7 +105,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// Two characters without colon is unchanged
-	if path, err = CheckSystemDriveAndRemoveDriveLetter("AB", "windows"); err != nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter("AB", "windows", keepSlash); err != nil {
 		t.Fatalf("2 characters without colon should pass")
 	}
 	if path != "AB" {
@@ -112,7 +113,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// Abs path without drive letter
-	if path, err = CheckSystemDriveAndRemoveDriveLetter(`\l`, "windows"); err != nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(`\l`, "windows", keepSlash); err != nil {
 		t.Fatalf("abs path no drive letter should pass")
 	}
 	if path != `/l` {
@@ -120,7 +121,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// Abs path without drive letter, linux style
-	if path, err = CheckSystemDriveAndRemoveDriveLetter(`/l`, "windows"); err != nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(`/l`, "windows", keepSlash); err != nil {
 		t.Fatalf("abs path no drive letter linux style should pass")
 	}
 	if path != `/l` {
@@ -128,7 +129,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// Drive-colon should be stripped
-	if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:\`, "windows"); err != nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:\`, "windows", keepSlash); err != nil {
 		t.Fatalf("An absolute path should pass")
 	}
 	if path != `/` {
@@ -136,7 +137,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// Verify with a linux-style path
-	if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:/`, "windows"); err != nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:/`, "windows", keepSlash); err != nil {
 		t.Fatalf("An absolute path should pass")
 	}
 	if path != `/` {
@@ -144,7 +145,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// Failure on c:
-	if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:`, "windows"); err == nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:`, "windows", keepSlash); err == nil {
 		t.Fatalf("c: should fail")
 	}
 	if err.Error() != `No relative path specified in "c:"` {
@@ -152,7 +153,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// Failure on d:
-	if path, err = CheckSystemDriveAndRemoveDriveLetter(`d:`, "windows"); err == nil {
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(`d:`, "windows", keepSlash); err == nil {
 		t.Fatalf("c: should fail")
 	}
 	if err.Error() != `No relative path specified in "d:"` {
@@ -160,9 +161,45 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) {
 	}
 
 	// UNC path should fail.
-	if _, err = CheckSystemDriveAndRemoveDriveLetter(`\\.\C$\test`, "windows"); err == nil {
+	if _, err = CheckSystemDriveAndRemoveDriveLetter(`\\.\C$\test`, "windows", keepSlash); err == nil {
 		t.Fatalf("UNC path should fail")
 	}
+
+	// also testing for keepSlash = true
+	keepSlash = true
+	origPath := "\\a\\b\\..\\c\\"
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(origPath, "windows", keepSlash); err != nil {
+		t.Fatalf("windows relative paths should be cleaned and should pass")
+	}
+	// When input OS is Windows, the path should be properly cleaned
+	if path != "/a/c/" {
+		t.Fatalf("Path was not cleaned successfully")
+	}
+
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(origPath, "windows", false); err != nil {
+		t.Fatalf("windows relative paths should be cleaned and should pass [keepSlash = false]")
+	}
+	// When input OS is Windows, the path should be properly cleaned
+	if path != "/a/c" {
+		t.Fatalf("Path was not cleaned successfully [keepSlash = false]")
+	}
+
+	// windows-style relative paths on linux
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(origPath, "linux", keepSlash); err != nil {
+		t.Fatalf("windows style relative paths should be considered a valid path element in linux and should pass")
+	}
+	// When input OS is Linux, this is a valid path element name.
+	if path != "\\a\\b\\..\\c\\" {
+		t.Fatalf("Path was not cleaned successfully")
+	}
+
+	if path, err = CheckSystemDriveAndRemoveDriveLetter(origPath, "linux", false); err != nil {
+		t.Fatalf("windows style relative paths should be considered a valid path element in linux and should pass")
+	}
+	// When input OS is Linux, this is a valid path element name.
+	if path != "\\a\\b\\..\\c\\" {
+		t.Fatalf("Path was not cleaned successfully [keepSlash = false]")
+	}
 }
 
 // TestNormalizeWorkdirWindows tests NormalizeWorkdir
diff --git a/util/testutil/integration/run.go b/util/testutil/integration/run.go
index 2c0810291..7dd1e2262 100644
--- a/util/testutil/integration/run.go
+++ b/util/testutil/integration/run.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"fmt"
 	"maps"
+	"math"
 	"math/rand"
 	"os"
 	"os/exec"
@@ -12,6 +13,7 @@ import (
 	"reflect"
 	"runtime"
 	"sort"
+	"strconv"
 	"strings"
 	"sync"
 	"testing"
@@ -161,6 +163,24 @@ func Run(t *testing.T, testCases []Test, opt ...TestOpt) {
 		t.Skip("skipping integration tests")
 	}
 
+	var sliceSplit int
+	v, ok := os.LookupEnv("TESTSLICE")
+	if ok {
+		t.Logf("TESTSLICE=%s", v)
+		offsetS, totalS, ok := strings.Cut(v, "-")
+		if !ok {
+			t.Fatalf("invalid TESTSLICE=%s", v)
+		}
+		offset, err := strconv.Atoi(offsetS)
+		require.NoError(t, err)
+		total, err := strconv.Atoi(totalS)
+		require.NoError(t, err)
+		if offset < 1 || total < 1 || offset > total {
+			t.Fatalf("invalid TESTSLICE=%s", v)
+		}
+		sliceSplit = total
+	}
+
 	var tc testConf
 	for _, o := range opt {
 		o(&tc)
@@ -182,9 +202,14 @@ func Run(t *testing.T, testCases []Test, opt ...TestOpt) {
 	})
 
 	for _, br := range list {
-		for _, tc := range testCases {
+		for i, tc := range testCases {
 			for _, mv := range matrix {
 				fn := tc.Name()
+				if sliceSplit > 0 {
+					pageLimit := int(math.Ceil(float64(len(testCases)) / float64(sliceSplit)))
+					sliceName := fmt.Sprintf("slice=%d-%d/", i/pageLimit+1, sliceSplit)
+					fn = sliceName + fn
+				}
 				name := fn + "/worker=" + br.Name() + mv.functionSuffix()
 				func(fn, testName string, br Worker, tc Test, mv matrixValue) {
 					ok := t.Run(testName, func(t *testing.T) {