Skip to content

Commit

Permalink
imageutil: validate source on resolving existing blob by digest
Browse files Browse the repository at this point in the history
Signed-off-by: Tonis Tiigi <[email protected]>
(cherry picked from commit 04fc13480d8ef253e0c3cc4c1f889455aefbdcf7)
  • Loading branch information
tonistiigi committed Oct 17, 2022
1 parent 3a1eeca commit 450155e
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 3 deletions.
68 changes: 68 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func TestIntegration(t *testing.T) {
testStargzLazyPull,
testFileOpInputSwap,
testRelativeMountpoint,
testValidateDigestOrigin,
}, mirrors)

integration.Run(t, []integration.Test{
Expand Down Expand Up @@ -3485,6 +3486,73 @@ func testRelativeMountpoint(t *testing.T, sb integration.Sandbox) {
require.Equal(t, dt, []byte(id))
}

func testValidateDigestOrigin(t *testing.T, sb integration.Sandbox) {
ctx := context.TODO()

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

st := llb.Image("busybox:latest").Run(llb.Shlex("touch foo"), llb.Dir("/wd")).AddMount("/wd", llb.Scratch())

def, err := st.Marshal(ctx)
require.NoError(t, err)

registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrorRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)

target := registry + "/buildkit/testdigest:latest"

resp, err := c.Solve(ctx, def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterImage,
Attrs: map[string]string{
"name": target,
"push": "true",
},
},
},
}, nil)
require.NoError(t, err)

dgst, ok := resp.ExporterResponse["containerimage.digest"]
require.True(t, ok)

err = c.Prune(ctx, nil, PruneAll)
require.NoError(t, err)

st = llb.Image(target + "@" + dgst)

def, err = st.Marshal(ctx)
require.NoError(t, err)

_, err = c.Solve(ctx, def, SolveOpt{}, nil)
require.NoError(t, err)

// accessing the digest from invalid names should fail
st = llb.Image("example.invalid/nosuchrepo@" + dgst)

def, err = st.Marshal(ctx)
require.NoError(t, err)

_, err = c.Solve(ctx, def, SolveOpt{}, nil)
require.Error(t, err)

// also check repo that does exists but not digest
st = llb.Image("docker.io/library/ubuntu@" + dgst)

def, err = st.Marshal(ctx)
require.NoError(t, err)

_, err = c.Solve(ctx, def, SolveOpt{}, nil)
require.Error(t, err)
}

