Skip to content

Commit dbfb610

Browse files
tonistiigijedevc
authored andcommitted
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]> (cherry picked from commit 1bbf73e) Signed-off-by: Justin Chadwell <[email protected]>
1 parent f94ed7c commit dbfb610

File tree

3 files changed

+197
-2
lines changed

3 files changed

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

frontend/dockerfile/dockerfile_test.go

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

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

0 commit comments

Comments
 (0)