diff --git a/runner/limiter_linux.go b/runner/limiter_linux.go index 1ec4101..050a709 100644 --- a/runner/limiter_linux.go +++ b/runner/limiter_linux.go @@ -15,67 +15,138 @@ import ( ) var ( - cgroupRootOnce sync.Once - cgroupRoot string - cgroupInitErr error - cgroupCounter int64 + cgroupRootOnce sync.Once + cgroupRoot string + cgroupRootOwned bool + cgroupInitErr error + cgroupCounter int64 ) const cgroupFSRoot = "/sys/fs/cgroup" func ensureCgroupRoot() (string, error) { cgroupRootOnce.Do(func() { - data, err := os.ReadFile("/proc/self/cgroup") + if root, err := createOwnedCgroup(); err == nil { + cgroupRoot = root + cgroupRootOwned = true + return + } + + root, err := createScopeCgroup() if err != nil { - cgroupInitErr = fmt.Errorf("read /proc/self/cgroup: %w", err) + cgroupInitErr = err return } - var rel string - for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { - if strings.HasPrefix(line, "0::") { - rel = strings.TrimPrefix(line, "0::") - break - } - } - if rel == "" { - cgroupInitErr = fmt.Errorf("cgroup v2 not found in /proc/self/cgroup (unified hierarchy required)") - return - } - ownCg := filepath.Join(cgroupFSRoot, rel) - - controllers, err := os.ReadFile(filepath.Join(ownCg, "cgroup.controllers")) - if err != nil { - cgroupInitErr = fmt.Errorf("cgroup %s not accessible: %w", ownCg, err) - return - } - if !strings.Contains(" "+string(controllers)+" ", " memory ") { - cgroupInitErr = fmt.Errorf("memory controller not delegated to %s (controllers: %s)", ownCg, strings.TrimSpace(string(controllers))) - return - } - - initCg := filepath.Join(ownCg, "judge.init") - if err := os.MkdirAll(initCg, 0755); err != nil { - cgroupInitErr = fmt.Errorf("mkdir %s: %w", initCg, err) - return - } - if err := os.WriteFile(filepath.Join(initCg, "cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0644); err != nil { - cgroupInitErr = fmt.Errorf("move judge into %s: %w", initCg, err) - return - } - - if err := os.WriteFile(filepath.Join(ownCg, "cgroup.subtree_control"), []byte("+memory"), 0644); err != nil { - current, _ := os.ReadFile(filepath.Join(ownCg, "cgroup.subtree_control")) - if !strings.Contains(" "+string(current)+" ", " memory ") { - cgroupInitErr = fmt.Errorf("enable +memory in %s/cgroup.subtree_control: %w", ownCg, err) - return - } - } - - cgroupRoot = ownCg + cgroupRoot = root }) return cgroupRoot, cgroupInitErr } +func hasController(path, name string) (bool, error) { + data, err := os.ReadFile(path) + if err != nil { + return false, err + } + needle := " " + name + " " + return strings.Contains(" "+strings.TrimSpace(string(data))+" ", needle), nil +} + +func createOwnedCgroup() (string, error) { + ok, err := hasController(filepath.Join(cgroupFSRoot, "cgroup.controllers"), "memory") + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("memory controller not available in root cgroup") + } + + enabled, err := hasController(filepath.Join(cgroupFSRoot, "cgroup.subtree_control"), "memory") + if err != nil { + return "", err + } + if !enabled { + if err := os.WriteFile(filepath.Join(cgroupFSRoot, "cgroup.subtree_control"), []byte("+memory"), 0644); err != nil { + return "", fmt.Errorf("enable +memory in root subtree_control: %w", err) + } + } + + name := fmt.Sprintf("judge.%d", os.Getpid()) + root := filepath.Join(cgroupFSRoot, name) + if err := os.Mkdir(root, 0755); err != nil { + if os.IsExist(err) { + } else { + return "", err + } + } + + ok, err = hasController(filepath.Join(root, "cgroup.controllers"), "memory") + if err != nil { + _ = os.Remove(root) + return "", err + } + if !ok { + _ = os.Remove(root) + return "", fmt.Errorf("memory controller not inherited into %s", root) + } + if err := os.WriteFile(filepath.Join(root, "cgroup.subtree_control"), []byte("+memory"), 0644); err != nil { + _ = os.Remove(root) + return "", fmt.Errorf("enable +memory in %s: %w", root, err) + } + + return root, nil +} + +func createScopeCgroup() (string, error) { + data, err := os.ReadFile("/proc/self/cgroup") + if err != nil { + return "", fmt.Errorf("read /proc/self/cgroup: %w", err) + } + var rel string + for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + if strings.HasPrefix(line, "0::") { + rel = strings.TrimPrefix(line, "0::") + break + } + } + if rel == "" { + return "", fmt.Errorf("cgroup v2 not found in /proc/self/cgroup (unified hierarchy required)") + } + ownCg := filepath.Join(cgroupFSRoot, rel) + + ok, err := hasController(filepath.Join(ownCg, "cgroup.controllers"), "memory") + if err != nil { + return "", fmt.Errorf("cgroup %s not accessible: %w", ownCg, err) + } + if !ok { + return "", fmt.Errorf("memory controller not delegated to %s", ownCg) + } + + initCg := filepath.Join(ownCg, "judge.init") + if err := os.MkdirAll(initCg, 0755); err != nil { + return "", fmt.Errorf("mkdir %s: %w", initCg, err) + } + if err := os.WriteFile(filepath.Join(initCg, "cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0644); err != nil { + return "", fmt.Errorf("move judge into %s: %w", initCg, err) + } + + if err := os.WriteFile(filepath.Join(ownCg, "cgroup.subtree_control"), []byte("+memory"), 0644); err != nil { + enabled, _ := hasController(filepath.Join(ownCg, "cgroup.subtree_control"), "memory") + if !enabled { + return "", fmt.Errorf("enable +memory in %s/cgroup.subtree_control: %w", ownCg, err) + } + } + return ownCg, nil +} + +func cleanupCgroupRoot() { + if cgroupRoot == "" || !cgroupRootOwned { + return + } + _ = os.Remove(cgroupRoot) + cgroupRoot = "" + cgroupRootOwned = false +} + type linuxLimiter struct { memLimit int64 cgPath string diff --git a/runner/limiter_other.go b/runner/limiter_other.go index e317096..3d1d86e 100644 --- a/runner/limiter_other.go +++ b/runner/limiter_other.go @@ -25,3 +25,5 @@ func (l *noopLimiter) prepare(cmd *exec.Cmd) error { func (l *noopLimiter) afterStart(cmd *exec.Cmd) error { return nil } func (l *noopLimiter) collect() limitStats { return limitStats{} } func (l *noopLimiter) cleanup() {} + +func cleanupCgroupRoot() {} diff --git a/runner/limiter_windows.go b/runner/limiter_windows.go index e826caf..a7ba22c 100644 --- a/runner/limiter_windows.go +++ b/runner/limiter_windows.go @@ -132,3 +132,5 @@ func (l *windowsLimiter) cleanup() { l.job = 0 } } + +func cleanupCgroupRoot() {} diff --git a/runner/runner.go b/runner/runner.go index 125c0d7..a477fca 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -59,6 +59,8 @@ func resolveBinary(workDir, name string) string { } func (r *Runner) Run() *SuiteResult { + defer cleanupCgroupRoot() + result := &SuiteResult{} buildLog, err := r.build()