Browse code

Merge pull request #36055 from cpuguy83/slave_mounts_for_root

Use rslave propagation for mounts from daemon root

Sebastiaan van Stijn authored on 2018/02/15 20:57:25
Showing 6 changed files
... ...
@@ -604,7 +604,8 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c
604 604
 		//
605 605
 		// For private volumes any root propagation value should work.
606 606
 		pFlag := mountPropagationMap[m.Propagation]
607
-		if pFlag == mount.SHARED || pFlag == mount.RSHARED {
607
+		switch pFlag {
608
+		case mount.SHARED, mount.RSHARED:
608 609
 			if err := ensureShared(m.Source); err != nil {
609 610
 				return err
610 611
 			}
... ...
@@ -612,13 +613,34 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c
612 612
 			if rootpg != mount.SHARED && rootpg != mount.RSHARED {
613 613
 				s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.SHARED]
614 614
 			}
615
-		} else if pFlag == mount.SLAVE || pFlag == mount.RSLAVE {
615
+		case mount.SLAVE, mount.RSLAVE:
616
+			var fallback bool
616 617
 			if err := ensureSharedOrSlave(m.Source); err != nil {
617
-				return err
618
+				// For backwards compatability purposes, treat mounts from the daemon root
619
+				// as special since we automatically add rslave propagation to these mounts
620
+				// when the user did not set anything, so we should fallback to the old
621
+				// behavior which is to use private propagation which is normally the
622
+				// default.
623
+				if !strings.HasPrefix(m.Source, daemon.root) && !strings.HasPrefix(daemon.root, m.Source) {
624
+					return err
625
+				}
626
+
627
+				cm, ok := c.MountPoints[m.Destination]
628
+				if !ok {
629
+					return err
630
+				}
631
+				if cm.Spec.BindOptions != nil && cm.Spec.BindOptions.Propagation != "" {
632
+					// This means the user explicitly set a propagation, do not fallback in that case.
633
+					return err
634
+				}
635
+				fallback = true
636
+				logrus.WithField("container", c.ID).WithField("source", m.Source).Warn("Falling back to default propagation for bind source in daemon root")
618 637
 			}
619
-			rootpg := mountPropagationMap[s.Linux.RootfsPropagation]
620
-			if rootpg != mount.SHARED && rootpg != mount.RSHARED && rootpg != mount.SLAVE && rootpg != mount.RSLAVE {
621
-				s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.RSLAVE]
638
+			if !fallback {
639
+				rootpg := mountPropagationMap[s.Linux.RootfsPropagation]
640
+				if rootpg != mount.SHARED && rootpg != mount.RSHARED && rootpg != mount.SLAVE && rootpg != mount.RSLAVE {
641
+					s.Linux.RootfsPropagation = mountPropagationReverseMap[mount.RSLAVE]
642
+				}
622 643
 			}
623 644
 		}
624 645
 
