Browse code

Inconsistent --tmpfs behavior

This fix tries to address the issue raised in #22420. When
`--tmpfs` is specified with `/tmp`, the default value is
`rw,nosuid,nodev,noexec,relatime,size=65536k`. When `--tmpfs`
is specified with `/tmp:rw`, then the value changed to
`rw,nosuid,nodev,noexec,relatime`.

The reason for such an inconsistency is because docker tries
to add `size=65536k` option only when user provides no option.

This fix tries to address this issue by always pre-progating
`size=65536k` along with `rw,nosuid,nodev,noexec,relatime`.
If user provides a different value (e.g., `size=8192k`), it
will override the `size=65536k` anyway since the combined
options will be parsed and merged to remove any duplicates.

Additional test cases have been added to cover the changes
in this fix.

This fix fixes #22420.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/05/01 11:42:19
Showing 4 changed files
... ...
@@ -480,14 +480,17 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c
480 480
 		}
481 481
 
482 482
 		if m.Source == "tmpfs" {
483
-			opt := []string{"noexec", "nosuid", "nodev", volume.DefaultPropagationMode}
483
+			options := []string{"noexec", "nosuid", "nodev", volume.DefaultPropagationMode, "size=65536k"}
484 484
 			if m.Data != "" {
485
-				opt = append(opt, strings.Split(m.Data, ",")...)
486
-			} else {
487
-				opt = append(opt, "size=65536k")
485
+				options = append(options, strings.Split(m.Data, ",")...)
488 486
 			}
489 487
 
490
-			s.Mounts = append(s.Mounts, specs.Mount{Destination: m.Destination, Source: m.Source, Type: "tmpfs", Options: opt})
488
+			merged, err := mount.MergeTmpfsOptions(options)
489
+			if err != nil {
490
+				return err
491
+			}
492
+
493
+			s.Mounts = append(s.Mounts, specs.Mount{Destination: m.Destination, Source: m.Source, Type: "tmpfs", Options: merged})
491 494
 			continue
492 495
 		}
493 496
 
... ...
@@ -824,6 +824,45 @@ func (s *DockerSuite) TestRunTmpfsMounts(c *check.C) {
824 824
 	}
825 825
 }
826 826
 
