Skip to content

Commit

Permalink
llb: avoid duplicate instances of sourcemaps in provenance
Browse files Browse the repository at this point in the history
If build contains multiple subbuilds all of their sources
are tracked in provenance attestations. When some subbuilds
are coming from same source file (eg. same Dockerfile but
different targets) currently the same file would appear
in multiple times. This detects such duplicates and makes
sure definitions from multiple subbuilds can map to same file.

Signed-off-by: Tonis Tiigi <[email protected]>
  • Loading branch information
tonistiigi committed Aug 31, 2023
1 parent 05eb728 commit 1bbf73e
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 2 deletions.
42 changes: 40 additions & 2 deletions client/llb/sourcemap.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package llb

import (
"bytes"
"context"

"github.com/moby/buildkit/solver/pb"
Expand Down Expand Up @@ -47,6 +48,33 @@ func (s *SourceMap) Location(r []*pb.Range) ConstraintsOpt {
})
}

func equalSourceMap(sm1, sm2 *SourceMap) (out bool) {
if sm1 == nil || sm2 == nil {
return false
}
if sm1.Filename != sm2.Filename {
return false
}
if sm1.Language != sm2.Language {
return false
}
if len(sm1.Data) != len(sm2.Data) {
return false
}
if !bytes.Equal(sm1.Data, sm2.Data) {
return false
}
if sm1.Definition != nil && sm2.Definition != nil {
if len(sm1.Definition.Def) != len(sm2.Definition.Def) && len(sm1.Definition.Def) != 0 {
return false
}
if !bytes.Equal(sm1.Definition.Def[len(sm1.Definition.Def)-1], sm2.Definition.Def[len(sm2.Definition.Def)-1]) {
return false
}
}
return true
}

type SourceLocation struct {
SourceMap *SourceMap
Ranges []*pb.Range
Expand All @@ -69,8 +97,18 @@ func (smc *sourceMapCollector) Add(dgst digest.Digest, ls []*SourceLocation) {
for _, l := range ls {
idx, ok := smc.index[l.SourceMap]
if !ok {
idx = len(smc.maps)
smc.maps = append(smc.maps, l.SourceMap)
idx = -1
// slow equality check
for i, m := range smc.maps {
if equalSourceMap(m, l.SourceMap) {
idx = i
break
}
}
if idx == -1 {
idx = len(smc.maps)
smc.maps = append(smc.maps, l.SourceMap)
}
}
smc.index[l.SourceMap] = idx
}
Expand Down
155 changes: 155 additions & 0 deletions frontend/dockerfile/dockerfile_provenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -16,16 +17,20 @@ import (

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/content/local"
"github.com/containerd/containerd/content/proxy"
"github.com/containerd/containerd/platforms"
"github.com/containerd/continuity/fs/fstest"
intoto "github.com/in-toto/in-toto-golang/in_toto"
provenanceCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/dockerui"
gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/solver/llbsolver/provenance"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/testutil"
"github.com/moby/buildkit/util/testutil/integration"
Expand Down Expand Up @@ -1113,3 +1118,153 @@ func testDockerIgnoreMissingProvenance(t *testing.T, sb integration.Sandbox) {
}, "", frontend, nil)
require.NoError(t, err)
}

func testFrontendDeduplicateSources(t *testing.T, sb integration.Sandbox) {
ctx := sb.Context()

c, err := client.New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()

dockerfile := []byte(`
FROM scratch as base
COPY foo foo2
FROM linked
COPY bar bar2
`)

dir := integration.Tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("data"), 0600),
fstest.CreateFile("bar", []byte("data2"), 0600),
)

f := getFrontend(t, sb)

b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{
FrontendOpt: map[string]string{
"target": "base",
},
})
if err != nil {
return nil, err
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
st, err := ref.ToState()
if err != nil {
return nil, err
}

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

dt, ok := res.Metadata["containerimage.config"]
if !ok {
return nil, errors.Errorf("no containerimage.config in metadata")
}

dt, err = json.Marshal(map[string][]byte{
"containerimage.config": dt,
})
if err != nil {
return nil, err
}

res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{
FrontendOpt: map[string]string{
"context:linked": "input:baseinput",
"input-metadata:linked": string(dt),
},
FrontendInputs: map[string]*pb.Definition{
"baseinput": def.ToPB(),
},
})
if err != nil {
return nil, err
}
return res, nil
}

product := "buildkit_test"

destDir := t.TempDir()

ref := identity.NewID()

_, err = c.Build(ctx, client.SolveOpt{
LocalDirs: map[string]string{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: destDir,
},
},
Ref: ref,
}, product, b, nil)
require.NoError(t, err)

dt, err := os.ReadFile(filepath.Join(destDir, "foo2"))
require.NoError(t, err)
require.Equal(t, "data", string(dt))

dt, err = os.ReadFile(filepath.Join(destDir, "bar2"))
require.NoError(t, err)
require.Equal(t, "data2", string(dt))

history, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
Ref: ref,
EarlyExit: true,
})
require.NoError(t, err)

store := proxy.NewContentStore(c.ContentClient())

var provDt []byte
for {
ev, err := history.Recv()
if err != nil {
require.Equal(t, io.EOF, err)
break
}
require.Equal(t, ref, ev.Record.Ref)

for _, prov := range ev.Record.Result.Attestations {
if len(prov.Annotations) == 0 || prov.Annotations["in-toto.io/predicate-type"] != "https://slsa.dev/provenance/v0.2" {
t.Logf("skipping non-slsa provenance: %s", prov.MediaType)
continue
}

provDt, err = content.ReadBlob(ctx, store, ocispecs.Descriptor{
MediaType: prov.MediaType,
Digest: prov.Digest,
Size: prov.Size_,
})
require.NoError(t, err)
}
}

require.NotEqual(t, len(provDt), 0)

var pred provenance.ProvenancePredicate
require.NoError(t, json.Unmarshal(provDt, &pred))

sources := pred.Metadata.BuildKitMetadata.Source.Infos

require.Equal(t, 1, len(sources))
require.Equal(t, "Dockerfile", sources[0].Filename)
require.Equal(t, "Dockerfile", sources[0].Language)

require.Equal(t, dockerfile, sources[0].Data)
require.NotEqual(t, 0, len(sources[0].Definition))
}
1 change: 1 addition & 0 deletions frontend/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ var allTests = integration.TestFuncs(
testMultiPlatformWarnings,
testNilContextInSolveGateway,
testCopyUnicodePath,
testFrontendDeduplicateSources,
)

// Tests that depend on the `security.*` entitlements
Expand Down

0 comments on commit 1bbf73e

Please sign in to comment.