Browse code

Fix denial of service with large numbers in cpuset-cpus and cpuset-mems

Using a value such as `--cpuset-mems=1-9223372036854775807` would cause
`dockerd` to run out of memory allocating a map of the values in the
validation code. Set limits to the normal limit of the number of CPUs,
and improve the error handling.

Reported by Huawei PSIRT.

Signed-off-by: Justin Cormack <justin.cormack@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Justin Cormack authored on 2018/09/04 23:49:09
Showing 4 changed files
... ...
@@ -482,14 +482,14 @@ func verifyContainerResources(resources *containertypes.Resources, sysInfo *sysi
482 482
 	}
483 483
 	cpusAvailable, err := sysInfo.IsCpusetCpusAvailable(resources.CpusetCpus)
484 484
 	if err != nil {
485
-		return warnings, fmt.Errorf("Invalid value %s for cpuset cpus", resources.CpusetCpus)
485
+		return warnings, errors.Wrapf(err, "Invalid value %s for cpuset cpus", resources.CpusetCpus)
486 486
 	}
487 487
 	if !cpusAvailable {
488 488
 		return warnings, fmt.Errorf("Requested CPUs are not available - requested %s, available: %s", resources.CpusetCpus, sysInfo.Cpus)
489 489
 	}
490 490
 	memsAvailable, err := sysInfo.IsCpusetMemsAvailable(resources.CpusetMems)
491 491
 	if err != nil {
492
-		return warnings, fmt.Errorf("Invalid value %s for cpuset mems", resources.CpusetMems)
492
+		return warnings, errors.Wrapf(err, "Invalid value %s for cpuset mems", resources.CpusetMems)
493 493
 	}
494 494
 	if !memsAvailable {
495 495
 		return warnings, fmt.Errorf("Requested memory nodes are not available - requested %s, available: %s", resources.CpusetMems, sysInfo.Mems)
... ...
@@ -18,6 +18,24 @@ func ParseKeyValueOpt(opt string) (string, string, error) {
18 18
 	return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil
19 19
 }
20 20
 
21
+// ParseUintListMaximum parses and validates the specified string as the value
22
+// found in some cgroup file (e.g. `cpuset.cpus`, `cpuset.mems`), which could be
23
+// one of the formats below. Note that duplicates are actually allowed in the
24
+// input string. It returns a `map[int]bool` with available elements from `val`
25
+// set to `true`. Values larger than `maximum` cause an error if max is non zero,
26
+// in order to stop the map becoming excessively large.
27
+// Supported formats:
28
+//     7
29
+//     1-6
30
+//     0,3-4,7,8-10
31
+//     0-0,0,1-7
32
+//     03,1-3      <- this is gonna get parsed as [1,2,3]
33
+//     3,2,1
34
+//     0-2,3,1
35
+func ParseUintListMaximum(val string, maximum int) (map[int]bool, error) {
36
+	return parseUintList(val, maximum)
37
+}
38
+
21 39
 // ParseUintList parses and validates the specified string as the value
22 40
 // found in some cgroup file (e.g. `cpuset.cpus`, `cpuset.mems`), which could be
23 41
 // one of the formats below. Note that duplicates are actually allowed in the
... ...
@@ -32,6 +50,10 @@ func ParseKeyValueOpt(opt string) (string, string, error) {
32 32
 //     3,2,1
33 33
 //     0-2,3,1
34 34
 func ParseUintList(val string) (map[int]bool, error) {
35
+	return parseUintList(val, 0)
36
+}
37
+
38
+func parseUintList(val string, maximum int) (map[int]bool, error) {
35 39
 	if val == "" {
36 40
 		return map[int]bool{}, nil
37 41
 	}
... ...
@@ -46,6 +68,9 @@ func ParseUintList(val string) (map[int]bool, error) {
46 46
 			if err != nil {
47 47
 				return nil, errInvalidFormat
48 48
 			}
49
+			if maximum != 0 && v > maximum {
50
+				return nil, fmt.Errorf("value of out range, maximum is %d", maximum)
51
+			}
49 52
 			availableInts[v] = true
50 53
 		} else {
51 54
 			split := strings.SplitN(r, "-", 2)
... ...
@@ -60,6 +85,9 @@ func ParseUintList(val string) (map[int]bool, error) {
60 60
 			if max < min {
61 61
 				return nil, errInvalidFormat
62 62
 			}
63
+			if maximum != 0 && max > maximum {
64
+				return nil, fmt.Errorf("value of out range, maximum is %d", maximum)
65
+			}
63 66
 			for i := min; i <= max; i++ {
64 67
 				availableInts[i] = true
65 68
 			}
... ...
@@ -68,3 +68,16 @@ func TestParseUintList(t *testing.T) {
68 68
 		}
69 69
 	}
70 70
 }
71
+
72
+func TestParseUintListMaximumLimits(t *testing.T) {
73
+	v := "10,1000"
74
+	if _, err := ParseUintListMaximum(v, 0); err != nil {
75
+		t.Fatalf("Expected not to fail, got %v", err)
76
+	}
77
+	if _, err := ParseUintListMaximum(v, 1000); err != nil {
78
+		t.Fatalf("Expected not to fail, got %v", err)
79
+	}
80
+	if out, err := ParseUintListMaximum(v, 100); err == nil {
81
+		t.Fatalf("Expected failure with %s but got %v", v, out)
82
+	}
83
+}
... ...
@@ -117,11 +117,19 @@ func (c cgroupCpusetInfo) IsCpusetMemsAvailable(provided string) (bool, error) {
117 117
 }
118 118
 
119 119
 func isCpusetListAvailable(provided, available string) (bool, error) {
120
-	parsedProvided, err := parsers.ParseUintList(provided)
120
+	parsedAvailable, err := parsers.ParseUintList(available)
121 121
 	if err != nil {
122 122
 		return false, err
123 123
 	}
124
-	parsedAvailable, err := parsers.ParseUintList(available)
124
+	// 8192 is the normal maximum number of CPUs in Linux, so accept numbers up to this
125
+	// or more if we actually have more CPUs.
126
+	max := 8192
127
+	for m := range parsedAvailable {
128
+		if m > max {
129
+			max = m
130
+		}
131
+	}
132
+	parsedProvided, err := parsers.ParseUintListMaximum(provided, max)
125 133
 	if err != nil {
126 134
 		return false, err
127 135
 	}