Skip to content

Commit 1bbf73e

Browse files
committed
llb: avoid duplicate instances of sourcemaps in provenance
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]>
1 parent 05eb728 commit 1bbf73e

File tree

3 files changed

+196
-2
lines changed

3 files changed

+196
-2
lines changed

client/llb/sourcemap.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package llb
22

33
import (
4+
"bytes"
45
"context"
56

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

51+
func equalSourceMap(sm1, sm2 *SourceMap) (out bool) {
52+
if sm1 == nil || sm2 == nil {
53+
return false
54+
}
55+
if sm1.Filename != sm2.Filename {
56+
return false
57+
}
58+
if sm1.Language != sm2.Language {
59+
return false
60+
}
61+
if len(sm1.Data) != len(sm2.Data) {
62+
return false
63+
}
64+
if !bytes.Equal(sm1.Data, sm2.Data) {
65+
return false
66+
}
67+
if sm1.Definition != nil && sm2.Definition != nil {
68+
if len(sm1.Definition.Def) != len(sm2.Definition.Def) && len(sm1.Definition.Def) != 0 {
69+
return false
70+
}
71+
if !bytes.Equal(sm1.Definition.Def[len(sm1.Definition.Def)-1], sm2.Definition.Def[len(sm2.Definition.Def)-1]) {
72+
return false
73+
}
74+
}
75+
return true
76+
}
77+
5078
type SourceLocation struct {
5179
SourceMap *SourceMap
5280
Ranges []*pb.Range
@@ -69,8 +97,18 @@ func (smc *sourceMapCollector) Add(dgst digest.Digest, ls []*SourceLocation) {
6997
for _, l := range ls {
7098
idx, ok := smc.index[l.SourceMap]
7199
if !ok {
72-
idx = len(smc.maps)
73-
smc.maps = append(smc.maps, l.SourceMap)
100+
idx = -1
101+
// slow equality check
102+
for i, m := range smc.maps {
103+
if equalSourceMap(m, l.SourceMap) {
104+
idx = i
105+
break
106+
}
107+
}
108+
if idx == -1 {
109+
idx = len(smc.maps)
110+
smc.maps = append(smc.maps, l.SourceMap)
111+
}
74112
}
75113
smc.index[l.SourceMap] = idx
76114
}

frontend/dockerfile/dockerfile_provenance_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/http"
89
"net/http/httptest"
910
"net/url"
@@ -16,16 +17,20 @@ import (
1617

1718
"github.com/containerd/containerd/content"
1819
"github.com/containerd/containerd/content/local"
20+
"github.com/containerd/containerd/content/proxy"
1921
"github.com/containerd/containerd/platforms"
2022
"github.com/containerd/continuity/fs/fstest"
2123
intoto "github.com/in-toto/in-toto-golang/in_toto"
2224
provenanceCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
25+
controlapi "github.com/moby/buildkit/api/services/control"
2326
"github.com/moby/buildkit/client"
2427
"github.com/moby/buildkit/client/llb"
2528
"github.com/moby/buildkit/exporter/containerimage/exptypes"
2629
"github.com/moby/buildkit/frontend/dockerui"
2730
gateway "github.com/moby/buildkit/frontend/gateway/client"
31+
"github.com/moby/buildkit/identity"
2832
"github.com/moby/buildkit/solver/llbsolver/provenance"
33+
"github.com/moby/buildkit/solver/pb"
2934
"github.com/moby/buildkit/util/contentutil"
3035
"github.com/moby/buildkit/util/testutil"
3136
"github.com/moby/buildkit/util/testutil/integration"
@@ -1113,3 +1118,153 @@ func testDockerIgnoreMissingProvenance(t *testing.T, sb integration.Sandbox) {
11131118
}, "", frontend, nil)
11141119
require.NoError(t, err)
11151120
}
1121+
1122+
func testFrontendDeduplicateSources(t *testing.T, sb integration.Sandbox) {
1123+
ctx := sb.Context()
1124+
1125+
c, err := client.New(ctx, sb.Address())
1126+
require.NoError(t, err)
1127+
defer c.Close()
1128+
1129+
dockerfile := []byte(`
1130+
FROM scratch as base
1131+
COPY foo foo2
1132+
1133+
FROM linked
1134+
COPY bar bar2
1135+
`)
1136+
1137+
dir := integration.Tmpdir(
1138+
t,
1139+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
1140+
fstest.CreateFile("foo", []byte("data"), 0600),
1141+
fstest.CreateFile("bar", []byte("data2"), 0600),
1142+
)
1143+
1144+
f := getFrontend(t, sb)
1145+
1146+
b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
1147+
res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{
1148+
FrontendOpt: map[string]string{
1149+
"target": "base",
1150+
},
1151+
})
1152+
if err != nil {
1153+
return nil, err
1154+
}
1155+
ref, err := res.SingleRef()
1156+
if err != nil {
1157+
return nil, err
1158+
}
1159+
st, err := ref.ToState()
1160+
if err != nil {
1161+
return nil, err
1162+
}
1163+
1164+
def, err := st.Marshal(ctx)
1165+
if err != nil {
1166+
return nil, err
1167+
}
1168+
1169+
dt, ok := res.Metadata["containerimage.config"]
1170+
if !ok {
1171+
return nil, errors.Errorf("no containerimage.config in metadata")
1172+
}
1173+
1174+
dt, err = json.Marshal(map[string][]byte{
1175+
"containerimage.config": dt,
1176+
})
1177+
if err != nil {
1178+
return nil, err
1179+
}
1180+
1181+
res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{
1182+
FrontendOpt: map[string]string{
1183+
"context:linked": "input:baseinput",
1184+
"input-metadata:linked": string(dt),
1185+
},
1186+
FrontendInputs: map[string]*pb.Definition{
1187+
"baseinput": def.ToPB(),
1188+
},
1189+
})
1190+
if err != nil {
1191+
return nil, err
1192+
}
1193+
return res, nil
1194+
}
1195+
1196+
product := "buildkit_test"
1197+
1198+
destDir := t.TempDir()
1199+
1200+
ref := identity.NewID()
1201+
1202+
_, err = c.Build(ctx, client.SolveOpt{
1203+
LocalDirs: map[string]string{
1204+
dockerui.DefaultLocalNameDockerfile: dir,
1205+
dockerui.DefaultLocalNameContext: dir,
1206+
},
1207+
Exports: []client.ExportEntry{
1208+
{
1209+
Type: client.ExporterLocal,
1210+
OutputDir: destDir,
1211+
},
1212+
},
1213+
Ref: ref,
1214+
}, product, b, nil)
1215+
require.NoError(t, err)
1216+
1217+
dt, err := os.ReadFile(filepath.Join(destDir, "foo2"))
1218+
require.NoError(t, err)
1219+
require.Equal(t, "data", string(dt))
1220+
1221+
dt, err = os.ReadFile(filepath.Join(destDir, "bar2"))
1222+
require.NoError(t, err)
1223+
require.Equal(t, "data2", string(dt))
1224+
1225+
history, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
1226+
Ref: ref,
1227+
EarlyExit: true,
1228+
})
1229+
require.NoError(t, err)
1230+
1231+
store := proxy.NewContentStore(c.ContentClient())
1232+
1233+
var provDt []byte
1234+
for {
1235+
ev, err := history.Recv()
1236+
if err != nil {
1237+
require.Equal(t, io.EOF, err)
1238+
break
1239+
}
1240+
require.Equal(t, ref, ev.Record.Ref)
1241+
1242+
for _, prov := range ev.Record.Result.Attestations {
1243+
if len(prov.Annotations) == 0 || prov.Annotations["in-toto.io/predicate-type"] != "https://slsa.dev/provenance/v0.2" {
1244+
t.Logf("skipping non-slsa provenance: %s", prov.MediaType)
1245+
continue
1246+
}
1247+
1248+
provDt, err = content.ReadBlob(ctx, store, ocispecs.Descriptor{
1249+
MediaType: prov.MediaType,
1250+
Digest: prov.Digest,
1251+
Size: prov.Size_,
1252+
})
1253+
require.NoError(t, err)
1254+
}
1255+
}
1256+
1257+
require.NotEqual(t, len(provDt), 0)
1258+
1259+
var pred provenance.ProvenancePredicate
1260+
require.NoError(t, json.Unmarshal(provDt, &pred))
1261+
1262+
sources := pred.Metadata.BuildKitMetadata.Source.Infos
1263+
1264+
require.Equal(t, 1, len(sources))
1265+
require.Equal(t, "Dockerfile", sources[0].Filename)
1266+
require.Equal(t, "Dockerfile", sources[0].Language)
1267+
1268+
require.Equal(t, dockerfile, sources[0].Data)
1269+
require.NotEqual(t, 0, len(sources[0].Definition))
1270+
}

frontend/dockerfile/dockerfile_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ var allTests = integration.TestFuncs(
170170
testMultiPlatformWarnings,
171171
testNilContextInSolveGateway,
172172
testCopyUnicodePath,
173+
testFrontendDeduplicateSources,
173174
)
174175

175176
// Tests that depend on the `security.*` entitlements

0 commit comments

Comments
 (0)