From f989a0b222d4d71656a1041b800bc5b6371e5263 Mon Sep 17 00:00:00 2001 From: divyanshu <86191568+justmorpheus@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:04:57 +0530 Subject: [PATCH] hugetlb: reject page sizes containing path separators The hugepage size from LinuxResources.HugepageLimits is used to build the hugetlb limit filename. A value containing path separators makes the resulting path resolve outside of the cgroup directory, and the write silently lands there. Return an error instead. For cgroup v2 the check is done in Value.write so it covers every filename written to the unified hierarchy. Valid page sizes (e.g. "2MB", "1GB") are unaffected. This matches the hardening applied in runc (opencontainers/runc#4103). Signed-off-by: divyanshu <86191568+justmorpheus@users.noreply.github.com> --- cgroup1/hugetlb.go | 7 ++++- cgroup1/hugetlb_test.go | 54 +++++++++++++++++++++++++++++++++++++++ cgroup2/hugetlbv2_test.go | 15 +++++++++++ cgroup2/manager.go | 3 +++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 cgroup1/hugetlb_test.go diff --git a/cgroup1/hugetlb.go b/cgroup1/hugetlb.go index 75519d9d..8d9be500 100644 --- a/cgroup1/hugetlb.go +++ b/cgroup1/hugetlb.go @@ -17,6 +17,7 @@ package cgroup1 import ( + "fmt" "os" "path/filepath" "strconv" @@ -56,8 +57,12 @@ func (h *hugetlbController) Create(path string, resources *specs.LinuxResources) return err } for _, limit := range resources.HugepageLimits { + filename := strings.Join([]string{"hugetlb", limit.Pagesize, "limit_in_bytes"}, ".") + if filepath.Base(filename) != filename { + return fmt.Errorf("invalid hugepage size %q", limit.Pagesize) + } if err := os.WriteFile( - filepath.Join(h.Path(path), strings.Join([]string{"hugetlb", limit.Pagesize, "limit_in_bytes"}, ".")), + filepath.Join(h.Path(path), filename), []byte(strconv.FormatUint(limit.Limit, 10)), defaultFilePerm, ); err != nil { diff --git a/cgroup1/hugetlb_test.go b/cgroup1/hugetlb_test.go new file mode 100644 index 00000000..6c42358b --- /dev/null +++ b/cgroup1/hugetlb_test.go @@ -0,0 +1,54 @@ +/* + 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 cgroup1 + +import ( + "os" + "path/filepath" + "testing" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +func TestHugetlbInvalidPageSize(t *testing.T) { + mock, err := newMock(t) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := mock.delete(); err != nil { + t.Errorf("failed delete: %v", err) + } + }() + h, err := NewHugetlb(mock.root) + if err != nil { + t.Fatal(err) + } + resources := specs.LinuxResources{ + HugepageLimits: []specs.LinuxHugepageLimit{ + // resolves to /escaped.limit_in_bytes + {Pagesize: "/../../../../escaped", Limit: 1337}, + }, + } + if err := h.Create("/a", &resources); err == nil { + t.Error("expected error for pagesize containing path separators") + } + escaped := filepath.Join(filepath.Dir(mock.root), "escaped.limit_in_bytes") + if _, err := os.Stat(escaped); !os.IsNotExist(err) { + t.Errorf("file written outside of cgroup root: %s", escaped) + } +} diff --git a/cgroup2/hugetlbv2_test.go b/cgroup2/hugetlbv2_test.go index d0252db5..88cd0242 100644 --- a/cgroup2/hugetlbv2_test.go +++ b/cgroup2/hugetlbv2_test.go @@ -19,6 +19,7 @@ package cgroup2 import ( "fmt" "os" + "path/filepath" "strconv" "testing" @@ -53,3 +54,17 @@ func TestCgroupv2HugetlbStats(t *testing.T) { } } } + +func TestCgroupv2HugetlbInvalidPageSize(t *testing.T) { + path := filepath.Join(t.TempDir(), "hugetlb-test-cg") + require.NoError(t, os.Mkdir(path, defaultDirPerm)) + res := Resources{ + // resolves to /escaped.max + HugeTlb: &HugeTlb{HugeTlbEntry{HugePageSize: "/../../escaped", Limit: 1337}}, + } + err := setResources(path, &res) + require.Error(t, err) + + _, err = os.Stat(filepath.Join(filepath.Dir(path), "escaped.max")) + assert.True(t, os.IsNotExist(err), "file written outside of cgroup path") +} diff --git a/cgroup2/manager.go b/cgroup2/manager.go index 8638ab92..351ee321 100644 --- a/cgroup2/manager.go +++ b/cgroup2/manager.go @@ -153,6 +153,9 @@ type Value struct { // write the value to the full, absolute path, of a unified hierarchy func (c *Value) write(path string, perm os.FileMode) error { + if filepath.Base(c.filename) != c.filename { + return fmt.Errorf("invalid cgroup filename: %q", c.filename) + } var data []byte switch t := c.value.(type) { case uint64: