Browse code

Ensure daemon root is unmounted on shutdown

This is only for the case when dockerd has had to re-mount the daemon
root as shared.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2018/01/25 08:10:01
Showing 3 changed files
... ...
@@ -10,6 +10,7 @@ import (
10 10
 
11 11
 	"github.com/docker/docker/pkg/fileutils"
12 12
 	"github.com/docker/docker/pkg/mount"
13
+	"github.com/pkg/errors"
13 14
 	"github.com/sirupsen/logrus"
14 15
 )
15 16
 
... ...
@@ -67,9 +68,29 @@ func (daemon *Daemon) cleanupMountsFromReaderByID(reader io.Reader, id string, u
67 67
 	return nil
68 68
 }
69 69
 
70
-// cleanupMounts umounts shm/mqueue mounts for old containers
70
+// cleanupMounts umounts used by container resources and the daemon root mount
71 71
 func (daemon *Daemon) cleanupMounts() error {
72
-	return daemon.cleanupMountsByID("")
72
+	if err := daemon.cleanupMountsByID(""); err != nil {
73
+		return err
74
+	}
75
+
76
+	infos, err := mount.GetMounts()
77
+	if err != nil {
78
+		return errors.Wrap(err, "error reading mount table for cleanup")
79
+	}
80
+
81
+	info := getMountInfo(infos, daemon.root)
82
+	// `info.Root` here is the root mountpoint of the passed in path (`daemon.root`).
83
+	// The ony cases that need to be cleaned up is when the daemon has performed a
84
+	//   `mount --bind /daemon/root /daemon/root && mount --make-shared /daemon/root`
85
+	// This is only done when the daemon is started up and `/daemon/root` is not
86
+	// already on a shared mountpoint.
87
+	if !shouldUnmountRoot(daemon.root, info) {
88
+		return nil
89
+	}
90
+
91
+	logrus.WithField("mountpoint", daemon.root).Debug("unmounting daemon root")
92
+	return mount.Unmount(daemon.root)
73 93
 }
74 94
 
