Browse Source

Windows: Block pulling uplevel images

Signed-off-by: John Howard <jhoward@microsoft.com>
John Howard 7 years ago
parent
commit
83908836d3

+ 17 - 7
distribution/config.go

@@ -19,6 +19,7 @@ import (
 	"github.com/docker/docker/registry"
 	"github.com/docker/libtrust"
 	"github.com/opencontainers/go-digest"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"golang.org/x/net/context"
 )
 
@@ -86,7 +87,8 @@ type ImagePushConfig struct {
 type ImageConfigStore interface {
 	Put([]byte) (digest.Digest, error)
 	Get(digest.Digest) ([]byte, error)
-	RootFSAndOSFromConfig([]byte) (*image.RootFS, string, error)
+	RootFSFromConfig([]byte) (*image.RootFS, error)
+	PlatformFromConfig([]byte) (*specs.Platform, error)
 }
 
 // PushLayerProvider provides layers to be pushed by ChainID.
@@ -140,18 +142,26 @@ func (s *imageConfigStore) Get(d digest.Digest) ([]byte, error) {
 	return img.RawJSON(), nil
 }
 
-func (s *imageConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
+func (s *imageConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
 	var unmarshalledConfig image.Image
 	if err := json.Unmarshal(c, &unmarshalledConfig); err != nil {
-		return nil, "", err
+		return nil, err
+	}
+	return unmarshalledConfig.RootFS, nil
+}
+
+func (s *imageConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
+	var unmarshalledConfig image.Image
+	if err := json.Unmarshal(c, &unmarshalledConfig); err != nil {
+		return nil, err
 	}
 
 	// fail immediately on Windows when downloading a non-Windows image
 	// and vice versa. Exception on Windows if Linux Containers are enabled.
 	if runtime.GOOS == "windows" && unmarshalledConfig.OS == "linux" && !system.LCOWSupported() {
-		return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
+		return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
 	} else if runtime.GOOS != "windows" && unmarshalledConfig.OS == "windows" {
-		return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
+		return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
 	}
 
 	os := unmarshalledConfig.OS
@@ -159,9 +169,9 @@ func (s *imageConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, strin
 		os = runtime.GOOS
 	}
 	if !system.IsOSSupported(os) {
-		return nil, "", system.ErrNotSupportedOperatingSystem
+		return nil, system.ErrNotSupportedOperatingSystem
 	}
-	return unmarshalledConfig.RootFS, os, nil
+	return &specs.Platform{OS: os, OSVersion: unmarshalledConfig.OSVersion}, nil
 }
 
 type storeLayerProvider struct {

+ 25 - 14
distribution/pull_v2.go

@@ -30,6 +30,7 @@ import (
 	refstore "github.com/docker/docker/reference"
 	"github.com/docker/docker/registry"
 	digest "github.com/opencontainers/go-digest"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/net/context"
@@ -584,11 +585,11 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
 	}()
 
 	var (
-		configJSON       []byte        // raw serialized image config
-		downloadedRootFS *image.RootFS // rootFS from registered layers
-		configRootFS     *image.RootFS // rootFS from configuration
-		release          func()        // release resources from rootFS download
-		configOS         string        // for LCOW when registering downloaded layers
+		configJSON       []byte          // raw serialized image config
+		downloadedRootFS *image.RootFS   // rootFS from registered layers
+		configRootFS     *image.RootFS   // rootFS from configuration
+		release          func()          // release resources from rootFS download
+		configPlatform   *specs.Platform // for LCOW when registering downloaded layers
 	)
 
 	// https://github.com/docker/docker/issues/24766 - Err on the side of caution,
@@ -600,14 +601,16 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
 	// check to block Windows images being pulled on Linux is implemented, it
 	// may be necessary to perform the same type of serialisation.
 	if runtime.GOOS == "windows" {
-		configJSON, configRootFS, configOS, err = receiveConfig(p.config.ImageStore, configChan, configErrChan)
+		configJSON, configRootFS, configPlatform, err = receiveConfig(p.config.ImageStore, configChan, configErrChan)
 		if err != nil {
 			return "", "", err
 		}
-
 		if configRootFS == nil {
 			return "", "", errRootFSInvalid
 		}
+		if err := checkImageCompatibility(configPlatform.OS, configPlatform.OSVersion); err != nil {
+			return "", "", err
+		}
 
 		if len(descriptors) != len(configRootFS.DiffIDs) {
 			return "", "", errRootFSMismatch
@@ -615,8 +618,8 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
 
 		// Early bath if the requested OS doesn't match that of the configuration.
 		// This avoids doing the download, only to potentially fail later.
-		if !strings.EqualFold(configOS, requestedOS) {
-			return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configOS, requestedOS)
+		if !strings.EqualFold(configPlatform.OS, requestedOS) {
+			return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configPlatform.OS, requestedOS)
 		}
 
 		// Populate diff ids in descriptors to avoid downloading foreign layers
@@ -698,16 +701,20 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
 	return imageID, manifestDigest, nil
 }
 
-func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, string, error) {
+func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, *specs.Platform, error) {
 	select {
 	case configJSON := <-configChan:
-		rootfs, os, err := s.RootFSAndOSFromConfig(configJSON)
+		rootfs, err := s.RootFSFromConfig(configJSON)
+		if err != nil {
+			return nil, nil, nil, err
+		}
+		platform, err := s.PlatformFromConfig(configJSON)
 		if err != nil {
-			return nil, nil, "", err
+			return nil, nil, nil, err
 		}
-		return configJSON, rootfs, os, nil
+		return configJSON, rootfs, platform, nil
 	case err := <-errChan:
-		return nil, nil, "", err
+		return nil, nil, nil, err
 		// Don't need a case for ctx.Done in the select because cancellation
 		// will trigger an error in p.pullSchema2ImageConfig.
 	}
@@ -736,6 +743,10 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf
 	}
 	manifestDigest := manifestMatches[0].Digest
 
+	if err := checkImageCompatibility(manifestMatches[0].Platform.OS, manifestMatches[0].Platform.OSVersion); err != nil {
+		return "", "", err
+	}
+
 	manSvc, err := p.repo.Manifests(ctx)
 	if err != nil {
 		return "", "", err

+ 5 - 0
distribution/pull_v2_unix.go

@@ -27,3 +27,8 @@ func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []m
 	}
 	return matches
 }
+
+// checkImageCompatibility is a Windows-specific function. No-op on Linux
+func checkImageCompatibility(imageOS, imageOSVersion string) error {
+	return nil
+}

+ 24 - 3
distribution/pull_v2_windows.go

@@ -1,11 +1,13 @@
 package distribution // import "github.com/docker/docker/distribution"
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"os"
 	"runtime"
 	"sort"
+	"strconv"
 	"strings"
 
 	"github.com/docker/distribution"
@@ -63,7 +65,6 @@ func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekClo
 func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor {
 	osVersion := ""
 	if os == "windows" {
-		// TODO: Add UBR (Update Build Release) component after build
 		version := system.GetOSVersion()
 		osVersion = fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build)
 		logrus.Debugf("will prefer entries with version %s", osVersion)
@@ -71,10 +72,11 @@ func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []m
 
 	var matches []manifestlist.ManifestDescriptor
 	for _, manifestDescriptor := range manifests {
-		// TODO: Consider filtering out greater versions, including only greater UBR
 		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os {
 			matches = append(matches, manifestDescriptor)
-			logrus.Debugf("found match for %s/%s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
+			logrus.Debugf("found match for %s/%s %s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
+		} else {
+			logrus.Debugf("ignoring %s/%s %s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
 		}
 	}
 	if os == "windows" {
@@ -107,3 +109,22 @@ func (mbv manifestsByVersion) Len() int {
 func (mbv manifestsByVersion) Swap(i, j int) {
 	mbv.list[i], mbv.list[j] = mbv.list[j], mbv.list[i]
 }
+
+// checkImageCompatibility blocks pulling incompatible images based on a later OS build
+// Fixes https://github.com/moby/moby/issues/36184.
+func checkImageCompatibility(imageOS, imageOSVersion string) error {
+	if imageOS == "windows" {
+		hostOSV := system.GetOSVersion()
+		splitImageOSVersion := strings.Split(imageOSVersion, ".") // eg 10.0.16299.nnnn
+		if len(splitImageOSVersion) >= 3 {
+			if imageOSBuild, err := strconv.Atoi(splitImageOSVersion[2]); err == nil {
+				if imageOSBuild > int(hostOSV.Build) {
+					errMsg := fmt.Sprintf("a Windows version %s.%s.%s-based image is incompatible with a %s host", splitImageOSVersion[0], splitImageOSVersion[1], splitImageOSVersion[2], hostOSV.ToString())
+					logrus.Debugf(errMsg)
+					return errors.New(errMsg)
+				}
+			}
+		}
+	}
+	return nil
+}

+ 7 - 2
distribution/push_v2.go

@@ -118,12 +118,17 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, id
 		return fmt.Errorf("could not find image from tag %s: %v", reference.FamiliarString(ref), err)
 	}
 
-	rootfs, os, err := p.config.ImageStore.RootFSAndOSFromConfig(imgConfig)
+	rootfs, err := p.config.ImageStore.RootFSFromConfig(imgConfig)
 	if err != nil {
 		return fmt.Errorf("unable to get rootfs for image %s: %s", reference.FamiliarString(ref), err)
 	}
 
-	l, err := p.config.LayerStores[os].Get(rootfs.ChainID())
+	platform, err := p.config.ImageStore.PlatformFromConfig(imgConfig)
+	if err != nil {
+		return fmt.Errorf("unable to get platform for image %s: %s", reference.FamiliarString(ref), err)
+	}
+
+	l, err := p.config.LayerStores[platform.OS].Get(rootfs.ChainID())
 	if err != nil {
 		return fmt.Errorf("failed to get top layer from image: %v", err)
 	}

+ 5 - 0
pkg/system/syscall_windows.go

@@ -1,6 +1,7 @@
 package system // import "github.com/docker/docker/pkg/system"
 
 import (
+	"fmt"
 	"unsafe"
 
 	"github.com/sirupsen/logrus"
@@ -53,6 +54,10 @@ func GetOSVersion() OSVersion {
 	return osv
 }
 
+func (osv OSVersion) ToString() string {
+	return fmt.Sprintf("%d.%d.%d", osv.MajorVersion, osv.MinorVersion, osv.Build)
+}
+
 // IsWindowsClient returns true if the SKU is client
 // @engine maintainers - this function should not be removed or modified as it
 // is used to enforce licensing restrictions on Windows.

+ 13 - 2
plugin/backend_linux.go

@@ -33,6 +33,7 @@ import (
 	"github.com/docker/docker/plugin/v2"
 	refstore "github.com/docker/docker/reference"
 	digest "github.com/opencontainers/go-digest"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/net/context"
@@ -146,10 +147,15 @@ func (s *tempConfigStore) Get(d digest.Digest) ([]byte, error) {
 	return s.config, nil
 }
 
-func (s *tempConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
+func (s *tempConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
 	return configToRootFS(c)
 }
 
+func (s *tempConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
+	// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
+	return &specs.Platform{OS: runtime.GOOS}, nil
+}
+
 func computePrivileges(c types.PluginConfig) types.PluginPrivileges {
 	var privileges types.PluginPrivileges
 	if c.Network.Type != "null" && c.Network.Type != "bridge" && c.Network.Type != "" {
@@ -534,10 +540,15 @@ func (s *pluginConfigStore) Get(d digest.Digest) ([]byte, error) {
 	return ioutil.ReadAll(rwc)
 }
 
-func (s *pluginConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
+func (s *pluginConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
 	return configToRootFS(c)
 }
 
+func (s *pluginConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
+	// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
+	return &specs.Platform{OS: runtime.GOOS}, nil
+}
+
 type pluginLayerProvider struct {
 	pm     *Manager
 	plugin *v2.Plugin

+ 7 - 1
plugin/blobstore.go

@@ -6,6 +6,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"runtime"
 
 	"github.com/docker/docker/distribution/xfer"
 	"github.com/docker/docker/image"
@@ -14,6 +15,7 @@ import (
 	"github.com/docker/docker/pkg/chrootarchive"
 	"github.com/docker/docker/pkg/progress"
 	"github.com/opencontainers/go-digest"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/net/context"
@@ -178,6 +180,10 @@ func (dm *downloadManager) Put(dt []byte) (digest.Digest, error) {
 func (dm *downloadManager) Get(d digest.Digest) ([]byte, error) {
 	return nil, fmt.Errorf("digest not found")
 }
-func (dm *downloadManager) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
+func (dm *downloadManager) RootFSFromConfig(c []byte) (*image.RootFS, error) {
 	return configToRootFS(c)
 }
+func (dm *downloadManager) PlatformFromConfig(c []byte) (*specs.Platform, error) {
+	// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
+	return &specs.Platform{OS: runtime.GOOS}, nil
+}

+ 4 - 7
plugin/manager.go

@@ -8,7 +8,6 @@ import (
 	"path/filepath"
 	"reflect"
 	"regexp"
-	"runtime"
 	"sort"
 	"strings"
 	"sync"
@@ -375,19 +374,17 @@ func isEqualPrivilege(a, b types.PluginPrivilege) bool {
 	return reflect.DeepEqual(a.Value, b.Value)
 }
 
-func configToRootFS(c []byte) (*image.RootFS, string, error) {
-	// TODO @jhowardmsft LCOW - Will need to revisit this.
-	os := runtime.GOOS
+func configToRootFS(c []byte) (*image.RootFS, error) {
 	var pluginConfig types.PluginConfig
 	if err := json.Unmarshal(c, &pluginConfig); err != nil {
-		return nil, "", err
+		return nil, err
 	}
 	// validation for empty rootfs is in distribution code
 	if pluginConfig.Rootfs == nil {
-		return nil, os, nil
+		return nil, nil
 	}
 
-	return rootFSFromPlugin(pluginConfig.Rootfs), os, nil
+	return rootFSFromPlugin(pluginConfig.Rootfs), nil
 }
 
 func rootFSFromPlugin(pluginfs *types.PluginConfigRootfs) *image.RootFS {