Merge pull request #14090 from samuelkarp/mask-interrupts

pkg/oci: mask thermal interrupt info
This commit is contained in:
Samuel Karp authored and GitHub committed 2026-09-10 17:03:31 +00:00
commit f6132dbe1f
6 files changed
+344 -17

No files matched your search

+18 -4
View File
@@ -18,16 +18,19 @@ package v2
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/containerd/log"
"github.com/containerd/typeurl/v2"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/pkg/identifiers"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/typeurl/v2"
"github.com/opencontainers/runtime-spec/specs-go"
)
// LoadBundle loads an existing bundle from disk
@@ -106,10 +109,21 @@ func NewBundle(ctx context.Context, root, state, id string, spec typeurl.Any) (b
// Spec may be nil for some sandboxers that do not initialize the spec, for
// example the shim sandboxer with a hostNetwork container.
if spec != nil {
if spec := spec.GetValue(); spec != nil {
if specVal := spec.GetValue(); specVal != nil {
logger := log.G(ctx)
if logger.Logger.IsLevelEnabled(log.TraceLevel) && typeurl.Is(spec, &specs.Spec{}) {
var s specs.Spec
if err := json.Unmarshal(specVal, &s); err == nil && s.Linux != nil {
logger.WithFields(log.Fields{
"bundle": b.Path,
"maskedPaths": s.Linux.MaskedPaths,
"readonlyPaths": s.Linux.ReadonlyPaths,
}).Trace("configured bundle paths")
}
}
// write the spec to the bundle
specPath := filepath.Join(b.Path, oci.ConfigFilename)
err = os.WriteFile(specPath, spec, 0666)
err = os.WriteFile(specPath, specVal, 0666)
if err != nil {
return nil, fmt.Errorf("failed to write bundle spec: %w", err)
}
+81 -13
View File
@@ -19,9 +19,15 @@ package oci
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -186,19 +192,7 @@ func populateDefaultUnixSpec(ctx context.Context, s *Spec, id string) error {
},
},
Linux: &specs.Linux{
MaskedPaths: []string{
"/proc/acpi",
"/proc/asound",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/sys/devices/virtual/powercap",
"/proc/scsi",
},
MaskedPaths: defaultLinuxMaskedPaths(),
ReadonlyPaths: []string{
"/proc/bus",
"/proc/fs",
@@ -264,3 +258,77 @@ func DescriptorToProto(d ocispec.Descriptor) *types.Descriptor {
Annotations: d.Annotations,
}
}
var cachedDefaultLinuxMaskedPaths = sync.OnceValue(func() []string {
maskedPaths := []string{
"/proc/acpi",
"/proc/asound",
"/proc/interrupts",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/sys/devices/virtual/powercap",
"/proc/scsi",
}
return appendCPUThrottlePaths(maskedPaths, possibleCPUs())
})
func defaultLinuxMaskedPaths() []string {
return slices.Clone(cachedDefaultLinuxMaskedPaths())
}
func possibleCPUs() []int {
if cpus, err := possibleCPUsParsed(); err == nil && len(cpus) > 0 {
return cpus
}
var cpus []int
for i := range runtime.NumCPU() {
cpus = append(cpus, i)
}
return cpus
}
func parsePossibleCPUs(content string) ([]int, error) {
if content == "" {
return nil, errors.New("empty possible cpu string")
}
ranges := strings.Split(content, ",")
var cpus []int
for _, r := range ranges {
if rStart, rEnd, ok := strings.Cut(r, "-"); ok {
start, err := strconv.Atoi(rStart)
if err != nil {
return nil, fmt.Errorf("invalid cpu range start %q: %w", rStart, err)
}
if start < 0 {
return nil, fmt.Errorf("negative cpu range start %d", start)
}
end, err := strconv.Atoi(rEnd)
if err != nil {
return nil, fmt.Errorf("invalid cpu range end %q: %w", rEnd, err)
}
if start > end {
return nil, fmt.Errorf("invalid cpu range %d-%d: start greater than end", start, end)
}
for i := start; i <= end; i++ {
cpus = append(cpus, i)
}
} else {
cpu, err := strconv.Atoi(rStart)
if err != nil {
return nil, fmt.Errorf("invalid cpu number %q: %w", rStart, err)
}
if cpu < 0 {
return nil, fmt.Errorf("negative cpu number %d", cpu)
}
cpus = append(cpus, cpu)
}
}
return cpus, nil
}
+42
View File
@@ -0,0 +1,42 @@
/*
Copyright The containerd 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 oci
import (
"fmt"
"os"
"strings"
"sync"
)
var possibleCPUsParsed = sync.OnceValues(func() ([]int, error) {
data, err := os.ReadFile("/sys/devices/system/cpu/possible")
if err != nil {
return nil, err
}
return parsePossibleCPUs(strings.TrimSpace(string(data)))
})
func appendCPUThrottlePaths(paths []string, cpus []int) []string {
for _, cpu := range cpus {
path := fmt.Sprintf("/sys/devices/system/cpu/cpu%d/thermal_throttle", cpu)
if _, err := os.Stat(path); err == nil {
paths = append(paths, path)
}
}
return paths
}
+97
View File
@@ -0,0 +1,97 @@
/*
Copyright The containerd 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 oci
import (
"errors"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPossibleCPUs_HostSysfs(t *testing.T) {
const possiblePath = "/sys/devices/system/cpu/possible"
data, err := os.ReadFile(possiblePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
t.Skipf("%s not present on host", possiblePath)
}
t.Fatalf("failed to read %s: %v", possiblePath, err)
}
content := strings.TrimSpace(string(data))
parsed, err := parsePossibleCPUs(content)
require.NoError(t, err, "host %s content %q failed to parse", possiblePath, content)
require.NotEmpty(t, parsed, "parsed CPU list should not be empty")
cachedParsed, err := possibleCPUsParsed()
require.NoError(t, err, "possibleCPUsParsed() returned error")
assert.Equal(t, parsed, cachedParsed, "possibleCPUsParsed() should match parsePossibleCPUs")
cpus := possibleCPUs()
assert.Equal(t, parsed, cpus, "possibleCPUs() should return parsed possible CPUs without falling back to NumCPU counting")
assert.GreaterOrEqual(t, len(cpus), runtime.NumCPU(), "possible CPUs count should be >= runtime.NumCPU()")
}
func TestDefaultLinuxMaskedPaths(t *testing.T) {
paths := defaultLinuxMaskedPaths()
assert.Contains(t, paths, "/proc/interrupts")
root := "/sys/devices/system/cpu"
entries, err := os.ReadDir(root)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
t.Skipf("%s not present on host", root)
}
t.Fatalf("failed to read %s: %v", root, err)
}
for _, entry := range entries {
name := entry.Name()
if !strings.HasPrefix(name, "cpu") {
continue
}
if _, err := strconv.Atoi(strings.TrimPrefix(name, "cpu")); err != nil {
continue
}
thermalPath := filepath.Join(root, name, "thermal_throttle")
if _, err := os.Stat(thermalPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
t.Fatalf("unexpected error stating %s: %v", thermalPath, err)
}
assert.Contains(t, paths, thermalPath)
}
}
func TestDefaultLinuxMaskedPaths_Clone(t *testing.T) {
paths1 := defaultLinuxMaskedPaths()
paths2 := defaultLinuxMaskedPaths()
require.Equal(t, paths1, paths2)
require.NotEmpty(t, paths1)
paths1[0] = "/mutated"
paths3 := defaultLinuxMaskedPaths()
assert.NotEqual(t, paths1[0], paths3[0])
}
+32
View File
@@ -0,0 +1,32 @@
//go:build !linux
/*
Copyright The containerd 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 oci
import (
"errors"
"sync"
)
var possibleCPUsParsed = sync.OnceValues(func() ([]int, error) {
return nil, errors.New("not supported")
})
func appendCPUThrottlePaths(paths []string, _ []int) []string {
return paths
}
+74
View File
@@ -30,6 +30,7 @@ import (
"github.com/containerd/continuity/fs/fstest"
"github.com/moby/sys/user"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -479,3 +480,76 @@ func TestGroupLookup_Symlink(t *testing.T) {
})
}
}
func TestParsePossibleCPUs(t *testing.T) {
tests := []struct {
name string
input string
expected []int
expectError bool
}{
{
name: "continuous range",
input: "0-3",
expected: []int{0, 1, 2, 3},
},
{
name: "large range",
input: "0-127",
expected: func() []int {
res := make([]int, 128)
for i := range res {
res[i] = i
}
return res
}(),
},
{
name: "non-continuous range",
input: "0-2,4,6-7",
expected: []int{0, 1, 2, 4, 6, 7},
},
{
name: "single cpu",
input: "5",
expected: []int{5},
},
{
name: "empty input",
input: "",
expectError: true,
},
{
name: "invalid range",
input: "0-2,invalid",
expectError: true,
},
{
name: "malformed range with multiple dashes",
input: "0-2-3",
expectError: true,
},
{
name: "reversed range",
input: "3-0",
expectError: true,
},
{
name: "negative start",
input: "-1-3",
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := parsePossibleCPUs(test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
}