diff --git a/components/engine/distribution/config.go b/components/engine/distribution/config.go index efd0d3fe7c..44aacd732e 100644 --- a/components/engine/distribution/config.go +++ b/components/engine/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 { diff --git a/components/engine/distribution/pull_v2.go b/components/engine/distribution/pull_v2.go index 2f1dac9a3c..a1316920f3 100644 --- a/components/engine/distribution/pull_v2.go +++ b/components/engine/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, "", err + return nil, nil, nil, err } - return configJSON, rootfs, os, nil + platform, err := s.PlatformFromConfig(configJSON) + if err != nil { + return nil, nil, nil, err + } + 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 diff --git a/components/engine/distribution/pull_v2_unix.go b/components/engine/distribution/pull_v2_unix.go index 9ddf0ac6e5..39c166fbc4 100644 --- a/components/engine/distribution/pull_v2_unix.go +++ b/components/engine/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 +} diff --git a/components/engine/distribution/pull_v2_windows.go b/components/engine/distribution/pull_v2_windows.go index 6102dc5b09..7527f62714 100644 --- a/components/engine/distribution/pull_v2_windows.go +++ b/components/engine/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 +} diff --git a/components/engine/distribution/push_v2.go b/components/engine/distribution/push_v2.go index ca066afc71..7b7155169d 100644 --- a/components/engine/distribution/push_v2.go +++ b/components/engine/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) } diff --git a/components/engine/pkg/system/syscall_windows.go b/components/engine/pkg/system/syscall_windows.go index 85e89a7eea..ee7e0256f3 100644 --- a/components/engine/pkg/system/syscall_windows.go +++ b/components/engine/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. diff --git a/components/engine/plugin/backend_linux.go b/components/engine/plugin/backend_linux.go index c86b11f61a..000ee996dc 100644 --- a/components/engine/plugin/backend_linux.go +++ b/components/engine/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 diff --git a/components/engine/plugin/blobstore.go b/components/engine/plugin/blobstore.go index 1702220704..82d6ce18e6 100644 --- a/components/engine/plugin/blobstore.go +++ b/components/engine/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 +} diff --git a/components/engine/plugin/manager.go b/components/engine/plugin/manager.go index 1879a78903..7595e7cbcc 100644 --- a/components/engine/plugin/manager.go +++ b/components/engine/plugin/manager.go @@ -8,7 +8,6 @@ import ( "path/filepath" "reflect" "regexp" - "runtime" "sort" "strings" "sync" @@ -353,19 +352,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 {