func tmpdir(appliers ...fstest.Applier) (string, error) {
tmpdir, err := ioutil.TempDir("", "buildkit-client")
if err != nil {
Expand Down
54 changes: 54 additions & 0 deletions util/contentutil/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"io/ioutil"
"strings"
"sync"
"time"

Expand All @@ -18,22 +19,74 @@ import (
type Buffer interface {
content.Provider
content.Ingester
content.Manager
}

// NewBuffer returns a new buffer
func NewBuffer() Buffer {
return &buffer{
buffers: map[digest.Digest][]byte{},
infos: map[digest.Digest]content.Info{},
refs: map[string]struct{}{},
}
}

type buffer struct {
mu sync.Mutex
buffers map[digest.Digest][]byte
infos map[digest.Digest]content.Info
refs map[string]struct{}
}

func (b *buffer) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
b.mu.Lock()
v, ok := b.infos[dgst]
b.mu.Unlock()
if !ok {
return content.Info{}, errdefs.ErrNotFound
}
return v, nil
}

func (b *buffer) Update(ctx context.Context, new content.Info, fieldpaths ...string) (content.Info, error) {
b.mu.Lock()
defer b.mu.Unlock()

updated, ok := b.infos[new.Digest]
if !ok {
return content.Info{}, errdefs.ErrNotFound
}

if len(fieldpaths) == 0 {
fieldpaths = []string{"labels"}
}

for _, path := range fieldpaths {
if strings.HasPrefix(path, "labels.") {
if updated.Labels == nil {
updated.Labels = map[string]string{}
}
key := strings.TrimPrefix(path, "labels.")
updated.Labels[key] = new.Labels[key]
continue
}
if path == "labels" {
updated.Labels = new.Labels
}
}

b.infos[new.Digest] = updated
return updated, nil
}

func (b *buffer) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
return nil // not implemented
}

func (b *buffer) Delete(ctx context.Context, dgst digest.Digest) error {
return nil // not implemented
}

func (b *buffer) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
var wOpts content.WriterOpts
for _, opt := range opts {
Expand Down Expand Up @@ -82,6 +135,7 @@ func (b *buffer) addValue(k digest.Digest, dt []byte) {
b.mu.Lock()
defer b.mu.Unlock()
b.buffers[k] = dt
b.infos[k] = content.Info{Digest: k, Size: int64(len(dt))}
}

type bufferedWriter struct {
Expand Down
47 changes: 47 additions & 0 deletions util/contentutil/buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/remotes/docker"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
Expand Down Expand Up @@ -71,3 +72,49 @@ func TestReaderAt(t *testing.T) {
require.Equal(t, err, io.EOF)
require.Equal(t, "bar", string(buf[:n]))
}

func TestLabels(t *testing.T) {
t.Parallel()
ctx := context.TODO()

b := NewBuffer()

err := content.WriteBlob(ctx, b, "foo", bytes.NewBuffer([]byte("foobar")), ocispec.Descriptor{Size: -1})
require.NoError(t, err)

_, err = b.Info(ctx, digest.FromBytes([]byte("abc")))
require.Error(t, err)

info, err := b.Info(ctx, digest.FromBytes([]byte("foobar")))
require.NoError(t, err)

require.Equal(t, info.Digest, digest.FromBytes([]byte("foobar")))

hf, err := docker.AppendDistributionSourceLabel(b, "docker.io/library/busybox:latest")
require.NoError(t, err)
_, err = hf.Handle(ctx, ocispec.Descriptor{Digest: digest.FromBytes([]byte("foobar"))})
require.NoError(t, err)

info, err = b.Info(ctx, digest.FromBytes([]byte("foobar")))
require.NoError(t, err)
require.Equal(t, info.Digest, digest.FromBytes([]byte("foobar")))

require.Equal(t, "library/busybox", info.Labels["containerd.io/distribution.source.docker.io"])

hf, err = docker.AppendDistributionSourceLabel(b, "docker.io/library/alpine:3.15")
require.NoError(t, err)
_, err = hf.Handle(ctx, ocispec.Descriptor{Digest: digest.FromBytes([]byte("foobar"))})
require.NoError(t, err)

hf, err = docker.AppendDistributionSourceLabel(b, "ghcr.io/repos/alpine:3.11")
require.NoError(t, err)
_, err = hf.Handle(ctx, ocispec.Descriptor{Digest: digest.FromBytes([]byte("foobar"))})
require.NoError(t, err)

info, err = b.Info(ctx, digest.FromBytes([]byte("foobar")))
require.NoError(t, err)
require.Equal(t, info.Digest, digest.FromBytes([]byte("foobar")))

require.Equal(t, "library/alpine,library/busybox", info.Labels["containerd.io/distribution.source.docker.io"])
require.Equal(t, "repos/alpine", info.Labels["containerd.io/distribution.source.ghcr.io"])
}
34 changes: 34 additions & 0 deletions util/contentutil/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package contentutil

import (
"net/url"
"strings"

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/reference"
)

func HasSource(info content.Info, refspec reference.Spec) (bool, error) {
u, err := url.Parse("dummy://" + refspec.Locator)
if err != nil {
return false, err
}

if info.Labels == nil {
return false, nil
}

source, target := u.Hostname(), strings.TrimPrefix(u.Path, "/")
repoLabel, ok := info.Labels["containerd.io/distribution.source."+source]
if !ok || repoLabel == "" {
return false, nil
}

for _, repo := range strings.Split(repoLabel, ",") {
// the target repo is not a candidate
if repo == target {
return true, nil
}
}
return false, nil
}
57 changes: 57 additions & 0 deletions util/contentutil/source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package contentutil

import (
"testing"

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/reference"
"github.com/stretchr/testify/require"
)

func TestHasSource(t *testing.T) {
info := content.Info{
Labels: map[string]string{
"containerd.io/distribution.source.docker.io": "library/alpine",
},
}
ref, err := reference.Parse("docker.io/library/alpine:latest")
require.NoError(t, err)
b, err := HasSource(info, ref)
require.NoError(t, err)
require.True(t, b)

info = content.Info{
Labels: map[string]string{
"containerd.io/distribution.source.docker.io": "library/alpine,library/ubuntu",
},
}
b, err = HasSource(info, ref)
require.NoError(t, err)
require.True(t, b)

info = content.Info{}
b, err = HasSource(info, ref)
require.NoError(t, err)
require.False(t, b)

info = content.Info{Labels: map[string]string{}}
b, err = HasSource(info, ref)
require.NoError(t, err)
require.False(t, b)

info = content.Info{
Labels: map[string]string{
"containerd.io/distribution.source.docker.io": "library/ubuntu",
},
}
b, err = HasSource(info, ref)
require.NoError(t, err)
require.False(t, b)

info = content.Info{Labels: map[string]string{
"containerd.io/distribution.source.ghcr.io": "library/alpine",
}}
b, err = HasSource(info, ref)
require.NoError(t, err)
require.False(t, b)
}
19 changes: 16 additions & 3 deletions util/imageutil/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/containerd/containerd/reference"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/leaseutil"
"github.com/moby/buildkit/util/resolver/retryhandler"
digest "github.com/opencontainers/go-digest"
Expand All @@ -23,6 +24,7 @@ import (
type ContentCache interface {
content.Ingester
content.Provider
content.Manager
}

var leasesMu sync.Mutex
Expand Down Expand Up @@ -74,10 +76,15 @@ func Config(ctx context.Context, str string, resolver remotes.Resolver, cache Co
if desc.Digest != "" {
ra, err := cache.ReaderAt(ctx, desc)
if err == nil {
desc.Size = ra.Size()
mt, err := DetectManifestMediaType(ra)
info, err := cache.Info(ctx, desc.Digest)
if err == nil {
desc.MediaType = mt
if ok, err := contentutil.HasSource(info, ref); err == nil && ok {
desc.Size = ra.Size()
mt, err := DetectManifestMediaType(ra)
if err == nil {
desc.MediaType = mt
}
}
}
}
}
Expand All @@ -100,8 +107,14 @@ func Config(ctx context.Context, str string, resolver remotes.Resolver, cache Co

children := childrenConfigHandler(cache, platform)

dslHandler, err := docker.AppendDistributionSourceLabel(cache, ref.String())
if err != nil {
return "", nil, err
}

handlers := []images.Handler{
retryhandler.New(remotes.FetchHandler(cache, fetcher), func(_ []byte) {}),
dslHandler,
children,
}
if err := images.Dispatch(ctx, images.Handlers(handlers...), nil, desc); err != nil {
Expand Down
5 changes: 5 additions & 0 deletions util/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ func (p *Puller) tryLocalResolve(ctx context.Context) error {
if err != nil {
return err
}

if ok, err := contentutil.HasSource(info, p.Src); err != nil || !ok {
return errors.Errorf("no matching source")
}

desc.Size = info.Size
p.ref = p.Src.String()
ra, err := p.ContentStore.ReaderAt(ctx, desc)
Expand Down

0 comments on commit 450155e

Please sign in to comment.