diff --git a/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go b/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go index 5aa2a078..89771c38 100644 --- a/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go +++ b/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "runtime" "github.com/mndrix/tap-go" @@ -10,16 +11,7 @@ import ( "github.com/opencontainers/runtime-tools/validation/util" ) -func testHugetlbCgroups() error { - t := tap.New() - t.Header(0) - defer t.AutoPlan() - - pageSizes, err := cgroups.GetHugePageSize() - if err != nil { - t.Fail(fmt.Sprintf("error when getting hugepage sizes: %+v", err)) - } - +func testHugetlbCgroups(t *tap.T, pageSizes []string) { // When setting the limit just for checking if writing works, the amount of memory // requested does not matter, as all insigned integers will be accepted. // Use 2GiB as an example @@ -28,7 +20,7 @@ func testHugetlbCgroups() error { for _, pageSize := range pageSizes { g, err := util.GetDefaultGenerator() if err != nil { - return err + util.Fatal(err) } g.SetLinuxCgroupsPath(cgroups.AbsCgroupPath) g.AddLinuxResourcesHugepageLimit(pageSize, limit) @@ -51,14 +43,12 @@ func testHugetlbCgroups() error { return nil }) if err != nil { - return err + t.Fail(err.Error()) } } - - return nil } -func testWrongHugetlb() error { +func testWrongHugetlb(t *tap.T) { // We deliberately set the page size to a wrong value, "3MB", to see // if the container really returns an error. Page sizes will always be a // on the format 2^(integer) @@ -67,24 +57,17 @@ func testWrongHugetlb() error { g, err := util.GetDefaultGenerator() if err != nil { - return err + util.Fatal(err) } - t := tap.New() - t.Header(0) - defer t.AutoPlan() - g.SetLinuxCgroupsPath(cgroups.AbsCgroupPath) g.AddLinuxResourcesHugepageLimit(page, limit) - err = util.RuntimeOutsideValidate(g, t, func(config *rspec.Spec, t *tap.T, state *rspec.State) error { - return nil - }) + err = util.RuntimeOutsideValidate(g, t, nil) t.Ok(err != nil, "hugepage invalid pagesize results in an errror") if err == nil { t.Diagnosticf("expect: err != nil, actual: err == nil") } - return err } func main() { @@ -92,11 +75,19 @@ func main() { util.Fatal(fmt.Errorf("linux-specific cgroup test")) } - if err := testHugetlbCgroups(); err != nil { - util.Fatal(err) + pageSizes, err := util.GetHugetlbPageSizes() + if err != nil { + util.Fatal(fmt.Errorf("error when getting hugepage sizes: %+v", err)) } - - if err := testWrongHugetlb(); err == nil { - util.Fatal(err) + if len(pageSizes) == 0 { + util.Skip("no huge pages or hugetlb cgroup controller on the host", nil) + os.Exit(0) } + + t := tap.New() + t.Header(0) + defer t.AutoPlan() + + testHugetlbCgroups(t, pageSizes) + testWrongHugetlb(t) } diff --git a/validation/linux_cgroups_relative_hugetlb/linux_cgroups_relative_hugetlb.go b/validation/linux_cgroups_relative_hugetlb/linux_cgroups_relative_hugetlb.go index 8ff7fa59..336df2f1 100644 --- a/validation/linux_cgroups_relative_hugetlb/linux_cgroups_relative_hugetlb.go +++ b/validation/linux_cgroups_relative_hugetlb/linux_cgroups_relative_hugetlb.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "github.com/mndrix/tap-go" rspec "github.com/opencontainers/runtime-spec/specs-go" @@ -10,14 +11,19 @@ import ( ) func main() { + pageSizes, err := util.GetHugetlbPageSizes() + if err != nil { + util.Fatal(fmt.Errorf("error when getting hugepage sizes: %+v", err)) + } + if len(pageSizes) == 0 { + util.Skip("no huge pages or hugetlb cgroup controller on the host", nil) + os.Exit(0) + } + t := tap.New() t.Header(0) defer t.AutoPlan() - pageSizes, err := cgroups.GetHugePageSize() - if err != nil { - t.Fail(fmt.Sprintf("error when getting hugepage sizes: %+v", err)) - } // When setting the limit just for checking if writing works, the amount of memory // requested does not matter, as all insigned integers will be accepted. // Use 2GiB as an example diff --git a/validation/process_rlimits/process_rlimits.go b/validation/process_rlimits/process_rlimits.go index 21e8d4cb..ab7c897c 100644 --- a/validation/process_rlimits/process_rlimits.go +++ b/validation/process_rlimits/process_rlimits.go @@ -18,15 +18,30 @@ func main() { util.Fatal(err) } + large, err := util.LargeRlimitsSupported() + if err != nil { + util.Fatal(err) + } + var gigaBytes uint64 = 1024 * 1024 * 1024 - g.AddProcessRlimits("RLIMIT_AS", 2*gigaBytes, 1*gigaBytes) - g.AddProcessRlimits("RLIMIT_CORE", 4*gigaBytes, 3*gigaBytes) - g.AddProcessRlimits("RLIMIT_DATA", 6*gigaBytes, 5*gigaBytes) - g.AddProcessRlimits("RLIMIT_FSIZE", 8*gigaBytes, 7*gigaBytes) - g.AddProcessRlimits("RLIMIT_STACK", 10*gigaBytes, 9*gigaBytes) + for _, r := range []struct { + rlimit string + hard, soft uint64 + }{ + {"RLIMIT_AS", 2 * gigaBytes, 1 * gigaBytes}, + {"RLIMIT_CORE", 4 * gigaBytes, 3 * gigaBytes}, + {"RLIMIT_DATA", 6 * gigaBytes, 5 * gigaBytes}, + {"RLIMIT_FSIZE", 8 * gigaBytes, 7 * gigaBytes}, + {"RLIMIT_STACK", 10 * gigaBytes, 9 * gigaBytes}, + {"RLIMIT_CPU", 120, 60}, // seconds + {"RLIMIT_NOFILE", 4000, 3000}, // number of files + } { + if !large && r.hard >= 4*gigaBytes { + continue + } + g.AddProcessRlimits(r.rlimit, r.hard, r.soft) + } - g.AddProcessRlimits("RLIMIT_CPU", 120, 60) // seconds - g.AddProcessRlimits("RLIMIT_NOFILE", 4000, 3000) // number of files err = util.RuntimeInsideValidate(g, nil, nil) if err != nil { util.Fatal(err) diff --git a/validation/util/linux_resources_hugetlb.go b/validation/util/linux_resources_hugetlb.go new file mode 100644 index 00000000..78796cea --- /dev/null +++ b/validation/util/linux_resources_hugetlb.go @@ -0,0 +1,50 @@ +package util + +import ( + "bufio" + "errors" + "io" + "os" + "strings" + + "github.com/opencontainers/runtime-tools/cgroups" +) + +// GetHugetlbPageSizes returns the huge page sizes of the host. The list is +// empty when the kernel offers no huge pages or no hugetlb cgroup +// controller, in which case no runtime can apply hugepage limits. +func GetHugetlbPageSizes() ([]string, error) { + enabled, err := cgroupControllerEnabled("hugetlb") + if err != nil || !enabled { + return nil, err + } + + pageSizes, err := cgroups.GetHugePageSize() + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return pageSizes, err +} + +// cgroupControllerEnabled reports whether the kernel provides the named +// cgroup controller, as listed in /proc/cgroups for both cgroup versions. +func cgroupControllerEnabled(name string) (bool, error) { + f, err := os.Open("/proc/cgroups") + if err != nil { + return false, err + } + defer f.Close() + + return controllerEnabled(f, name) +} + +func controllerEnabled(r io.Reader, name string) (bool, error) { + s := bufio.NewScanner(r) + for s.Scan() { + fields := strings.Fields(s.Text()) + if len(fields) == 4 && fields[0] == name { + return fields[3] == "1", nil + } + } + return false, s.Err() +} diff --git a/validation/util/linux_resources_hugetlb_test.go b/validation/util/linux_resources_hugetlb_test.go new file mode 100644 index 00000000..809cacd8 --- /dev/null +++ b/validation/util/linux_resources_hugetlb_test.go @@ -0,0 +1,33 @@ +package util + +import ( + "strings" + "testing" +) + +const procCgroups = `#subsys_name hierarchy num_cgroups enabled +cpuset 0 12 1 +cpu 0 12 1 +memory 0 12 0 +pids 0 12 1 +` + +func TestControllerEnabled(t *testing.T) { + for _, tc := range []struct { + name string + enabled bool + }{ + {"cpu", true}, + {"pids", true}, + {"memory", false}, + {"hugetlb", false}, + } { + enabled, err := controllerEnabled(strings.NewReader(procCgroups), tc.name) + if err != nil { + t.Fatalf("%s: %v", tc.name, err) + } + if enabled != tc.enabled { + t.Errorf("%s: enabled %v, want %v", tc.name, enabled, tc.enabled) + } + } +} diff --git a/validation/util/rlimit_linux.go b/validation/util/rlimit_linux.go new file mode 100644 index 00000000..61ee73a5 --- /dev/null +++ b/validation/util/rlimit_linux.go @@ -0,0 +1,34 @@ +package util + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +// LargeRlimitsSupported reports whether resource limits of 4 GiB and more +// can be set and read back on this host. A 32-bit kernel stores them in an +// unsigned long and turns any larger value into RLIM_INFINITY, and without +// CAP_SYS_RESOURCE a hard limit below 4 GiB cannot be raised to them. +func LargeRlimitsSupported() (bool, error) { + const probe = 1 << 32 + var old, cur unix.Rlimit + + if err := unix.Prlimit(0, unix.RLIMIT_FSIZE, nil, &old); err != nil { + return false, err + } + + cur = unix.Rlimit{Cur: probe, Max: max(old.Max, probe)} + if err := unix.Prlimit(0, unix.RLIMIT_FSIZE, &cur, nil); err != nil { + if errors.Is(err, unix.EPERM) { + return false, nil + } + return false, err + } + defer unix.Prlimit(0, unix.RLIMIT_FSIZE, &old, nil) + + if err := unix.Prlimit(0, unix.RLIMIT_FSIZE, nil, &cur); err != nil { + return false, err + } + return cur.Cur == probe, nil +} diff --git a/validation/util/rlimit_linux_test.go b/validation/util/rlimit_linux_test.go new file mode 100644 index 00000000..180d3e0d --- /dev/null +++ b/validation/util/rlimit_linux_test.go @@ -0,0 +1,24 @@ +package util + +import ( + "testing" + + "golang.org/x/sys/unix" +) + +func TestLargeRlimitsSupportedRestores(t *testing.T) { + var before, after unix.Rlimit + + if err := unix.Prlimit(0, unix.RLIMIT_FSIZE, nil, &before); err != nil { + t.Fatal(err) + } + if _, err := LargeRlimitsSupported(); err != nil { + t.Fatal(err) + } + if err := unix.Prlimit(0, unix.RLIMIT_FSIZE, nil, &after); err != nil { + t.Fatal(err) + } + if before != after { + t.Errorf("RLIMIT_FSIZE changed from %+v to %+v", before, after) + } +} diff --git a/validation/util/rlimit_other.go b/validation/util/rlimit_other.go new file mode 100644 index 00000000..a993fa83 --- /dev/null +++ b/validation/util/rlimit_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package util + +// LargeRlimitsSupported reports whether the host kernel holds resource +// limits of 4 GiB and more. +func LargeRlimitsSupported() (bool, error) { + return true, nil +}