... ...
@@ -10,6 +10,7 @@ import (
10 10
 
11 11
 	"github.com/docker/docker/api/types"
12 12
 	containertypes "github.com/docker/docker/api/types/container"
13
+	"github.com/docker/docker/api/types/mount"
13 14
 	mounttypes "github.com/docker/docker/api/types/mount"
14 15
 	"github.com/docker/docker/container"
15 16
 	"github.com/docker/docker/errdefs"
... ...
@@ -146,6 +147,13 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
146 146
 		if err != nil {
147 147
 			return err
148 148
 		}
149
+		needsSlavePropagation, err := daemon.validateBindDaemonRoot(bind.Spec)
150
+		if err != nil {
151
+			return err
152
+		}
153
+		if needsSlavePropagation {
154
+			bind.Propagation = mount.PropagationRSlave
155
+		}
149 156
 
150 157
 		// #10618
151 158
 		_, tmpfsExists := hostConfig.Tmpfs[bind.Destination]
... ...
@@ -178,6 +186,13 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
178 178
 		if err != nil {
179 179
 			return errdefs.InvalidParameter(err)
180 180
 		}
181
+		needsSlavePropagation, err := daemon.validateBindDaemonRoot(mp.Spec)
182
+		if err != nil {
183
+			return err
184
+		}
185
+		if needsSlavePropagation {
186
+			mp.Propagation = mount.PropagationRSlave
187
+		}
181 188
 
182 189
 		if binds[mp.Destination] {
183 190
 			return duplicateMountPointError(cfg.Target)
184 191
new file mode 100644
... ...
@@ -0,0 +1,36 @@
0
+package daemon
1
+
2
+import (
3
+	"strings"
4
+
5
+	"github.com/docker/docker/api/types/mount"
6
+	"github.com/docker/docker/errdefs"
7
+	"github.com/pkg/errors"
8
+)
9
+
10
+// validateBindDaemonRoot ensures that if a given mountpoint's source is within
11
+// the daemon root path, that the propagation is setup to prevent a container
12
+// from holding private refereneces to a mount within the daemon root, which
13
+// can cause issues when the daemon attempts to remove the mountpoint.
14
+func (daemon *Daemon) validateBindDaemonRoot(m mount.Mount) (bool, error) {
15
+	if m.Type != mount.TypeBind {
16
+		return false, nil
17
+	}
18
+
19
+	// check if the source is within the daemon root, or if the daemon root is within the source
20
+	if !strings.HasPrefix(m.Source, daemon.root) && !strings.HasPrefix(daemon.root, m.Source) {
21
+		return false, nil
22
+	}
23
+
24
+	if m.BindOptions == nil {
25
+		return true, nil
26
+	}
27
+
28
+	switch m.BindOptions.Propagation {
29
+	case mount.PropagationRSlave, mount.PropagationRShared, "":
30
+		return m.BindOptions.Propagation == "", nil
31
+	default:
32
+	}
33
+
34
+	return false, errdefs.InvalidParameter(errors.Errorf(`invalid mount config: must use either propagation mode "rslave" or "rshared" when mount source is within the daemon root, daemon root: %q, bind mount source: %q, propagation: %q`, daemon.root, m.Source, m.BindOptions.Propagation))
35
+}
0 36
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package daemon
1
+
2
+import (
3
+	"path/filepath"
4
+	"testing"
5
+
6
+	"github.com/docker/docker/api/types/mount"
7
+)
8
+
9
+func TestBindDaemonRoot(t *testing.T) {
10
+	t.Parallel()
11
+	d := &Daemon{root: "/a/b/c/daemon"}
12
+	for _, test := range []struct {
13
+		desc      string
14
+		opts      *mount.BindOptions
15
+		needsProp bool
16
+		err       bool
17
+	}{
18
+		{desc: "nil propagation settings", opts: nil, needsProp: true, err: false},
19
+		{desc: "empty propagation settings", opts: &mount.BindOptions{}, needsProp: true, err: false},
20
+		{desc: "private propagation", opts: &mount.BindOptions{Propagation: mount.PropagationPrivate}, err: true},
21
+		{desc: "rprivate propagation", opts: &mount.BindOptions{Propagation: mount.PropagationRPrivate}, err: true},
22
+		{desc: "slave propagation", opts: &mount.BindOptions{Propagation: mount.PropagationSlave}, err: true},
23
+		{desc: "rslave propagation", opts: &mount.BindOptions{Propagation: mount.PropagationRSlave}, err: false, needsProp: false},
24
+		{desc: "shared propagation", opts: &mount.BindOptions{Propagation: mount.PropagationShared}, err: true},
25
+		{desc: "rshared propagation", opts: &mount.BindOptions{Propagation: mount.PropagationRSlave}, err: false, needsProp: false},
26
+	} {
27
+		t.Run(test.desc, func(t *testing.T) {
28
+			test := test
29
+			for desc, source := range map[string]string{
30
+				"source is root":    d.root,
31
+				"source is subpath": filepath.Join(d.root, "a", "b"),
32
+				"source is parent":  filepath.Dir(d.root),
33
+				"source is /":       "/",
34
+			} {
35
+				t.Run(desc, func(t *testing.T) {
36
+					mount := mount.Mount{
37
+						Type:        mount.TypeBind,
38
+						Source:      source,
39
+						BindOptions: test.opts,
40
+					}
41
+					needsProp, err := d.validateBindDaemonRoot(mount)
42
+					if (err != nil) != test.err {
43
+						t.Fatalf("expected err=%v, got: %v", test.err, err)
44
+					}
45
+					if test.err {
46
+						return
47
+					}
48
+					if test.needsProp != needsProp {
49
+						t.Fatalf("expected needsProp=%v, got: %v", test.needsProp, needsProp)
50
+					}
51
+				})
52
+			}
53
+		})
54
+	}
55
+}
... ...
@@ -3,6 +3,7 @@ package daemon // import "github.com/docker/docker/daemon"
3 3
 import (
4 4
 	"sort"
5 5
 
6
+	"github.com/docker/docker/api/types/mount"
6 7
 	"github.com/docker/docker/container"
7 8
 	"github.com/docker/docker/pkg/idtools"
8 9
 	"github.com/docker/docker/volume"
... ...
@@ -44,3 +45,7 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er
44 44
 func setBindModeIfNull(bind *volume.MountPoint) {
45 45
 	return
46 46
 }
47
+
48
+func (daemon *Daemon) validateBindDaemonRoot(m mount.Mount) (bool, error) {
49
+	return false, nil
50
+}
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"bytes"
5 5
 	"context"
6 6
 	"fmt"
7
+	"path/filepath"
7 8
 	"testing"
8 9
 
9 10
 	"github.com/docker/docker/api/types"
... ...
@@ -12,6 +13,7 @@ import (
12 12
 	"github.com/docker/docker/api/types/network"
13 13
 	"github.com/docker/docker/client"
14 14
 	"github.com/docker/docker/integration-cli/daemon"
15
+	"github.com/docker/docker/integration/util/request"
15 16
 	"github.com/docker/docker/pkg/stdcopy"
16 17
 	"github.com/docker/docker/pkg/system"
17 18
 	"github.com/gotestyourself/gotestyourself/fs"
... ...
@@ -51,10 +53,10 @@ func TestContainerShmNoLeak(t *testing.T) {
51 51
 	hc := container.HostConfig{
52 52
 		Mounts: []mount.Mount{
53 53
 			{
54
-				Type:        mount.TypeBind,
55
-				Source:      d.Root,
56
-				Target:      "/testdaemonroot",
57
-				BindOptions: &mount.BindOptions{Propagation: mount.PropagationRPrivate}},
54
+				Type:   mount.TypeBind,
55
+				Source: d.Root,
56
+				Target: "/testdaemonroot",
57
+			},
58 58
 		},
59 59
 	}
60 60
 	cfg.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("mount | grep testdaemonroot | grep containers | grep %s", ctr.ID)}
... ...
@@ -150,3 +152,129 @@ func TestContainerNetworkMountsNoChown(t *testing.T) {
150 150
 	require.NoError(t, err)
151 151
 	assert.Equal(t, uint32(0), statT.UID(), "bind mounted network file should not change ownership from root")
152 152
 }
153
+
154
+func TestMountDaemonRoot(t *testing.T) {
155
+	t.Parallel()
156
+
157
+	client := request.NewAPIClient(t)
158
+	ctx := context.Background()
159
+	info, err := client.Info(ctx)
160
+	if err != nil {
161
+		t.Fatal(err)
162
+	}
163
+
164
+	for _, test := range []struct {
165
+		desc        string
166
+		propagation mount.Propagation
167
+		expected    mount.Propagation
168
+	}{
169
+		{
170
+			desc:        "default",
171
+			propagation: "",
172
+			expected:    mount.PropagationRSlave,
173
+		},
174
+		{
175
+			desc:        "private",
176
+			propagation: mount.PropagationPrivate,
177
+		},
178
+		{
179
+			desc:        "rprivate",
180
+			propagation: mount.PropagationRPrivate,
181
+		},
182
+		{
183
+			desc:        "slave",
184
+			propagation: mount.PropagationSlave,
185
+		},
186
+		{
187
+			desc:        "rslave",
188
+			propagation: mount.PropagationRSlave,
189
+			expected:    mount.PropagationRSlave,
190
+		},
191
+		{
192
+			desc:        "shared",
193
+			propagation: mount.PropagationShared,
194
+		},
195
+		{
196
+			desc:        "rshared",
197
+			propagation: mount.PropagationRShared,
198
+			expected:    mount.PropagationRShared,
199
+		},
200
+	} {
201
+		t.Run(test.desc, func(t *testing.T) {
202
+			test := test
203
+			t.Parallel()
204
+
205
+			propagationSpec := fmt.Sprintf(":%s", test.propagation)
206
+			if test.propagation == "" {
207
+				propagationSpec = ""
208
+			}
209
+			bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec
210
+			bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec
211
+
212
+			for name, hc := range map[string]*container.HostConfig{
213
+				"bind root":    {Binds: []string{bindSpecRoot}},
214
+				"bind subpath": {Binds: []string{bindSpecSub}},
215
+				"mount root": {
216
+					Mounts: []mount.Mount{
217
+						{
218
+							Type:        mount.TypeBind,
219
+							Source:      info.DockerRootDir,
220
+							Target:      "/foo",
221
+							BindOptions: &mount.BindOptions{Propagation: test.propagation},
222
+						},
223
+					},
224
+				},
225
+				"mount subpath": {
226
+					Mounts: []mount.Mount{
227
+						{
228
+							Type:        mount.TypeBind,
229
+							Source:      filepath.Join(info.DockerRootDir, "containers"),
230
+							Target:      "/foo",
231
+							BindOptions: &mount.BindOptions{Propagation: test.propagation},
232
+						},
233
+					},
234
+				},
235
+			} {
236
+				t.Run(name, func(t *testing.T) {
237
+					hc := hc
238
+					t.Parallel()
239
+
240
+					c, err := client.ContainerCreate(ctx, &container.Config{
241
+						Image: "busybox",
242
+						Cmd:   []string{"true"},
243
+					}, hc, nil, "")
244
+
245
+					if err != nil {
246
+						if test.expected != "" {
247
+							t.Fatal(err)
248
+						}
249
+						// expected an error, so this is ok and should not continue
250
+						return
251
+					}
252
+					if test.expected == "" {
253
+						t.Fatal("expected create to fail")
254
+					}
255
+
256
+					defer func() {
257
+						if err := client.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{Force: true}); err != nil {
258
+							panic(err)
259
+						}
260
+					}()
261
+
262
+					inspect, err := client.ContainerInspect(ctx, c.ID)
263
+					if err != nil {
264
+						t.Fatal(err)
265
+					}
266
+					if len(inspect.Mounts) != 1 {
267
+						t.Fatalf("unexpected number of mounts: %+v", inspect.Mounts)
268
+					}
269
+
270
+					m := inspect.Mounts[0]
271
+					if m.Propagation != test.expected {
272
+						t.Fatalf("got unexpected propagation mode, expected %q, got: %v", test.expected, m.Propagation)
273
+					}
274
+				})
275
+			}
276
+		})
277
+	}
278
+}