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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions client/client_export_local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"

Expand Down Expand Up @@ -443,6 +444,105 @@ func testExportLocalNoPlatformSplitOverwrite(t *testing.T, sb integration.Sandbo
require.ErrorContains(t, err, "when split option is disabled")
}

func testExportTarPlatformIDSanitized(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform)
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

const platformID = `..\buildkit-outside`
platform := platforms.DefaultSpec()

frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
st := llb.Scratch().File(
llb.Mkfile("payload.txt", 0600, []byte("payload")),
)

def, err := st.Marshal(ctx)
if err != nil {
return nil, err
}

r, err := c.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}

ref, err := r.SingleRef()
if err != nil {
return nil, err
}

res := gateway.NewResult()
res.AddRef(platformID, ref)

dt, err := json.Marshal(&exptypes.Platforms{
Platforms: []exptypes.Platform{{
ID: platformID,
Platform: platform,
}},
})
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterPlatformsKey, dt)

return res, nil
}

outW := bytes.NewBuffer(nil)
_, err = c.Build(sb.Context(), SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterTar,
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW}),
},
},
}, "", frontend, nil)
require.NoError(t, err)

m, err := testutil.ReadTarToMap(outW.Bytes(), false)
require.NoError(t, err)

for name := range m {
require.Falsef(t, strings.HasPrefix(name, "../") ||
strings.HasPrefix(name, `..\`) ||
strings.Contains(name, `/../`) ||
strings.Contains(name, `\..\`) ||
strings.Contains(name, `\`) ||
strings.Contains(name, ":"),
"tar exporter emitted unsafe platform path %q", name)
}

tarPaths := make([]string, 0, len(m))
for name := range m {
tarPaths = append(tarPaths, name)
}
sort.Strings(tarPaths)

const platformDir = ".._buildkit-outside"
payloadPath := ""
for name, item := range m {
// The tar can contain directory entries and unrelated files; this test
// only verifies that the payload is exported under the sanitized root.
if item.Header.Typeflag != tar.TypeReg {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These skips at least need comments describing what cases they are handling and why we have files in the tar that we can't strictly verify.

continue
}
if !strings.HasPrefix(name, platformDir+"/") {
continue
}
if path.Base(name) != "payload.txt" {
continue
}
payloadPath = name
require.Equal(t, "payload", string(item.Data))
break
}
require.NotEmptyf(t, payloadPath, "expected payload under sanitized tar path %q in %v", platformDir, tarPaths)
}

func testExporterTargetExists(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
c, err := New(sb.Context(), sb.Address())
Expand Down
1 change: 1 addition & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testExportLocalModeDeleteMultiPlatformKeepsAllPlatforms,
testExportLocalNoPlatformSplit,
testExportLocalNoPlatformSplitOverwrite,
testExportTarPlatformIDSanitized,
testExporterTargetExists,
testMultipleExporters,
testSessionExporter,
Expand Down
3 changes: 1 addition & 2 deletions exporter/local/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package local
import (
"context"
"os"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -127,7 +126,7 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
platformDirStat := func(k string, opt CreateFSOpts) *fstypes.Stat {
st := &fstypes.Stat{
Mode: uint32(os.ModeDir | 0755),
Path: strings.ReplaceAll(k, "/", "_"),
Path: PlatformIDToPath(k),
}
if opt.Epoch != nil && opt.Epoch.Value != nil {
st.ModTime = opt.Epoch.Value.UnixNano()
Expand Down
2 changes: 1 addition & 1 deletion exporter/local/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func CreateFS(ctx context.Context, sessionID string, k string, ref cache.Immutab
if addPlatformToFilename {
nameExt := path.Ext(name)
namBase := strings.TrimSuffix(name, nameExt)
name = fmt.Sprintf("%s.%s%s", namBase, strings.ReplaceAll(k, "/", "_"), nameExt)
name = fmt.Sprintf("%s.%s%s", namBase, PlatformIDToPath(k), nameExt)
}
if _, ok := names[name]; ok {
return nil, nil, errors.Errorf("duplicate attestation path name %s", name)
Expand Down
14 changes: 14 additions & 0 deletions exporter/local/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package local

import "strings"

// PlatformIDToPath maps an exporter platform ID to a single path component.
func PlatformIDToPath(id string) string {
id = strings.NewReplacer("/", "_", `\`, "_", ":", "_").Replace(id)
switch id {
case ".", "..":
return strings.Repeat("_", len(id))
default:
return id
}
}
50 changes: 50 additions & 0 deletions exporter/local/platform_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package local

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestPlatformIDToPath(t *testing.T) {
for _, tc := range []struct {
name string
id string
want string
}{
{
name: "slashes",
id: "linux/amd64",
want: "linux_amd64",
},
{
name: "windows separators",
id: `..\buildkit-outside`,
want: ".._buildkit-outside",
},
{
name: "drive separator",
id: `windows/amd64:C`,
want: "windows_amd64_C",
},
{
name: "dot",
id: ".",
want: "_",
},
{
name: "dot dot",
id: "..",
want: "__",
},
{
name: "embedded dots",
id: "linux.v2/amd64",
want: "linux.v2_amd64",
},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, PlatformIDToPath(tc.id))
})
}
}
3 changes: 1 addition & 2 deletions exporter/tar/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"os"
"slices"
"strings"
"time"

"github.com/moby/buildkit/cache"
Expand Down Expand Up @@ -109,7 +108,7 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source

st := &fstypes.Stat{
Mode: uint32(os.ModeDir | 0755),
Path: strings.ReplaceAll(k, "/", "_"),
Path: local.PlatformIDToPath(k),
}
if opt.Epoch != nil && opt.Epoch.Value != nil {
st.ModTime = opt.Epoch.Value.UnixNano()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ require (
github.com/spdx/tools-golang v0.5.7
github.com/stretchr/testify v1.12.1
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323
github.com/tonistiigi/fsutil v0.0.0-20260717003753-6d9dc2ebad62
github.com/tonistiigi/fsutil v0.0.0-20260818132828-ea1b24afbd39
github.com/tonistiigi/go-actions-cache v0.0.0-20260120203934-54bc28c26fd2
github.com/tonistiigi/go-archvariant v1.0.0
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -584,8 +584,8 @@ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20260717003753-6d9dc2ebad62 h1:uppBiK+tE8tYG6fc0N8VnsC7FMZcWxnXIyaQ9GcUIU8=
github.com/tonistiigi/fsutil v0.0.0-20260717003753-6d9dc2ebad62/go.mod h1:K5zrLch9UaSGNiek5XHZeqZUf1zPWJHqDfLIcnpquQ4=
github.com/tonistiigi/fsutil v0.0.0-20260818132828-ea1b24afbd39 h1:g12NNHiPRetQ/siMoRWPGhYnAmfoln1ERSal11ll7Pg=
github.com/tonistiigi/fsutil v0.0.0-20260818132828-ea1b24afbd39/go.mod h1:K5zrLch9UaSGNiek5XHZeqZUf1zPWJHqDfLIcnpquQ4=
github.com/tonistiigi/go-actions-cache v0.0.0-20260120203934-54bc28c26fd2 h1:5p6hffZeB25G4rhBc3HU6x1aIlyDELfib+/Omq+ZfQA=
github.com/tonistiigi/go-actions-cache v0.0.0-20260120203934-54bc28c26fd2/go.mod h1:cD0SB2270BYw6HYKriFn4H6NRLhGj6ytf48YTpsm8LY=
github.com/tonistiigi/go-archvariant v1.0.0 h1:5LC1eDWiBNflnTF1prCiX09yfNHIxDC/aukdhCdTyb0=
Expand Down
3 changes: 2 additions & 1 deletion vendor/github.com/tonistiigi/fsutil/tarwriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ github.com/theupdateframework/go-tuf/v2/metadata/updater
# github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323
## explicit; go 1.21
github.com/tonistiigi/dchapes-mode
# github.com/tonistiigi/fsutil v0.0.0-20260717003753-6d9dc2ebad62
# github.com/tonistiigi/fsutil v0.0.0-20260818132828-ea1b24afbd39
## explicit; go 1.25.0
github.com/tonistiigi/fsutil
github.com/tonistiigi/fsutil/copy
Expand Down