Skip to content

Commit

Permalink
Merge pull request moby#49193 from thaJeztah/internalize_pkg_parsers
Browse files Browse the repository at this point in the history
pkg/sysinfo: internalize parsing cpusets
  • Loading branch information
thaJeztah authored Jan 6, 2025
2 parents fe7b02d + 2282279 commit 120f616
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 59 deletions.
41 changes: 8 additions & 33 deletions pkg/sysinfo/sysinfo.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Package sysinfo stores information about which features a kernel supports.
package sysinfo // import "github.com/docker/docker/pkg/sysinfo"

import "github.com/docker/docker/pkg/parsers"

// Opt for New().
type Opt func(info *SysInfo)

Expand Down Expand Up @@ -122,10 +120,12 @@ type cgroupCpusetInfo struct {
// Whether Cpuset is supported or not
Cpuset bool

// Available Cpuset's cpus
// Available Cpuset's cpus as read from "cpuset.cpus.effective" (cgroups v2)
// or "cpuset.cpus" (cgroups v1).
Cpus string

// Available Cpuset's memory nodes
// Available Cpuset's memory nodes as read from "cpuset.mems.effective" (cgroups v2)
// or "cpuset.mems" (cgroups v1).
Mems string
}

Expand All @@ -137,38 +137,13 @@ type cgroupPids struct {
// IsCpusetCpusAvailable returns `true` if the provided string set is contained
// in cgroup's cpuset.cpus set, `false` otherwise.
// If error is not nil a parsing error occurred.
func (c cgroupCpusetInfo) IsCpusetCpusAvailable(provided string) (bool, error) {
return isCpusetListAvailable(provided, c.Cpus)
func (c cgroupCpusetInfo) IsCpusetCpusAvailable(requested string) (bool, error) {
return isCpusetListAvailable(requested, c.Cpus)
}

// IsCpusetMemsAvailable returns `true` if the provided string set is contained
// in cgroup's cpuset.mems set, `false` otherwise.
// If error is not nil a parsing error occurred.
func (c cgroupCpusetInfo) IsCpusetMemsAvailable(provided string) (bool, error) {
return isCpusetListAvailable(provided, c.Mems)
}

func isCpusetListAvailable(provided, available string) (bool, error) {
parsedAvailable, err := parsers.ParseUintList(available)
if err != nil {
return false, err
}
// 8192 is the normal maximum number of CPUs in Linux, so accept numbers up to this
// or more if we actually have more CPUs.
maxCPUs := 8192
for m := range parsedAvailable {
if m > maxCPUs {
maxCPUs = m
}
}
parsedProvided, err := parsers.ParseUintListMaximum(provided, maxCPUs)
if err != nil {
return false, err
}
for k := range parsedProvided {
if !parsedAvailable[k] {
return false, nil
}
}
return true, nil
func (c cgroupCpusetInfo) IsCpusetMemsAvailable(requested string) (bool, error) {
return isCpusetListAvailable(requested, c.Mems)
}
96 changes: 96 additions & 0 deletions pkg/sysinfo/sysinfo_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path"
"strconv"
"strings"
"sync"

Expand Down Expand Up @@ -314,3 +315,98 @@ func readProcBool(path string) bool {
}
return strings.TrimSpace(string(val)) == "1"
}

// defaultMaxCPUs is the normal maximum number of CPUs on Linux.
const defaultMaxCPUs = 8192

func isCpusetListAvailable(requested, available string) (bool, error) {
parsedAvailable, err := parseUintList(available, 0)
if err != nil {
return false, err
}
// Start with the normal maximum number of CPUs on Linux, but accept
// more if we actually have more CPUs available.
//
// This limit was added in f8e876d7616469d07b8b049ecb48967eeb8fa7a5
// to address CVE-2018-20699:
//
// 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.
//
// More details in https://github.com/docker-archive/engine/pull/70#issuecomment-458458288
maxCPUs := defaultMaxCPUs
for m := range parsedAvailable {
if m > maxCPUs {
maxCPUs = m
}
}
parsedRequested, err := parseUintList(requested, maxCPUs)
if err != nil {
return false, err
}
for k := range parsedRequested {
if !parsedAvailable[k] {
return false, nil
}
}
return true, nil
}

// parseUintList parses and validates the specified string as the value
// found in some cgroup file (e.g. `cpuset.cpus`, `cpuset.mems`), which could be
// one of the formats below. Note that duplicates are actually allowed in the
// input string. It returns a `map[int]bool` with available elements from `val`
// set to `true`. Values larger than `maximum` cause an error if max is non zero,
// in order to stop the map becoming excessively large.
// Supported formats:
//
// 7
// 1-6
// 0,3-4,7,8-10
// 0-0,0,1-7
// 03,1-3 <- this is gonna get parsed as [1,2,3]
// 3,2,1
// 0-2,3,1
func parseUintList(val string, maximum int) (map[int]bool, error) {
if val == "" {
return map[int]bool{}, nil
}

availableInts := make(map[int]bool)
split := strings.Split(val, ",")
errInvalidFormat := fmt.Errorf("invalid format: %s", val)

for _, r := range split {
if !strings.Contains(r, "-") {
v, err := strconv.Atoi(r)
if err != nil {
return nil, errInvalidFormat
}
if maximum != 0 && v > maximum {
return nil, fmt.Errorf("value of out range, maximum is %d", maximum)
}
availableInts[v] = true
} else {
minS, maxS, _ := strings.Cut(r, "-")
minAvailable, err := strconv.Atoi(minS)
if err != nil {
return nil, errInvalidFormat
}
maxAvailable, err := strconv.Atoi(maxS)
if err != nil {
return nil, errInvalidFormat
}
if maxAvailable < minAvailable {
return nil, errInvalidFormat
}
if maximum != 0 && maxAvailable > maximum {
return nil, fmt.Errorf("value of out range, maximum is %d", maximum)
}
for i := minAvailable; i <= maxAvailable; i++ {
availableInts[i] = true
}
}
}
return availableInts, nil
}
74 changes: 74 additions & 0 deletions pkg/sysinfo/sysinfo_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sysinfo // import "github.com/docker/docker/pkg/sysinfo"
import (
"os"
"path/filepath"
"reflect"
"testing"

"github.com/containerd/containerd/pkg/seccomp"
Expand Down Expand Up @@ -70,3 +71,76 @@ func TestNumCPU(t *testing.T) {
t.Fatal("CPU returned must be greater than zero")
}
}

func TestIsCpusetListAvailable(t *testing.T) {
cases := []struct {
provided string
available string
res bool
err bool
}{
{"1", "0-4", true, false},
{"01,3", "0-4", true, false},
{"", "0-7", true, false},
{"1--42", "0-7", false, true},
{"1-42", "00-1,8,,9", false, true},
{"1,41-42", "43,45", false, false},
{"0-3", "", false, false},
}
for _, c := range cases {
r, err := isCpusetListAvailable(c.provided, c.available)
if (c.err && err == nil) && r != c.res {
t.Fatalf("Expected pair: %v, %v for %s, %s. Got %v, %v instead", c.res, c.err, c.provided, c.available, (c.err && err == nil), r)
}
}
}

func TestParseUintList(t *testing.T) {
valids := map[string]map[int]bool{
"": {},
"7": {7: true},
"1-6": {1: true, 2: true, 3: true, 4: true, 5: true, 6: true},
"0-7": {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true},
"0,3-4,7,8-10": {0: true, 3: true, 4: true, 7: true, 8: true, 9: true, 10: true},
"0-0,0,1-4": {0: true, 1: true, 2: true, 3: true, 4: true},
"03,1-3": {1: true, 2: true, 3: true},
"3,2,1": {1: true, 2: true, 3: true},
"0-2,3,1": {0: true, 1: true, 2: true, 3: true},
}
for k, v := range valids {
out, err := parseUintList(k, 0)
if err != nil {
t.Fatalf("Expected not to fail, got %v", err)
}
if !reflect.DeepEqual(out, v) {
t.Fatalf("Expected %v, got %v", v, out)
}
}

invalids := []string{
"this",
"1--",
"1-10,,10",
"10-1",
"-1",
"-1,0",
}
for _, v := range invalids {
if out, err := parseUintList(v, 0); err == nil {
t.Fatalf("Expected failure with %s but got %v", v, out)
}
}
}

func TestParseUintListMaximumLimits(t *testing.T) {
v := "10,1000"
if _, err := parseUintList(v, 0); err != nil {
t.Fatalf("Expected not to fail, got %v", err)
}
if _, err := parseUintList(v, 1000); err != nil {
t.Fatalf("Expected not to fail, got %v", err)
}
if out, err := parseUintList(v, 100); err == nil {
t.Fatalf("Expected failure with %s but got %v", v, out)
}
}
4 changes: 4 additions & 0 deletions pkg/sysinfo/sysinfo_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ package sysinfo // import "github.com/docker/docker/pkg/sysinfo"
func New(options ...Opt) *SysInfo {
return &SysInfo{}
}

func isCpusetListAvailable(string, string) (bool, error) {
return false, nil
}
26 changes: 0 additions & 26 deletions pkg/sysinfo/sysinfo_test.go

This file was deleted.

0 comments on commit 120f616

Please sign in to comment.