827
+// Test case for #22420
828
+func (s *DockerSuite) TestRunTmpfsMountsWithOptions(c *check.C) {
829
+	testRequires(c, DaemonIsLinux)
830
+
831
+	expectedOptions := []string{"rw", "nosuid", "nodev", "noexec", "relatime", "size=65536k"}
832
+	out, _ := dockerCmd(c, "run", "--tmpfs", "/tmp", "busybox", "sh", "-c", "mount | grep 'tmpfs on /tmp'")
833
+	for _, option := range expectedOptions {
834
+		c.Assert(out, checker.Contains, option)
835
+	}
836
+
837
+	expectedOptions = []string{"rw", "nosuid", "nodev", "noexec", "relatime", "size=65536k"}
838
+	out, _ = dockerCmd(c, "run", "--tmpfs", "/tmp:rw", "busybox", "sh", "-c", "mount | grep 'tmpfs on /tmp'")
839
+	for _, option := range expectedOptions {
840
+		c.Assert(out, checker.Contains, option)
841
+	}
842
+
843
+	expectedOptions = []string{"rw", "nosuid", "nodev", "relatime", "size=8192k"}
844
+	out, _ = dockerCmd(c, "run", "--tmpfs", "/tmp:rw,exec,size=8192k", "busybox", "sh", "-c", "mount | grep 'tmpfs on /tmp'")
845
+	for _, option := range expectedOptions {
846
+		c.Assert(out, checker.Contains, option)
847
+	}
848
+
849
+	expectedOptions = []string{"rw", "nosuid", "nodev", "noexec", "relatime", "size=4096k"}
850
+	out, _ = dockerCmd(c, "run", "--tmpfs", "/tmp:rw,size=8192k,exec,size=4096k,noexec", "busybox", "sh", "-c", "mount | grep 'tmpfs on /tmp'")
851
+	for _, option := range expectedOptions {
852
+		c.Assert(out, checker.Contains, option)
853
+	}
854
+
855
+	// We use debian:jessie as there is no findmnt in busybox. Also the output will be in the format of
856
+	// TARGET PROPAGATION
857
+	// /tmp   shared
858
+	// so we only capture `shared` here.
859
+	expectedOptions = []string{"shared"}
860
+	out, _ = dockerCmd(c, "run", "--tmpfs", "/tmp:shared", "debian:jessie", "findmnt", "-o", "TARGET,PROPAGATION", "/tmp")
861
+	for _, option := range expectedOptions {
862
+		c.Assert(out, checker.Contains, option)
863
+	}
864
+}
865
+
827 866
 func (s *DockerSuite) TestRunSysctls(c *check.C) {
828 867
 
829 868
 	testRequires(c, DaemonIsLinux)
... ...
@@ -5,6 +5,112 @@ import (
5 5
 	"strings"
6 6
 )
7 7
 
8
+var flags = map[string]struct {
9
+	clear bool
10
+	flag  int
11
+}{
12
+	"defaults":      {false, 0},
13
+	"ro":            {false, RDONLY},
14
+	"rw":            {true, RDONLY},
15
+	"suid":          {true, NOSUID},
16
+	"nosuid":        {false, NOSUID},
17
+	"dev":           {true, NODEV},
18
+	"nodev":         {false, NODEV},
19
+	"exec":          {true, NOEXEC},
20
+	"noexec":        {false, NOEXEC},
21
+	"sync":          {false, SYNCHRONOUS},
22
+	"async":         {true, SYNCHRONOUS},
23
+	"dirsync":       {false, DIRSYNC},
24
+	"remount":       {false, REMOUNT},
25
+	"mand":          {false, MANDLOCK},
26
+	"nomand":        {true, MANDLOCK},
27
+	"atime":         {true, NOATIME},
28
+	"noatime":       {false, NOATIME},
29
+	"diratime":      {true, NODIRATIME},
30
+	"nodiratime":    {false, NODIRATIME},
31
+	"bind":          {false, BIND},
32
+	"rbind":         {false, RBIND},
33
+	"unbindable":    {false, UNBINDABLE},
34
+	"runbindable":   {false, RUNBINDABLE},
35
+	"private":       {false, PRIVATE},
36
+	"rprivate":      {false, RPRIVATE},
37
+	"shared":        {false, SHARED},
38
+	"rshared":       {false, RSHARED},
39
+	"slave":         {false, SLAVE},
40
+	"rslave":        {false, RSLAVE},
41
+	"relatime":      {false, RELATIME},
42
+	"norelatime":    {true, RELATIME},
43
+	"strictatime":   {false, STRICTATIME},
44
+	"nostrictatime": {true, STRICTATIME},
45
+}
46
+
47
+var validFlags = map[string]bool{
48
+	"":          true,
49
+	"size":      true,
50
+	"mode":      true,
51
+	"uid":       true,
52
+	"gid":       true,
53
+	"nr_inodes": true,
54
+	"nr_blocks": true,
55
+	"mpol":      true,
56
+}
57
+
58
+var propagationFlags = map[string]bool{
59
+	"bind":        true,
60
+	"rbind":       true,
61
+	"unbindable":  true,
62
+	"runbindable": true,
63
+	"private":     true,
64
+	"rprivate":    true,
65
+	"shared":      true,
66
+	"rshared":     true,
67
+	"slave":       true,
68
+	"rslave":      true,
69
+}
70
+
71
+// MergeTmpfsOptions merge mount options to make sure there is no duplicate.
72
+func MergeTmpfsOptions(options []string) ([]string, error) {
73
+	// We use collisions maps to remove duplicates.
74
+	// For flag, the key is the flag value (the key for propagation flag is -1)
75
+	// For data=value, the key is the data
76
+	flagCollisions := map[int]bool{}
77
+	dataCollisions := map[string]bool{}
78
+
79
+	var newOptions []string
80
+	// We process in reverse order
81
+	for i := len(options) - 1; i >= 0; i-- {
82
+		option := options[i]
83
+		if option == "defaults" {
84
+			continue
85
+		}
86
+		if f, ok := flags[option]; ok && f.flag != 0 {
87
+			// There is only one propagation mode
88
+			key := f.flag
89
+			if propagationFlags[option] {
90
+				key = -1
91
+			}
92
+			// Check to see if there is collision for flag
93
+			if !flagCollisions[key] {
94
+				// We prepend the option and add to collision map
95
+				newOptions = append([]string{option}, newOptions...)
96
+				flagCollisions[key] = true
97
+			}
98
+			continue
99
+		}
100
+		opt := strings.SplitN(option, "=", 2)
101
+		if len(opt) != 2 || !validFlags[opt[0]] {
102
+			return nil, fmt.Errorf("Invalid tmpfs option %q", opt)
103
+		}
104
+		if !dataCollisions[opt[0]] {
105
+			// We prepend the option and add to collision map
106
+			newOptions = append([]string{option}, newOptions...)
107
+			dataCollisions[opt[0]] = true
108
+		}
109
+	}
110
+
111
+	return newOptions, nil
112
+}
113
+
8 114
 // Parse fstab type mount options into mount() flags
9 115
 // and device specific data
10 116
 func parseOptions(options string) (int, string) {
... ...
@@ -13,45 +119,6 @@ func parseOptions(options string) (int, string) {
13 13
 		data []string
14 14
 	)
15 15
 
16
-	flags := map[string]struct {
17
-		clear bool
18
-		flag  int
19
-	}{
20
-		"defaults":      {false, 0},
21
-		"ro":            {false, RDONLY},
22
-		"rw":            {true, RDONLY},
23
-		"suid":          {true, NOSUID},
24
-		"nosuid":        {false, NOSUID},
25
-		"dev":           {true, NODEV},
26
-		"nodev":         {false, NODEV},
27
-		"exec":          {true, NOEXEC},
28
-		"noexec":        {false, NOEXEC},
29
-		"sync":          {false, SYNCHRONOUS},
30
-		"async":         {true, SYNCHRONOUS},
31
-		"dirsync":       {false, DIRSYNC},
32
-		"remount":       {false, REMOUNT},
33
-		"mand":          {false, MANDLOCK},
34
-		"nomand":        {true, MANDLOCK},
35
-		"atime":         {true, NOATIME},
36
-		"noatime":       {false, NOATIME},
37
-		"diratime":      {true, NODIRATIME},
38
-		"nodiratime":    {false, NODIRATIME},
39
-		"bind":          {false, BIND},
40
-		"rbind":         {false, RBIND},
41
-		"unbindable":    {false, UNBINDABLE},
42
-		"runbindable":   {false, RUNBINDABLE},
43
-		"private":       {false, PRIVATE},
44
-		"rprivate":      {false, RPRIVATE},
45
-		"shared":        {false, SHARED},
46
-		"rshared":       {false, RSHARED},
47
-		"slave":         {false, SLAVE},
48
-		"rslave":        {false, RSLAVE},
49
-		"relatime":      {false, RELATIME},
50
-		"norelatime":    {true, RELATIME},
51
-		"strictatime":   {false, STRICTATIME},
52
-		"nostrictatime": {true, STRICTATIME},
53
-	}
54
-
55 16
 	for _, o := range strings.Split(options, ",") {
56 17
 		// If the option does not exist in the flags table or the flag
57 18
 		// is not supported on the platform,
... ...
@@ -72,16 +139,6 @@ func parseOptions(options string) (int, string) {
72 72
 // ParseTmpfsOptions parse fstab type mount options into flags and data
73 73
 func ParseTmpfsOptions(options string) (int, string, error) {
74 74
 	flags, data := parseOptions(options)
75
-	validFlags := map[string]bool{
76
-		"":          true,
77
-		"size":      true,
78
-		"mode":      true,
79
-		"uid":       true,
80
-		"gid":       true,
81
-		"nr_inodes": true,
82
-		"nr_blocks": true,
83
-		"mpol":      true,
84
-	}
85 75
 	for _, o := range strings.Split(data, ",") {
86 76
 		opt := strings.SplitN(o, "=", 2)
87 77
 		if !validFlags[opt[0]] {
... ...
@@ -137,3 +137,26 @@ func TestGetMounts(t *testing.T) {
137 137
 		t.Fatal("/ should be mounted at least")
138 138
 	}
139 139
 }
140
+
141
+func TestMergeTmpfsOptions(t *testing.T) {
142
+	options := []string{"noatime", "ro", "size=10k", "defaults", "atime", "defaults", "rw", "rprivate", "size=1024k", "slave"}
143
+	expected := []string{"atime", "rw", "size=1024k", "slave"}
144
+	merged, err := MergeTmpfsOptions(options)
145
+	if err != nil {
146
+		t.Fatal(err)
147
+	}
148
+	if len(expected) != len(merged) {
149
+		t.Fatalf("Expected %s got %s", expected, merged)
150
+	}
151
+	for index := range merged {
152
+		if merged[index] != expected[index] {
153
+			t.Fatalf("Expected %s for the %dth option, got %s", expected, index, merged)
154
+		}
155
+	}
156
+
157
+	options = []string{"noatime", "ro", "size=10k", "atime", "rw", "rprivate", "size=1024k", "slave", "size"}
158
+	_, err = MergeTmpfsOptions(options)
159
+	if err == nil {
160
+		t.Fatal("Expected error got nil")
161
+	}
162
+}