Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions cmd/runtimetest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -341,6 +342,63 @@ func (c *complianceTester) validateHostname(spec *rspec.Spec) error {
return nil
}

func parseProcLimit(value string) (uint64, error) {
if value == "unlimited" {
return unix.RLIM_INFINITY, nil
}

return strconv.ParseUint(value, 10, 64)
}

// parseNofileLimits extracts RLIMIT_NOFILE from the contents of a
// /proc/<pid>/limits file.
func parseNofileLimits(limits string) (syscall.Rlimit, error) {
var rlimit syscall.Rlimit

for _, line := range strings.Split(limits, "\n") {
rest, ok := strings.CutPrefix(line, "Max open files")
if !ok {
continue
}
fields := strings.Fields(rest)
if len(fields) < 2 {
break
}
soft, err := parseProcLimit(fields[0])
if err != nil {
return rlimit, err
}
hard, err := parseProcLimit(fields[1])
if err != nil {
return rlimit, err
}
rlimit.Cur = soft
rlimit.Max = hard
return rlimit, nil
}

return rlimit, errors.New("no RLIMIT_NOFILE entry in the process limits")
}

// getRlimit reads a resource limit as the runtime set it. The Go runtime
// raises its own soft RLIMIT_NOFILE at start-up and restores the original
// in the processes it starts, so that one is read from a child.
func getRlimit(resource int) (syscall.Rlimit, error) {
var rlimit syscall.Rlimit

if resource != syscall.RLIMIT_NOFILE {
err := syscall.Getrlimit(resource, &rlimit)
return rlimit, err
}

limits, err := exec.Command("/bin/cat", "/proc/self/limits").Output()
if err != nil {
return rlimit, err
}

return parseNofileLimits(string(limits))
}

func (c *complianceTester) validateRlimits(spec *rspec.Spec) error {
if spec.Process == nil {
c.harness.Skip(1, "process.rlimits not set")
Expand All @@ -353,8 +411,8 @@ func (c *complianceTester) validateRlimits(spec *rspec.Spec) error {
return err
}

var rlimit syscall.Rlimit
if err := syscall.Getrlimit(rl, &rlimit); err != nil {
rlimit, err := getRlimit(rl)
if err != nil {
return err
}

Expand Down
54 changes: 54 additions & 0 deletions cmd/runtimetest/rlimit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"syscall"
"testing"

"golang.org/x/sys/unix"
)

func TestParseNofileLimits(t *testing.T) {
limits := `Limit Soft Limit Hard Limit Units
Max processes 63432 63432 processes
Max open files 3000 4000 files
Max locked memory 8388608 8388608 bytes
`
rlimit, err := parseNofileLimits(limits)
if err != nil {
t.Fatal(err)
}
if rlimit.Cur != 3000 || rlimit.Max != 4000 {
t.Errorf("limits == %d/%d, expected 3000/4000", rlimit.Cur, rlimit.Max)
}

rlimit, err = parseNofileLimits("Max open files unlimited unlimited files\n")
if err != nil {
t.Fatal(err)
}
if rlimit.Cur != unix.RLIM_INFINITY || rlimit.Max != unix.RLIM_INFINITY {
t.Errorf("limits == %d/%d, expected unlimited", rlimit.Cur, rlimit.Max)
}

if _, err := parseNofileLimits("Max processes 1 1 processes\n"); err == nil {
t.Error("expected an error without a RLIMIT_NOFILE entry")
}
}

func TestGetRlimitNofile(t *testing.T) {
var own syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &own); err != nil {
t.Fatal(err)
}

rlimit, err := getRlimit(syscall.RLIMIT_NOFILE)
if err != nil {
t.Skipf("cannot read the limits of a child: %v", err)
}
// Go only raises the soft limit, never the hard one.
if rlimit.Max != own.Max {
t.Errorf("hard limit == %d, expected %d", rlimit.Max, own.Max)
}
if rlimit.Cur > own.Cur {
t.Errorf("soft limit %d of a child exceeds our own %d", rlimit.Cur, own.Cur)
}
}