75 95
 func getCleanPatterns(id string) (regexps []*regexp.Regexp) {
... ...
@@ -91,3 +112,16 @@ func getCleanPatterns(id string) (regexps []*regexp.Regexp) {
91 91
 func getRealPath(path string) (string, error) {
92 92
 	return fileutils.ReadSymlinkedDirectory(path)
93 93
 }
94
+
95
+func shouldUnmountRoot(root string, info *mount.Info) bool {
96
+	if info == nil {
97
+		return false
98
+	}
99
+	if info.Mountpoint != root {
100
+		return false
101
+	}
102
+	if !strings.HasSuffix(root, info.Root) {
103
+		return false
104
+	}
105
+	return hasMountinfoOption(info.Optional, sharedPropagationOption)
106
+}
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"github.com/docker/docker/container"
11 11
 	"github.com/docker/docker/oci"
12 12
 	"github.com/docker/docker/pkg/idtools"
13
+	"github.com/docker/docker/pkg/mount"
13 14
 
14 15
 	"github.com/stretchr/testify/assert"
15 16
 )
... ...
@@ -164,3 +165,66 @@ func TestValidateContainerIsolationLinux(t *testing.T) {
164 164
 	_, err := d.verifyContainerSettings("linux", &containertypes.HostConfig{Isolation: containertypes.IsolationHyperV}, nil, false)
165 165
 	assert.EqualError(t, err, "invalid isolation 'hyperv' on linux")
166 166
 }
167
+
168
+func TestShouldUnmountRoot(t *testing.T) {
169
+	for _, test := range []struct {
170
+		desc   string
171
+		root   string
172
+		info   *mount.Info
173
+		expect bool
174
+	}{
175
+		{
176
+			desc:   "root is at /",
177
+			root:   "/docker",
178
+			info:   &mount.Info{Root: "/docker", Mountpoint: "/docker"},
179
+			expect: true,
180
+		},
181
+		{
182
+			desc:   "not a mountpoint",
183
+			root:   "/docker",
184
+			info:   nil,
185
+			expect: false,
186
+		},
187
+		{
188
+			desc:   "root is at in a submount from `/`",
189
+			root:   "/foo/docker",
190
+			info:   &mount.Info{Root: "/docker", Mountpoint: "/foo/docker"},
191
+			expect: true,
192
+		},
193
+		{
194
+			desc:   "root is mounted in from a parent mount namespace same root dir", // dind is an example of this
195
+			root:   "/docker",
196
+			info:   &mount.Info{Root: "/docker/volumes/1234657/_data", Mountpoint: "/docker"},
197
+			expect: false,
198
+		},
199
+		{
200
+			desc:   "root is mounted in from a parent mount namespace different root dir",
201
+			root:   "/foo/bar",
202
+			info:   &mount.Info{Root: "/docker/volumes/1234657/_data", Mountpoint: "/foo/bar"},
203
+			expect: false,
204
+		},
205
+	} {
206
+		t.Run(test.desc, func(t *testing.T) {
207
+			for _, options := range []struct {
208
+				desc     string
209
+				Optional string
210
+				expect   bool
211
+			}{
212
+				{desc: "shared", Optional: "shared:", expect: true},
213
+				{desc: "slave", Optional: "slave:", expect: false},
214
+				{desc: "private", Optional: "private:", expect: false},
215
+			} {
216
+				t.Run(options.desc, func(t *testing.T) {
217
+					expect := options.expect
218
+					if expect {
219
+						expect = test.expect
220
+					}
221
+					if test.info != nil {
222
+						test.info.Optional = options.Optional
223
+					}
224
+					assert.Equal(t, expect, shouldUnmountRoot(test.root, test.info))
225
+				})
226
+			}
227
+		})
228
+	}
229
+}
... ...
@@ -24,6 +24,7 @@ import (
24 24
 	"github.com/opencontainers/runc/libcontainer/devices"
25 25
 	"github.com/opencontainers/runc/libcontainer/user"
26 26
 	specs "github.com/opencontainers/runtime-spec/specs-go"
27
+	"github.com/pkg/errors"
27 28
 	"github.com/sirupsen/logrus"
28 29
 	"golang.org/x/sys/unix"
29 30
 )
... ...
@@ -419,52 +420,46 @@ func getSourceMount(source string) (string, string, error) {
419 419
 	return "", "", fmt.Errorf("Could not find source mount of %s", source)
420 420
 }
421 421
 
422
+const (
423
+	sharedPropagationOption = "shared:"
424
+	slavePropagationOption  = "master:"
425
+)
426
+
427
+// hasMountinfoOption checks if any of the passed any of the given option values
428
+// are set in the passed in option string.
429
+func hasMountinfoOption(opts string, vals ...string) bool {
430
+	for _, opt := range strings.Split(opts, " ") {
431
+		for _, val := range vals {
432
+			if strings.HasPrefix(opt, val) {
433
+				return true
434
+			}
435
+		}
436
+	}
437
+	return false
438
+}
439
+
422 440
 // Ensure mount point on which path is mounted, is shared.
423 441
 func ensureShared(path string) error {
424
-	sharedMount := false
425
-
426 442
 	sourceMount, optionalOpts, err := getSourceMount(path)
427 443
 	if err != nil {
428 444
 		return err
429 445
 	}
430 446
 	// Make sure source mount point is shared.
431
-	optsSplit := strings.Split(optionalOpts, " ")
432
-	for _, opt := range optsSplit {
433
-		if strings.HasPrefix(opt, "shared:") {
434
-			sharedMount = true
435
-			break
436
-		}
437
-	}
438
-
439
-	if !sharedMount {
440
-		return fmt.Errorf("path %s is mounted on %s but it is not a shared mount", path, sourceMount)
447
+	if !hasMountinfoOption(optionalOpts, sharedPropagationOption) {
448
+		return errors.Errorf("path %s is mounted on %s but it is not a shared mount", path, sourceMount)
441 449
 	}
442 450
 	return nil
443 451
 }
444 452
 
445 453
 // Ensure mount point on which path is mounted, is either shared or slave.
446 454
 func ensureSharedOrSlave(path string) error {
447
-	sharedMount := false
448
-	slaveMount := false
449
-
450 455
 	sourceMount, optionalOpts, err := getSourceMount(path)
451 456
 	if err != nil {
452 457
 		return err
453 458
 	}
454
-	// Make sure source mount point is shared.
455
-	optsSplit := strings.Split(optionalOpts, " ")
456
-	for _, opt := range optsSplit {
457
-		if strings.HasPrefix(opt, "shared:") {
458
-			sharedMount = true
459
-			break
460
-		} else if strings.HasPrefix(opt, "master:") {
461
-			slaveMount = true
462
-			break
463
-		}
464
-	}
465 459
 
466
-	if !sharedMount && !slaveMount {
467
-		return fmt.Errorf("path %s is mounted on %s but it is not a shared or slave mount", path, sourceMount)
460
+	if !hasMountinfoOption(optionalOpts, sharedPropagationOption, slavePropagationOption) {
461
+		return errors.Errorf("path %s is mounted on %s but it is not a shared or slave mount", path, sourceMount)
468 462
 	}
469 463
 	return nil
470 464
 }