From 5acc138583f08ef2bd4fa8e6fb6248c93c784c52 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Wed, 21 Oct 2026 12:49:40 +0100 Subject: [PATCH 1/3] validation: skip the hugetlb tests without hugetlb support A kernel built without huge pages or without the hugetlb cgroup controller cannot apply hugepage limits, so no runtime can pass the hugetlb tests there; they failed on reading /sys/kernel/mm/hugepages. Skip both tests unless /proc/cgroups lists the hugetlb controller as enabled and the kernel offers at least one huge page size. Signed-off-by: Daniel Golle --- .../linux_cgroups_hugetlb.go | 19 ++++--- .../linux_cgroups_relative_hugetlb.go | 14 ++++-- validation/util/linux_resources_hugetlb.go | 50 +++++++++++++++++++ .../util/linux_resources_hugetlb_test.go | 33 ++++++++++++ 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 validation/util/linux_resources_hugetlb.go create mode 100644 validation/util/linux_resources_hugetlb_test.go diff --git a/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go b/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go index 5aa2a078..0e6bd470 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,11 @@ import ( "github.com/opencontainers/runtime-tools/validation/util" ) -func testHugetlbCgroups() error { +func testHugetlbCgroups(pageSizes []string) 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)) - } - // 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 @@ -92,7 +88,16 @@ func main() { util.Fatal(fmt.Errorf("linux-specific cgroup test")) } - if err := testHugetlbCgroups(); err != nil { + 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) + } + + if err := testHugetlbCgroups(pageSizes); err != nil { util.Fatal(err) } 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/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) + } + } +} From ac836a893071c66bc9b4d7da1861185864b17504 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Wed, 21 Oct 2026 12:50:04 +0100 Subject: [PATCH 2/3] validation/linux_cgroups_hugetlb: report failures to start a container When the runtime fails to create the container with a hugepage limit, testHugetlbCgroups() returns the error, its deferred AutoPlan() prints an empty plan and main() exits through util.Fatal(), so the failure never shows up as a failed test. The second check then does not run at all, and its own TAP header follows the first one in the same output. Report both checks on one TAP stream and record a failure to run the container as a failed test. Signed-off-by: Daniel Golle --- .../linux_cgroups_hugetlb.go | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go b/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go index 0e6bd470..89771c38 100644 --- a/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go +++ b/validation/linux_cgroups_hugetlb/linux_cgroups_hugetlb.go @@ -11,11 +11,7 @@ import ( "github.com/opencontainers/runtime-tools/validation/util" ) -func testHugetlbCgroups(pageSizes []string) error { - t := tap.New() - t.Header(0) - defer t.AutoPlan() - +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 @@ -24,7 +20,7 @@ func testHugetlbCgroups(pageSizes []string) 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) @@ -47,14 +43,12 @@ func testHugetlbCgroups(pageSizes []string) 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) @@ -63,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() { @@ -97,11 +84,10 @@ func main() { os.Exit(0) } - if err := testHugetlbCgroups(pageSizes); err != nil { - util.Fatal(err) - } + t := tap.New() + t.Header(0) + defer t.AutoPlan() - if err := testWrongHugetlb(); err == nil { - util.Fatal(err) - } + testHugetlbCgroups(t, pageSizes) + testWrongHugetlb(t) } From ffe110fdd75afd173985c246e42d174424513018 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Wed, 21 Oct 2026 12:51:26 +0100 Subject: [PATCH 3/3] validation/process_rlimits: skip limits the host cannot hold A 32-bit kernel stores resource limits in an unsigned long and turns any value of 4 GiB or more into RLIM_INFINITY, so the hard limits for RLIMIT_CORE, RLIMIT_DATA, RLIMIT_FSIZE and RLIMIT_STACK can never match there. Probe the host by setting a 4 GiB soft RLIMIT_FSIZE on the test itself, raising the hard limit as far as that needs, and reading it back; leave out the limits of 4 GiB and more when this fails. A 32-bit userland on a 64-bit kernel still tests all of them, and a runner without CAP_SYS_RESOURCE under a lower hard limit leaves them out, as a runtime it starts could not set them either. Signed-off-by: Daniel Golle --- validation/process_rlimits/process_rlimits.go | 29 ++++++++++++---- validation/util/rlimit_linux.go | 34 +++++++++++++++++++ validation/util/rlimit_linux_test.go | 24 +++++++++++++ validation/util/rlimit_other.go | 9 +++++ 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 validation/util/rlimit_linux.go create mode 100644 validation/util/rlimit_linux_test.go create mode 100644 validation/util/rlimit_other.go 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/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 +}