Skip to content

Commit

Permalink
Merge pull request from GHSA-xw73-rw38-6vjc
Browse files Browse the repository at this point in the history
[24.0 backport] image/cache: Restrict cache candidates to locally built images
  • Loading branch information
thaJeztah committed Feb 1, 2024
2 parents f78a772 + 44e6f3d commit fca702d
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 57 deletions.
3 changes: 2 additions & 1 deletion builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

const (
Expand Down Expand Up @@ -89,7 +90,7 @@ type ImageCacheBuilder interface {
type ImageCache interface {
// GetCache returns a reference to a cached image whose parent equals `parent`
// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error.
GetCache(parentID string, cfg *container.Config) (imageID string, err error)
GetCache(parentID string, cfg *container.Config, platform ocispec.Platform) (imageID string, err error)
}

// Image represents a Docker image used by the builder.
Expand Down
17 changes: 2 additions & 15 deletions builder/dockerfile/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -74,7 +73,7 @@ type copier struct {
source builder.Source
pathCache pathCache
download sourceDownloader
platform *ocispec.Platform
platform ocispec.Platform
// for cleanup. TODO: having copier.cleanup() is error prone and hard to
// follow. Code calling performCopy should manage the lifecycle of its params.
// Copier should take override source as input, not imageMount.
Expand All @@ -83,19 +82,7 @@ type copier struct {
}

func copierFromDispatchRequest(req dispatchRequest, download sourceDownloader, imageSource *imageMount) copier {
platform := req.builder.platform
if platform == nil {
// May be nil if not explicitly set in API/dockerfile
platform = &ocispec.Platform{}
}
if platform.OS == "" {
// Default to the dispatch requests operating system if not explicit in API/dockerfile
platform.OS = req.state.operatingSystem
}
if platform.OS == "" {
// This is a failsafe just in case. Shouldn't be hit.
platform.OS = runtime.GOOS
}
platform := req.builder.getPlatform(req.state)

return copier{
source: req.source,
Expand Down
9 changes: 8 additions & 1 deletion builder/dockerfile/dispatchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,16 @@ func dispatchRun(ctx context.Context, d dispatchRequest, c *instructions.RunComm
saveCmd = prependEnvOnCmd(d.state.buildArgs, buildArgs, cmdFromArgs)
}

cacheArgsEscaped := argsEscaped
// ArgsEscaped is not persisted in the committed image on Windows.
// Use the original from previous build steps for cache probing.
if d.state.operatingSystem == "windows" {
cacheArgsEscaped = stateRunConfig.ArgsEscaped
}

runConfigForCacheProbe := copyRunConfig(stateRunConfig,
withCmd(saveCmd),
withArgsEscaped(argsEscaped),
withArgsEscaped(cacheArgsEscaped),
withEntrypointOverride(saveCmd, nil))
if hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe); err != nil || hit {
return err
Expand Down
9 changes: 5 additions & 4 deletions builder/dockerfile/imageprobe.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import (

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/builder"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
)

// ImageProber exposes an Image cache to the Builder. It supports resetting a
// cache.
type ImageProber interface {
Reset(ctx context.Context) error
Probe(parentID string, runConfig *container.Config) (string, error)
Probe(parentID string, runConfig *container.Config, platform ocispec.Platform) (string, error)
}

type resetFunc func(context.Context) (builder.ImageCache, error)
Expand Down Expand Up @@ -51,11 +52,11 @@ func (c *imageProber) Reset(ctx context.Context) error {

// Probe checks if cache match can be found for current build instruction.
// It returns the cachedID if there is a hit, and the empty string on miss
func (c *imageProber) Probe(parentID string, runConfig *container.Config) (string, error) {
func (c *imageProber) Probe(parentID string, runConfig *container.Config, platform ocispec.Platform) (string, error) {
if c.cacheBusted {
return "", nil
}
cacheID, err := c.cache.GetCache(parentID, runConfig)
cacheID, err := c.cache.GetCache(parentID, runConfig, platform)
if err != nil {
return "", err
}
Expand All @@ -74,6 +75,6 @@ func (c *nopProber) Reset(ctx context.Context) error {
return nil
}

func (c *nopProber) Probe(_ string, _ *container.Config) (string, error) {
func (c *nopProber) Probe(_ string, _ *container.Config, _ ocispec.Platform) (string, error) {
return "", nil
}
17 changes: 16 additions & 1 deletion builder/dockerfile/internals.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"strings"

"github.com/containerd/containerd/platforms"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/api/types/container"
Expand Down Expand Up @@ -328,7 +329,7 @@ func getShell(c *container.Config, os string) []string {
}

func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) {
cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig)
cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig, b.getPlatform(dispatchState))
if cachedID == "" || err != nil {
return false, err
}
Expand Down Expand Up @@ -388,3 +389,17 @@ func hostConfigFromOptions(options *types.ImageBuildOptions) *container.HostConf
}
return hc
}

func (b *Builder) getPlatform(state *dispatchState) ocispec.Platform {
// May be nil if not explicitly set in API/dockerfile
out := platforms.DefaultSpec()
if b.platform != nil {
out = *b.platform
}

if state.operatingSystem != "" {
out.OS = state.operatingSystem
}

return out
}
3 changes: 2 additions & 1 deletion builder/dockerfile/mockbackend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// MockBackend implements the builder.Backend interface for unit testing
Expand Down Expand Up @@ -111,7 +112,7 @@ type mockImageCache struct {
getCacheFunc func(parentID string, cfg *container.Config) (string, error)
}

func (mic *mockImageCache) GetCache(parentID string, cfg *container.Config) (string, error) {
func (mic *mockImageCache) GetCache(parentID string, cfg *container.Config, _ ocispec.Platform) (string, error) {
if mic.getCacheFunc != nil {
return mic.getCacheFunc(parentID, cfg)
}
Expand Down
14 changes: 11 additions & 3 deletions daemon/containerd/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"github.com/docker/docker/api/types/container"
imagetype "github.com/docker/docker/api/types/image"
"github.com/docker/docker/builder"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// MakeImageCache creates a stateful image cache.
Expand All @@ -29,16 +31,19 @@ type imageCache struct {
c *ImageService
}

func (ic *imageCache) GetCache(parentID string, cfg *container.Config) (imageID string, err error) {
func (ic *imageCache) GetCache(parentID string, cfg *container.Config, platform ocispec.Platform) (imageID string, err error) {
ctx := context.TODO()

if parentID == "" {
// TODO handle "parentless" image cache lookups ("FROM scratch")
return "", nil
}

parent, err := ic.c.GetImage(ctx, parentID, imagetype.GetImageOpts{})
parent, err := ic.c.GetImage(ctx, parentID, imagetype.GetImageOpts{Platform: &platform})
if err != nil {
if errdefs.IsNotFound(err) {
return "", nil
}
return "", err
}

Expand All @@ -54,8 +59,11 @@ func (ic *imageCache) GetCache(parentID string, cfg *container.Config) (imageID
}

for _, children := range children {
childImage, err := ic.c.GetImage(ctx, children.String(), imagetype.GetImageOpts{})
childImage, err := ic.c.GetImage(ctx, children.String(), imagetype.GetImageOpts{Platform: &platform})
if err != nil {
if errdefs.IsNotFound(err) {
continue
}
return "", err
}

Expand Down
3 changes: 3 additions & 0 deletions daemon/images/image_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ func (i *ImageService) CreateImage(ctx context.Context, config []byte, parent st
return nil, errors.Wrapf(err, "failed to set parent %s", parent)
}
}
if err := i.imageStore.SetBuiltLocally(id); err != nil {
return nil, errors.Wrapf(err, "failed to mark image %s as built locally", id)
}

return i.imageStore.Get(id)
}
3 changes: 3 additions & 0 deletions daemon/images/image_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ func (i *ImageService) CommitImage(ctx context.Context, c backend.CommitConfig)
if err != nil {
return "", err
}
if err := i.imageStore.SetBuiltLocally(id); err != nil {
return "", err
}

if c.ParentImageID != "" {
if err := i.imageStore.SetParent(id, image.ID(c.ParentImageID)); err != nil {
Expand Down
82 changes: 75 additions & 7 deletions image/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"reflect"
"strings"

"github.com/containerd/containerd/platforms"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

// NewLocal returns a local image cache, based on parent chain
Expand All @@ -26,8 +29,8 @@ type LocalImageCache struct {
}

// GetCache returns the image id found in the cache
func (lic *LocalImageCache) GetCache(imgID string, config *containertypes.Config) (string, error) {
return getImageIDAndError(getLocalCachedImage(lic.store, image.ID(imgID), config))
func (lic *LocalImageCache) GetCache(imgID string, config *containertypes.Config, platform ocispec.Platform) (string, error) {
return getImageIDAndError(getLocalCachedImage(lic.store, image.ID(imgID), config, platform))
}

// New returns an image cache, based on history objects
Expand All @@ -51,8 +54,8 @@ func (ic *ImageCache) Populate(image *image.Image) {
}

// GetCache returns the image id found in the cache
func (ic *ImageCache) GetCache(parentID string, cfg *containertypes.Config) (string, error) {
imgID, err := ic.localImageCache.GetCache(parentID, cfg)
func (ic *ImageCache) GetCache(parentID string, cfg *containertypes.Config, platform ocispec.Platform) (string, error) {
imgID, err := ic.localImageCache.GetCache(parentID, cfg, platform)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -215,7 +218,23 @@ func getImageIDAndError(img *image.Image, err error) (string, error) {
// of the image with imgID, that had the same config when it was
// created. nil is returned if a child cannot be found. An error is
// returned if the parent image cannot be found.
func getLocalCachedImage(imageStore image.Store, imgID image.ID, config *containertypes.Config) (*image.Image, error) {
func getLocalCachedImage(imageStore image.Store, imgID image.ID, config *containertypes.Config, platform ocispec.Platform) (*image.Image, error) {
if config == nil {
return nil, nil
}

isBuiltLocally := func(id image.ID) bool {
builtLocally, err := imageStore.IsBuiltLocally(id)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"id": id,
}).Warn("failed to check if image was built locally")
return false
}
return builtLocally
}

// Loop on the children of the given image and check the config
getMatch := func(siblings []image.ID) (*image.Image, error) {
var match *image.Image
Expand All @@ -225,6 +244,25 @@ func getLocalCachedImage(imageStore image.Store, imgID image.ID, config *contain
return nil, fmt.Errorf("unable to find image %q", id)
}

if !isBuiltLocally(id) {
continue
}

imgPlatform := ocispec.Platform{
Architecture: img.Architecture,
OS: img.OS,
OSVersion: img.OSVersion,
OSFeatures: img.OSFeatures,
Variant: img.Variant,
}
// Discard old linux/amd64 images with empty platform.
if imgPlatform.OS == "" && imgPlatform.Architecture == "" {
continue
}
if !platforms.OnlyStrict(platform).Match(imgPlatform) {
continue
}

if compare(&img.ContainerConfig, config) {
// check for the most up to date match
if match == nil || match.Created.Before(img.Created) {
Expand All @@ -238,11 +276,29 @@ func getLocalCachedImage(imageStore image.Store, imgID image.ID, config *contain
// In this case, this is `FROM scratch`, which isn't an actual image.
if imgID == "" {
images := imageStore.Map()

var siblings []image.ID
for id, img := range images {
if img.Parent == imgID {
siblings = append(siblings, id)
if img.Parent != "" {
continue
}

if !isBuiltLocally(id) {
continue
}

// Do a quick initial filter on the Cmd to avoid adding all
// non-local images with empty parent to the siblings slice and
// performing a full config compare.
//
// config.Cmd is set to the current Dockerfile instruction so we
// check it against the img.ContainerConfig.Cmd which is the
// command of the last layer.
if !strSliceEqual(img.ContainerConfig.Cmd, config.Cmd) {
continue
}

siblings = append(siblings, id)
}
return getMatch(siblings)
}
Expand All @@ -251,3 +307,15 @@ func getLocalCachedImage(imageStore image.Store, imgID image.ID, config *contain
siblings := imageStore.Children(imgID)
return getMatch(siblings)
}

func strSliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}

0 comments on commit fca702d

Please sign in to comment.