Browse code

Correct parent chain in v2 push when v1Compatibility files on the disk are inconsistent

This fixes an issue where two images with the same filesystem contents
and configuration but different remote IDs could share a v1Compatibility
file, resulting in corrupted manifests.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2015/11/18 03:39:51
Showing 2 changed files
... ...
@@ -3,6 +3,8 @@ package graph
3 3
 import (
4 4
 	"bufio"
5 5
 	"compress/gzip"
6
+	"encoding/json"
7
+	"errors"
6 8
 	"fmt"
7 9
 	"io"
8 10
 	"io/ioutil"
... ...
@@ -205,6 +207,11 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
205 205
 		p.layersPushed[dgst] = true
206 206
 	}
207 207
 
208
+	// Fix parent chain if necessary
209
+	if err = fixHistory(m); err != nil {
210
+		return err
211
+	}
212
+
208 213
 	logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", p.repo.Name(), tag, p.trustKey.KeyID())
209 214
 	signed, err := schema1.Sign(m, p.trustKey)
210 215
 	if err != nil {
... ...
@@ -226,6 +233,90 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
226 226
 	return manSvc.Put(signed)
227 227
 }
228 228
 
229
+// fixHistory makes sure that the manifest has parent IDs that are consistent
230
+// with its image IDs. Because local image IDs are generated from the
231
+// configuration and filesystem contents, but IDs in the manifest are preserved
232
+// from the original pull, it's possible to have inconsistencies where parent
233
+// IDs don't match up with the other IDs in the manifest. This happens in the
234
+// case where an engine pulls images where are identical except the IDs from the
235
+// manifest - the local ID will be the same, and one of the v1Compatibility
236
+// files gets discarded.
237
+func fixHistory(m *schema1.Manifest) error {
238
+	var lastID string
239
+
240
+	for i := len(m.History) - 1; i >= 0; i-- {
241
+		var historyEntry map[string]*json.RawMessage
242
+		if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), &historyEntry); err != nil {
243
+			return err
244
+		}
245
+
246
+		idJSON, present := historyEntry["id"]
247
+		if !present || idJSON == nil {
248
+			return errors.New("missing id key in v1compatibility file")
249
+		}
250
+		var id string
251
+		if err := json.Unmarshal(*idJSON, &id); err != nil {
252
+			return err
253
+		}
254
+
255
+		parentJSON, present := historyEntry["parent"]
256
+
257
+		if i == len(m.History)-1 {
258
+			// The base layer must not reference a parent layer,
259
+			// otherwise the manifest is incomplete. There is an
260
+			// exception for Windows to handle base layers.
261
+			if !allowBaseParentImage && present && parentJSON != nil {
262
+				var parent string
263
+				if err := json.Unmarshal(*parentJSON, &parent); err != nil {
264
+					return err
265
+				}
266
+				if parent != "" {
267
+					logrus.Debugf("parent id mismatch detected; fixing. parent reference: %s", parent)
268
+					delete(historyEntry, "parent")
269
+					fixedHistory, err := json.Marshal(historyEntry)
270
+					if err != nil {
271
+						return err
272
+					}
273
+					m.History[i].V1Compatibility = string(fixedHistory)
274
+				}
275
+			}
276
+		} else {
277
+			// For all other layers, the parent ID should equal the
278
+			// ID of the next item in the history list. If it
279
+			// doesn't, fix it up (but preserve all other fields,
280
+			// possibly including fields that aren't known to this
281
+			// engine version).
282
+			if !present || parentJSON == nil {
283
+				return errors.New("missing parent key in v1compatibility file")
284
+			}
285
+			var parent string
286
+			if err := json.Unmarshal(*parentJSON, &parent); err != nil {
287
+				return err
288
+			}
289
+			if parent != lastID {
290
+				logrus.Debugf("parent id mismatch detected; fixing. parent reference: %s actual id: %s", parent, id)
291
+				historyEntry["parent"] = rawJSON(lastID)
292
+				fixedHistory, err := json.Marshal(historyEntry)
293
+				if err != nil {
294
+					return err
295
+				}
296
+				m.History[i].V1Compatibility = string(fixedHistory)
297
+			}
298
+		}
299
+		lastID = id
300
+	}
301
+
302
+	return nil
303
+}
304
+
305
+func rawJSON(value interface{}) *json.RawMessage {
306
+	jsonval, err := json.Marshal(value)
307
+	if err != nil {
308
+		return nil
309
+	}
310
+	return (*json.RawMessage)(&jsonval)
311
+}
312
+
229 313
 func (p *v2Pusher) pushV2Image(bs distribution.BlobService, img *image.Image) (digest.Digest, error) {
230 314
 	out := p.config.OutStream
231 315
 
... ...
@@ -2,13 +2,16 @@ package main
2 2
 
3 3
 import (
4 4
 	"archive/tar"
5
+	"encoding/json"
5 6
 	"fmt"
6 7
 	"io/ioutil"
7 8
 	"os"
8 9
 	"os/exec"
10
+	"path/filepath"
9 11
 	"strings"
10 12
 	"time"
11 13
 
14
+	"github.com/docker/docker/image"
12 15
 	"github.com/docker/docker/pkg/integration/checker"
13 16
 	"github.com/go-check/check"
14 17
 )
... ...
@@ -83,6 +86,46 @@ func (s *DockerRegistrySuite) TestPushMultipleTags(c *check.C) {
83 83
 	}
84 84
 }
85 85
 
86
+// TestPushBadParentChain tries to push an image with a corrupted parent chain
87
+// in the v1compatibility files, and makes sure the push process fixes it.
88
+func (s *DockerRegistrySuite) TestPushBadParentChain(c *check.C) {
89
+	repoName := fmt.Sprintf("%v/dockercli/badparent", privateRegistryURL)
90
+
91
+	id, err := buildImage(repoName, `
92
+	    FROM busybox
93
+	    CMD echo "adding another layer"
94
+	    `, true)
95
+	if err != nil {
96
+		c.Fatal(err)
97
+	}
98
+
99
+	// Push to create v1compatibility file
100
+	dockerCmd(c, "push", repoName)
101
+
102
+	// Corrupt the parent in the v1compatibility file from the top layer
103
+	filename := filepath.Join(dockerBasePath, "graph", id, "v1Compatibility")
104
+
105
+	jsonBytes, err := ioutil.ReadFile(filename)
106
+	c.Assert(err, check.IsNil, check.Commentf("Could not read v1Compatibility file: %s", err))
107
+
108
+	var img image.Image
109
+	err = json.Unmarshal(jsonBytes, &img)
110
+	c.Assert(err, check.IsNil, check.Commentf("Could not unmarshal json: %s", err))
111
+
112
+	img.Parent = "1234123412341234123412341234123412341234123412341234123412341234"
113
+
114
+	jsonBytes, err = json.Marshal(&img)
115
+	c.Assert(err, check.IsNil, check.Commentf("Could not marshal json: %s", err))
116
+
117
+	err = ioutil.WriteFile(filename, jsonBytes, 0600)
118
+	c.Assert(err, check.IsNil, check.Commentf("Could not write v1Compatibility file: %s", err))
119
+
120
+	dockerCmd(c, "push", repoName)
121
+
122
+	// pull should succeed
123
+	dockerCmd(c, "pull", repoName)
124
+}
125
+
86 126
 func (s *DockerRegistrySuite) TestPushEmptyLayer(c *check.C) {
87 127
 	repoName := fmt.Sprintf("%v/dockercli/emptylayer", privateRegistryURL)
88 128
 	emptyTarball, err := ioutil.TempFile("", "empty_tarball")