On Linux, mask "/proc/interrupts" and "/sys/devices/system/cpu/cpu<x>/thermal_throttle"
inside containers by default. Privileged containers or containers started
with --security-opt="systempaths=unconfined" are not affected.
Mitigates potential Thermal Side-Channel Vulnerability Exploit
(https://github.com/moby/moby/security/advisories/GHSA-6fw5-f8r9-fgfm).
Also: improve integration test TestCreateWithCustomMaskedPaths() to ensure
default masked paths don't apply to privileged containers.
Signed-off-by: Cesar Talledo <cesar.talledo@docker.com>
| ... | ... |
@@ -258,6 +258,7 @@ func TestCreateWithCustomMaskedPaths(t *testing.T) {
|
| 258 | 258 |
apiClient := testEnv.APIClient() |
| 259 | 259 |
|
| 260 | 260 |
testCases := []struct {
|
| 261 |
+ privileged bool |
|
| 261 | 262 |
maskedPaths []string |
| 262 | 263 |
expected []string |
| 263 | 264 |
}{
|
| ... | ... |
@@ -273,6 +274,11 @@ func TestCreateWithCustomMaskedPaths(t *testing.T) {
|
| 273 | 273 |
maskedPaths: []string{"/proc/kcore", "/proc/keys"},
|
| 274 | 274 |
expected: []string{"/proc/kcore", "/proc/keys"},
|
| 275 | 275 |
}, |
| 276 |
+ {
|
|
| 277 |
+ privileged: true, |
|
| 278 |
+ maskedPaths: nil, |
|
| 279 |
+ expected: nil, |
|
| 280 |
+ }, |
|
| 276 | 281 |
} |
| 277 | 282 |
|
| 278 | 283 |
checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) {
|
| ... | ... |
@@ -286,15 +292,20 @@ func TestCreateWithCustomMaskedPaths(t *testing.T) {
|
| 286 | 286 |
cfg, ok := inspectJSON["HostConfig"].(map[string]interface{})
|
| 287 | 287 |
assert.Check(t, is.Equal(true, ok), name) |
| 288 | 288 |
|
| 289 |
- maskedPaths, ok := cfg["MaskedPaths"].([]interface{})
|
|
| 290 |
- assert.Check(t, is.Equal(true, ok), name) |
|
| 289 |
+ if expected != nil {
|
|
| 290 |
+ maskedPaths, ok := cfg["MaskedPaths"].([]interface{})
|
|
| 291 |
+ assert.Check(t, is.Equal(true, ok), name) |
|
| 291 | 292 |
|
| 292 |
- mps := make([]string, 0, len(maskedPaths)) |
|
| 293 |
- for _, mp := range maskedPaths {
|
|
| 294 |
- mps = append(mps, mp.(string)) |
|
| 295 |
- } |
|
| 293 |
+ mps := make([]string, 0, len(maskedPaths)) |
|
| 294 |
+ for _, mp := range maskedPaths {
|
|
| 295 |
+ mps = append(mps, mp.(string)) |
|
| 296 |
+ } |
|
| 296 | 297 |
|
| 297 |
- assert.DeepEqual(t, expected, mps) |
|
| 298 |
+ assert.DeepEqual(t, expected, mps) |
|
| 299 |
+ } else {
|
|
| 300 |
+ _, ok := cfg["MaskedPaths"].([]interface{})
|
|
| 301 |
+ assert.Check(t, is.Equal(false, ok), name) |
|
| 302 |
+ } |
|
| 298 | 303 |
} |
| 299 | 304 |
|
| 300 | 305 |
// TODO: This should be using subtests |
| ... | ... |
@@ -305,7 +316,9 @@ func TestCreateWithCustomMaskedPaths(t *testing.T) {
|
| 305 | 305 |
Image: "busybox", |
| 306 | 306 |
Cmd: []string{"true"},
|
| 307 | 307 |
} |
| 308 |
- hc := container.HostConfig{}
|
|
| 308 |
+ hc := container.HostConfig{
|
|
| 309 |
+ Privileged: tc.privileged, |
|
| 310 |
+ } |
|
| 309 | 311 |
if tc.maskedPaths != nil {
|
| 310 | 312 |
hc.MaskedPaths = tc.maskedPaths |
| 311 | 313 |
} |
| ... | ... |
@@ -2,6 +2,7 @@ package platform |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"context" |
| 5 |
+ "runtime" |
|
| 5 | 6 |
"sync" |
| 6 | 7 |
|
| 7 | 8 |
"github.com/containerd/log" |
| ... | ... |
@@ -29,3 +30,22 @@ func Architecture() string {
|
| 29 | 29 |
}) |
| 30 | 30 |
return arch |
| 31 | 31 |
} |
| 32 |
+ |
|
| 33 |
+// PossibleCPU returns the set of possible CPUs on the host (which is equal or |
|
| 34 |
+// larger to the number of CPUs currently online). The returned set may be a |
|
| 35 |
+// single CPU number ({0}), or a continuous range of CPU numbers ({0,1,2,3}), or
|
|
| 36 |
+// a non-continuous range of CPU numbers ({0,1,2,3,12,13,14,15}).
|
|
| 37 |
+func PossibleCPU() []int {
|
|
| 38 |
+ if ncpu := possibleCPUs(); ncpu != nil {
|
|
| 39 |
+ return ncpu |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ // Fallback in case possibleCPUs() fails. |
|
| 43 |
+ var cpus []int |
|
| 44 |
+ ncpu := runtime.NumCPU() |
|
| 45 |
+ for i := 0; i <= ncpu; i++ {
|
|
| 46 |
+ cpus = append(cpus, i) |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ return cpus |
|
| 50 |
+} |
| 32 | 51 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,55 @@ |
| 0 |
+package platform |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "os" |
|
| 4 |
+ "strconv" |
|
| 5 |
+ "strings" |
|
| 6 |
+ "sync" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// possibleCPUs returns the set of possible CPUs on the host (which is |
|
| 10 |
+// equal or larger to the number of CPUs currently online). The returned |
|
| 11 |
+// set may be a single number ({0}), or a continuous range ({0,1,2,3}), or
|
|
| 12 |
+// a non-continuous range ({0,1,2,3,12,13,14,15})
|
|
| 13 |
+// |
|
| 14 |
+// Returns nil on errors. Assume CPUs are 0 -> runtime.NumCPU() in that case. |
|
| 15 |
+var possibleCPUs = sync.OnceValue(func() []int {
|
|
| 16 |
+ data, err := os.ReadFile("/sys/devices/system/cpu/possible")
|
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return nil |
|
| 19 |
+ } |
|
| 20 |
+ content := strings.TrimSpace(string(data)) |
|
| 21 |
+ return parsePossibleCPUs(content) |
|
| 22 |
+}) |
|
| 23 |
+ |
|
| 24 |
+func parsePossibleCPUs(content string) []int {
|
|
| 25 |
+ ranges := strings.Split(content, ",") |
|
| 26 |
+ |
|
| 27 |
+ var cpus []int |
|
| 28 |
+ for _, r := range ranges {
|
|
| 29 |
+ // Each entry is either a single number (e.g., "0") or a continuous range |
|
| 30 |
+ // (e.g., "0-3"). |
|
| 31 |
+ if rStart, rEnd, ok := strings.Cut(r, "-"); !ok {
|
|
| 32 |
+ cpu, err := strconv.Atoi(rStart) |
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return nil |
|
| 35 |
+ } |
|
| 36 |
+ cpus = append(cpus, cpu) |
|
| 37 |
+ } else {
|
|
| 38 |
+ var start, end int |
|
| 39 |
+ start, err := strconv.Atoi(rStart) |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return nil |
|
| 42 |
+ } |
|
| 43 |
+ end, err = strconv.Atoi(rEnd) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ return nil |
|
| 46 |
+ } |
|
| 47 |
+ for i := start; i <= end; i++ {
|
|
| 48 |
+ cpus = append(cpus, i) |
|
| 49 |
+ } |
|
| 50 |
+ } |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ return cpus |
|
| 54 |
+} |
| 0 | 55 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,54 @@ |
| 0 |
+package platform |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ "gotest.tools/v3/assert" |
|
| 6 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func TestParsePossibleCPUs(t *testing.T) {
|
|
| 10 |
+ tests := []struct {
|
|
| 11 |
+ name string |
|
| 12 |
+ input string |
|
| 13 |
+ expected []int |
|
| 14 |
+ }{
|
|
| 15 |
+ {
|
|
| 16 |
+ name: "Continuous Range", |
|
| 17 |
+ input: "0-3", |
|
| 18 |
+ expected: []int{0, 1, 2, 3},
|
|
| 19 |
+ }, |
|
| 20 |
+ {
|
|
| 21 |
+ name: "Non-Continuous Range", |
|
| 22 |
+ input: "0-2,4,6-7", |
|
| 23 |
+ expected: []int{0, 1, 2, 4, 6, 7},
|
|
| 24 |
+ }, |
|
| 25 |
+ {
|
|
| 26 |
+ name: "Single CPU", |
|
| 27 |
+ input: "5", |
|
| 28 |
+ expected: []int{5},
|
|
| 29 |
+ }, |
|
| 30 |
+ {
|
|
| 31 |
+ name: "Empty Input", |
|
| 32 |
+ input: "", |
|
| 33 |
+ expected: nil, |
|
| 34 |
+ }, |
|
| 35 |
+ {
|
|
| 36 |
+ name: "Invalid Range", |
|
| 37 |
+ input: "0-2,invalid", |
|
| 38 |
+ expected: nil, |
|
| 39 |
+ }, |
|
| 40 |
+ {
|
|
| 41 |
+ name: "Malformed Range", |
|
| 42 |
+ input: "0-2-3", |
|
| 43 |
+ expected: nil, |
|
| 44 |
+ }, |
|
| 45 |
+ } |
|
| 46 |
+ |
|
| 47 |
+ for _, test := range tests {
|
|
| 48 |
+ t.Run(test.name, func(t *testing.T) {
|
|
| 49 |
+ result := parsePossibleCPUs(test.input) |
|
| 50 |
+ assert.Assert(t, is.DeepEqual(result, test.expected), "Expected %v but got %v", test.expected, result) |
|
| 51 |
+ }) |
|
| 52 |
+ } |
|
| 53 |
+} |
| ... | ... |
@@ -1,8 +1,12 @@ |
| 1 | 1 |
package oci // import "github.com/docker/docker/oci" |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "fmt" |
|
| 5 |
+ "os" |
|
| 4 | 6 |
"runtime" |
| 7 |
+ "sync" |
|
| 5 | 8 |
|
| 9 |
+ "github.com/docker/docker/internal/platform" |
|
| 6 | 10 |
"github.com/docker/docker/oci/caps" |
| 7 | 11 |
"github.com/opencontainers/runtime-spec/specs-go" |
| 8 | 12 |
) |
| ... | ... |
@@ -102,19 +106,7 @@ func DefaultLinuxSpec() specs.Spec {
|
| 102 | 102 |
}, |
| 103 | 103 |
}, |
| 104 | 104 |
Linux: &specs.Linux{
|
| 105 |
- MaskedPaths: []string{
|
|
| 106 |
- "/proc/asound", |
|
| 107 |
- "/proc/acpi", |
|
| 108 |
- "/proc/kcore", |
|
| 109 |
- "/proc/keys", |
|
| 110 |
- "/proc/latency_stats", |
|
| 111 |
- "/proc/timer_list", |
|
| 112 |
- "/proc/timer_stats", |
|
| 113 |
- "/proc/sched_debug", |
|
| 114 |
- "/proc/scsi", |
|
| 115 |
- "/sys/firmware", |
|
| 116 |
- "/sys/devices/virtual/powercap", |
|
| 117 |
- }, |
|
| 105 |
+ MaskedPaths: defaultLinuxMaskedPaths(), |
|
| 118 | 106 |
ReadonlyPaths: []string{
|
| 119 | 107 |
"/proc/bus", |
| 120 | 108 |
"/proc/fs", |
| ... | ... |
@@ -194,3 +186,33 @@ func DefaultLinuxSpec() specs.Spec {
|
| 194 | 194 |
}, |
| 195 | 195 |
} |
| 196 | 196 |
} |
| 197 |
+ |
|
| 198 |
+// defaultLinuxMaskedPaths returns the default list of paths to mask in a Linux |
|
| 199 |
+// container. The paths won't change while the docker daemon is running, so just |
|
| 200 |
+// compute them once. |
|
| 201 |
+var defaultLinuxMaskedPaths = sync.OnceValue(func() []string {
|
|
| 202 |
+ maskedPaths := []string{
|
|
| 203 |
+ "/proc/asound", |
|
| 204 |
+ "/proc/acpi", |
|
| 205 |
+ "/proc/interrupts", // https://github.com/moby/moby/security/advisories/GHSA-6fw5-f8r9-fgfm |
|
| 206 |
+ "/proc/kcore", |
|
| 207 |
+ "/proc/keys", |
|
| 208 |
+ "/proc/latency_stats", |
|
| 209 |
+ "/proc/timer_list", |
|
| 210 |
+ "/proc/timer_stats", |
|
| 211 |
+ "/proc/sched_debug", |
|
| 212 |
+ "/proc/scsi", |
|
| 213 |
+ "/sys/firmware", |
|
| 214 |
+ "/sys/devices/virtual/powercap", // https://github.com/moby/moby/security/advisories/GHSA-jq35-85cj-fj4p |
|
| 215 |
+ } |
|
| 216 |
+ |
|
| 217 |
+ // https://github.com/moby/moby/security/advisories/GHSA-6fw5-f8r9-fgfm |
|
| 218 |
+ cpus := platform.PossibleCPU() |
|
| 219 |
+ for _, cpu := range cpus {
|
|
| 220 |
+ path := fmt.Sprintf("/sys/devices/system/cpu/cpu%d/thermal_throttle", cpu)
|
|
| 221 |
+ if _, err := os.Stat(path); err == nil {
|
|
| 222 |
+ maskedPaths = append(maskedPaths, path) |
|
| 223 |
+ } |
|
| 224 |
+ } |
|
| 225 |
+ return maskedPaths |
|
| 226 |
+}) |