Browse code

Windows: Block pulling uplevel images

Signed-off-by: John Howard <jhoward@microsoft.com>

John Howard authored on 2018/02/16 06:17:27
Showing 9 changed files
... ...
@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/docker/docker/registry"
20 20
 	"github.com/docker/libtrust"
21 21
 	"github.com/opencontainers/go-digest"
22
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
22 23
 	"golang.org/x/net/context"
23 24
 )
24 25
 
... ...
@@ -86,7 +87,8 @@ type ImagePushConfig struct {
86 86
 type ImageConfigStore interface {
87 87
 	Put([]byte) (digest.Digest, error)
88 88
 	Get(digest.Digest) ([]byte, error)
89
-	RootFSAndOSFromConfig([]byte) (*image.RootFS, string, error)
89
+	RootFSFromConfig([]byte) (*image.RootFS, error)
90
+	PlatformFromConfig([]byte) (*specs.Platform, error)
90 91
 }
91 92
 
92 93
 // PushLayerProvider provides layers to be pushed by ChainID.
... ...
@@ -140,18 +142,26 @@ func (s *imageConfigStore) Get(d digest.Digest) ([]byte, error) {
140 140
 	return img.RawJSON(), nil
141 141
 }
142 142
 
143
-func (s *imageConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
143
+func (s *imageConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
144 144
 	var unmarshalledConfig image.Image
145 145
 	if err := json.Unmarshal(c, &unmarshalledConfig); err != nil {
146
-		return nil, "", err
146
+		return nil, err
147
+	}
148
+	return unmarshalledConfig.RootFS, nil
149
+}
150
+
151
+func (s *imageConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
152
+	var unmarshalledConfig image.Image
153
+	if err := json.Unmarshal(c, &unmarshalledConfig); err != nil {
154
+		return nil, err
147 155
 	}
148 156
 
149 157
 	// fail immediately on Windows when downloading a non-Windows image
150 158
 	// and vice versa. Exception on Windows if Linux Containers are enabled.
151 159
 	if runtime.GOOS == "windows" && unmarshalledConfig.OS == "linux" && !system.LCOWSupported() {
152
-		return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
160
+		return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
153 161
 	} else if runtime.GOOS != "windows" && unmarshalledConfig.OS == "windows" {
154
-		return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
162
+		return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
155 163
 	}
156 164
 
157 165
 	os := unmarshalledConfig.OS
... ...
@@ -159,9 +169,9 @@ func (s *imageConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, strin
159 159
 		os = runtime.GOOS
160 160
 	}
161 161
 	if !system.IsOSSupported(os) {
162
-		return nil, "", system.ErrNotSupportedOperatingSystem
162
+		return nil, system.ErrNotSupportedOperatingSystem
163 163
 	}
164
-	return unmarshalledConfig.RootFS, os, nil
164
+	return &specs.Platform{OS: os, OSVersion: unmarshalledConfig.OSVersion}, nil
165 165
 }
166 166
 
167 167
 type storeLayerProvider struct {
... ...
@@ -30,6 +30,7 @@ import (
30 30
 	refstore "github.com/docker/docker/reference"
31 31
 	"github.com/docker/docker/registry"
32 32
 	digest "github.com/opencontainers/go-digest"
33
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
33 34
 	"github.com/pkg/errors"
34 35
 	"github.com/sirupsen/logrus"
35 36
 	"golang.org/x/net/context"
... ...
@@ -584,11 +585,11 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
584 584
 	}()
585 585
 
586 586
 	var (
587
-		configJSON       []byte        // raw serialized image config
588
-		downloadedRootFS *image.RootFS // rootFS from registered layers
589
-		configRootFS     *image.RootFS // rootFS from configuration
590
-		release          func()        // release resources from rootFS download
591
-		configOS         string        // for LCOW when registering downloaded layers
587
+		configJSON       []byte          // raw serialized image config
588
+		downloadedRootFS *image.RootFS   // rootFS from registered layers
589
+		configRootFS     *image.RootFS   // rootFS from configuration
590
+		release          func()          // release resources from rootFS download
591
+		configPlatform   *specs.Platform // for LCOW when registering downloaded layers
592 592
 	)
593 593
 
594 594
 	// 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
600 600
 	// check to block Windows images being pulled on Linux is implemented, it
601 601
 	// may be necessary to perform the same type of serialisation.
602 602
 	if runtime.GOOS == "windows" {
603
-		configJSON, configRootFS, configOS, err = receiveConfig(p.config.ImageStore, configChan, configErrChan)
603
+		configJSON, configRootFS, configPlatform, err = receiveConfig(p.config.ImageStore, configChan, configErrChan)
604 604
 		if err != nil {
605 605
 			return "", "", err
606 606
 		}
607
-
608 607
 		if configRootFS == nil {
609 608
 			return "", "", errRootFSInvalid
610 609
 		}
610
+		if err := checkImageCompatibility(configPlatform.OS, configPlatform.OSVersion); err != nil {
611
+			return "", "", err
612
+		}
611 613
 
612 614
 		if len(descriptors) != len(configRootFS.DiffIDs) {
613 615
 			return "", "", errRootFSMismatch
... ...
@@ -615,8 +618,8 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
615 615
 
616 616
 		// Early bath if the requested OS doesn't match that of the configuration.
617 617
 		// This avoids doing the download, only to potentially fail later.
618
-		if !strings.EqualFold(configOS, requestedOS) {
619
-			return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configOS, requestedOS)
618
+		if !strings.EqualFold(configPlatform.OS, requestedOS) {
619
+			return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configPlatform.OS, requestedOS)
620 620
 		}
621 621
 
622 622
 		// 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
698 698
 	return imageID, manifestDigest, nil
699 699
 }
700 700
 
701
-func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, string, error) {
701
+func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, *specs.Platform, error) {
702 702
 	select {
703 703
 	case configJSON := <-configChan:
704
-		rootfs, os, err := s.RootFSAndOSFromConfig(configJSON)
704
+		rootfs, err := s.RootFSFromConfig(configJSON)
705
+		if err != nil {
706
+			return nil, nil, nil, err
707
+		}
708
+		platform, err := s.PlatformFromConfig(configJSON)
705 709
 		if err != nil {
706
-			return nil, nil, "", err
710
+			return nil, nil, nil, err
707 711
 		}
708
-		return configJSON, rootfs, os, nil
712
+		return configJSON, rootfs, platform, nil
709 713
 	case err := <-errChan:
710
-		return nil, nil, "", err
714
+		return nil, nil, nil, err
711 715
 		// Don't need a case for ctx.Done in the select because cancellation
712 716
 		// will trigger an error in p.pullSchema2ImageConfig.
713 717
 	}
... ...
@@ -736,6 +743,10 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf
736 736
 	}
737 737
 	manifestDigest := manifestMatches[0].Digest
738 738
 
739
+	if err := checkImageCompatibility(manifestMatches[0].Platform.OS, manifestMatches[0].Platform.OSVersion); err != nil {
740
+		return "", "", err
741
+	}
742
+
739 743
 	manSvc, err := p.repo.Manifests(ctx)
740 744
 	if err != nil {
741 745
 		return "", "", err
... ...
@@ -27,3 +27,8 @@ func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []m
27 27
 	}
28 28
 	return matches
29 29
 }
30
+
31
+// checkImageCompatibility is a Windows-specific function. No-op on Linux
32
+func checkImageCompatibility(imageOS, imageOSVersion string) error {
33
+	return nil
34
+}
... ...
@@ -1,11 +1,13 @@
1 1
 package distribution // import "github.com/docker/docker/distribution"
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"fmt"
5 6
 	"net/http"
6 7
 	"os"
7 8
 	"runtime"
8 9
 	"sort"
10
+	"strconv"
9 11
 	"strings"
10 12
 
11 13
 	"github.com/docker/distribution"
... ...
@@ -63,7 +65,6 @@ func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekClo
63 63
 func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor {
64 64
 	osVersion := ""
65 65
 	if os == "windows" {
66
-		// TODO: Add UBR (Update Build Release) component after build
67 66
 		version := system.GetOSVersion()
68 67
 		osVersion = fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build)
69 68
 		logrus.Debugf("will prefer entries with version %s", osVersion)
... ...
@@ -71,10 +72,11 @@ func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []m
71 71
 
72 72
 	var matches []manifestlist.ManifestDescriptor
73 73
 	for _, manifestDescriptor := range manifests {
74
-		// TODO: Consider filtering out greater versions, including only greater UBR
75 74
 		if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os {
76 75
 			matches = append(matches, manifestDescriptor)
77
-			logrus.Debugf("found match for %s/%s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
76
+			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())
77
+		} else {
78
+			logrus.Debugf("ignoring %s/%s %s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
78 79
 		}
79 80
 	}
80 81
 	if os == "windows" {
... ...
@@ -107,3 +109,22 @@ func (mbv manifestsByVersion) Len() int {
107 107
 func (mbv manifestsByVersion) Swap(i, j int) {
108 108
 	mbv.list[i], mbv.list[j] = mbv.list[j], mbv.list[i]
109 109
 }
110
+
111
+// checkImageCompatibility blocks pulling incompatible images based on a later OS build
112
+// Fixes https://github.com/moby/moby/issues/36184.
113
+func checkImageCompatibility(imageOS, imageOSVersion string) error {
114
+	if imageOS == "windows" {
115
+		hostOSV := system.GetOSVersion()
116
+		splitImageOSVersion := strings.Split(imageOSVersion, ".") // eg 10.0.16299.nnnn
117
+		if len(splitImageOSVersion) >= 3 {
118
+			if imageOSBuild, err := strconv.Atoi(splitImageOSVersion[2]); err == nil {
119
+				if imageOSBuild > int(hostOSV.Build) {
120
+					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())
121
+					logrus.Debugf(errMsg)
122
+					return errors.New(errMsg)
123
+				}
124
+			}
125
+		}
126
+	}
127
+	return nil
128
+}
... ...
@@ -118,12 +118,17 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, id
118 118
 		return fmt.Errorf("could not find image from tag %s: %v", reference.FamiliarString(ref), err)
119 119
 	}
120 120
 
121
-	rootfs, os, err := p.config.ImageStore.RootFSAndOSFromConfig(imgConfig)
121
+	rootfs, err := p.config.ImageStore.RootFSFromConfig(imgConfig)
122 122
 	if err != nil {
123 123
 		return fmt.Errorf("unable to get rootfs for image %s: %s", reference.FamiliarString(ref), err)
124 124
 	}
125 125
 
126
-	l, err := p.config.LayerStores[os].Get(rootfs.ChainID())
126
+	platform, err := p.config.ImageStore.PlatformFromConfig(imgConfig)
127
+	if err != nil {
128
+		return fmt.Errorf("unable to get platform for image %s: %s", reference.FamiliarString(ref), err)
129
+	}
130
+
131
+	l, err := p.config.LayerStores[platform.OS].Get(rootfs.ChainID())
127 132
 	if err != nil {
128 133
 		return fmt.Errorf("failed to get top layer from image: %v", err)
129 134
 	}
... ...
@@ -1,6 +1,7 @@
1 1
 package system // import "github.com/docker/docker/pkg/system"
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"unsafe"
5 6
 
6 7
 	"github.com/sirupsen/logrus"
... ...
@@ -53,6 +54,10 @@ func GetOSVersion() OSVersion {
53 53
 	return osv
54 54
 }
55 55
 
56
+func (osv OSVersion) ToString() string {
57
+	return fmt.Sprintf("%d.%d.%d", osv.MajorVersion, osv.MinorVersion, osv.Build)
58
+}
59
+
56 60
 // IsWindowsClient returns true if the SKU is client
57 61
 // @engine maintainers - this function should not be removed or modified as it
58 62
 // is used to enforce licensing restrictions on Windows.
... ...
@@ -33,6 +33,7 @@ import (
33 33
 	"github.com/docker/docker/plugin/v2"
34 34
 	refstore "github.com/docker/docker/reference"
35 35
 	digest "github.com/opencontainers/go-digest"
36
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
36 37
 	"github.com/pkg/errors"
37 38
 	"github.com/sirupsen/logrus"
38 39
 	"golang.org/x/net/context"
... ...
@@ -146,10 +147,15 @@ func (s *tempConfigStore) Get(d digest.Digest) ([]byte, error) {
146 146
 	return s.config, nil
147 147
 }
148 148
 
149
-func (s *tempConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
149
+func (s *tempConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
150 150
 	return configToRootFS(c)
151 151
 }
152 152
 
153
+func (s *tempConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
154
+	// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
155
+	return &specs.Platform{OS: runtime.GOOS}, nil
156
+}
157
+
153 158
 func computePrivileges(c types.PluginConfig) types.PluginPrivileges {
154 159
 	var privileges types.PluginPrivileges
155 160
 	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) {
534 534
 	return ioutil.ReadAll(rwc)
535 535
 }
536 536
 
537
-func (s *pluginConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
537
+func (s *pluginConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
538 538
 	return configToRootFS(c)
539 539
 }
540 540
 
541
+func (s *pluginConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
542
+	// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
543
+	return &specs.Platform{OS: runtime.GOOS}, nil
544
+}
545
+
541 546
 type pluginLayerProvider struct {
542 547
 	pm     *Manager
543 548
 	plugin *v2.Plugin
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"io/ioutil"
7 7
 	"os"
8 8
 	"path/filepath"
9
+	"runtime"
9 10
 
10 11
 	"github.com/docker/docker/distribution/xfer"
11 12
 	"github.com/docker/docker/image"
... ...
@@ -14,6 +15,7 @@ import (
14 14
 	"github.com/docker/docker/pkg/chrootarchive"
15 15
 	"github.com/docker/docker/pkg/progress"
16 16
 	"github.com/opencontainers/go-digest"
17
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
17 18
 	"github.com/pkg/errors"
18 19
 	"github.com/sirupsen/logrus"
19 20
 	"golang.org/x/net/context"
... ...
@@ -178,6 +180,10 @@ func (dm *downloadManager) Put(dt []byte) (digest.Digest, error) {
178 178
 func (dm *downloadManager) Get(d digest.Digest) ([]byte, error) {
179 179
 	return nil, fmt.Errorf("digest not found")
180 180
 }
181
-func (dm *downloadManager) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
181
+func (dm *downloadManager) RootFSFromConfig(c []byte) (*image.RootFS, error) {
182 182
 	return configToRootFS(c)
183 183
 }
184
+func (dm *downloadManager) PlatformFromConfig(c []byte) (*specs.Platform, error) {
185
+	// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
186
+	return &specs.Platform{OS: runtime.GOOS}, nil
187
+}
... ...
@@ -8,7 +8,6 @@ import (
8 8
 	"path/filepath"
9 9
 	"reflect"
10 10
 	"regexp"
11
-	"runtime"
12 11
 	"sort"
13 12
 	"strings"
14 13
 	"sync"
... ...
@@ -375,19 +374,17 @@ func isEqualPrivilege(a, b types.PluginPrivilege) bool {
375 375
 	return reflect.DeepEqual(a.Value, b.Value)
376 376
 }
377 377
 
378
-func configToRootFS(c []byte) (*image.RootFS, string, error) {
379
-	// TODO @jhowardmsft LCOW - Will need to revisit this.
380
-	os := runtime.GOOS
378
+func configToRootFS(c []byte) (*image.RootFS, error) {
381 379
 	var pluginConfig types.PluginConfig
382 380
 	if err := json.Unmarshal(c, &pluginConfig); err != nil {
383
-		return nil, "", err
381
+		return nil, err
384 382
 	}
385 383
 	// validation for empty rootfs is in distribution code
386 384
 	if pluginConfig.Rootfs == nil {
387
-		return nil, os, nil
385
+		return nil, nil
388 386
 	}
389 387
 
390
-	return rootFSFromPlugin(pluginConfig.Rootfs), os, nil
388
+	return rootFSFromPlugin(pluginConfig.Rootfs), nil
391 389
 }
392 390
 
393 391
 func rootFSFromPlugin(pluginfs *types.PluginConfigRootfs) *image.RootFS {