Mask Linux thermal interrupt info in /proc and /sys.

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.

Refers to https://github.com/moby/moby/pull/49560

Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
This commit is contained in:
Sascha Grunert
2025-03-24 12:37:53 +01:00
parent 9cf0b2db70
commit b5fa540b21
5 changed files with 149 additions and 18 deletions

View File

@@ -17,6 +17,10 @@ limitations under the License.
package securitycontext
import (
"fmt"
"os"
"sync"
v1 "k8s.io/api/core/v1"
)
@@ -188,21 +192,32 @@ func AddNoNewPrivileges(sc *v1.SecurityContext) bool {
var (
// These *must* be kept in sync with moby/moby.
// https://github.com/moby/moby/blob/master/oci/defaults.go#L105-L124
// @jessfraz will watch changes to those files upstream.
defaultMaskedPaths = []string{
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap",
}
// https://github.com/moby/moby/blob/ecb03c4cdae6f323150fc11b303dcc5dc4d82416/oci/defaults.go#L190-L218
defaultMaskedPaths = sync.OnceValue(func() []string {
maskedPaths := []string{
"/proc/asound",
"/proc/acpi",
"/proc/interrupts",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap",
}
for _, cpu := range possibleCPUs() {
path := fmt.Sprintf("/sys/devices/system/cpu/cpu%d/thermal_throttle", cpu)
if _, err := os.Stat(path); err == nil {
maskedPaths = append(maskedPaths, path)
}
}
return maskedPaths
})
defaultReadonlyPaths = []string{
"/proc/bus",
"/proc/fs",
@@ -221,7 +236,7 @@ func ConvertToRuntimeMaskedPaths(opt *v1.ProcMountType) []string {
}
// Otherwise, add the default masked paths to the runtime security context.
return defaultMaskedPaths
return defaultMaskedPaths()
}
// ConvertToRuntimeReadonlyPaths converts the ProcMountType to the specified or default

View File

@@ -0,0 +1,21 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
func possibleCPUs() []int {
return nil
}

View File

@@ -0,0 +1,74 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
import (
"os"
"runtime"
"strconv"
"strings"
"sync"
)
// possibleCPUs returns the number of possible CPUs on this host.
func possibleCPUs() (cpus []int) {
if ncpu := possibleCPUsParsed(); ncpu != nil {
return ncpu
}
for i := range runtime.NumCPU() {
cpus = append(cpus, i)
}
return cpus
}
// possibleCPUsParsed is parsing the amount of possible CPUs on this host from
// /sys/devices.
var possibleCPUsParsed = sync.OnceValue(func() (cpus []int) {
data, err := os.ReadFile("/sys/devices/system/cpu/possible")
if err != nil {
return nil
}
ranges := strings.Split(strings.TrimSpace(string(data)), ",")
for _, r := range ranges {
if rStart, rEnd, ok := strings.Cut(r, "-"); !ok {
cpu, err := strconv.Atoi(rStart)
if err != nil {
return nil
}
cpus = append(cpus, cpu)
} else {
var start, end int
start, err := strconv.Atoi(rStart)
if err != nil {
return nil
}
end, err = strconv.Atoi(rEnd)
if err != nil {
return nil
}
for i := start; i <= end; i++ {
cpus = append(cpus, i)
}
}
}
return cpus
})

View File

@@ -73,11 +73,11 @@ func TestConvertToRuntimeMaskedPaths(t *testing.T) {
}{
"procMount nil": {
pm: nil,
expect: defaultMaskedPaths,
expect: defaultMaskedPaths(),
},
"procMount default": {
pm: &dPM,
expect: defaultMaskedPaths,
expect: defaultMaskedPaths(),
},
"procMount unmasked": {
pm: &uPM,

View File

@@ -0,0 +1,21 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
func possibleCPUs() []int {
return nil
}