Repository: cilium/ebpf
Pull request #438 (2021-09-24 14:48:24)

make RemoveMemlockRlimit conditional on kernel version
We only need to raise RLIMIT_MEMLOCK on old kernels. Check whether
the kernel uses memcg accounting by lowering the limit to zero and
creating a small map.


Patch:
diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml
index cee74e7..04d9f19 100644
--- a/.semaphore/semaphore.yml
+++ b/.semaphore/semaphore.yml
@@ -20,6 +20,7 @@ blocks:
       prologue:
         commands:
           - checkout
+          - curl -sSfL http://security.ubuntu.com/ubuntu/pool/main/c/ca-certificates/ca-certificates_20210119~20.04.2_all.deb -o /tmp/ca-certificates.deb && sudo dpkg -i /tmp/ca-certificates.deb
           - sudo mkdir -p /usr/local/golang/1.17 && curl -fL "https://golang.org/dl/go1.17.linux-amd64.tar.gz" | sudo tar -xz -C /usr/local/golang/1.17
           - sem-version go 1.17
           - go install github.com/mattn/goveralls@latest
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 83ff4fa..9aa70fa 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -5,7 +5,6 @@ package unix
 
 import (
 	"bytes"
-	"fmt"
 	"syscall"
 
 	linux "golang.org/x/sys/unix"
@@ -204,19 +203,6 @@ func KernelRelease() (string, error) {
 	return release, nil
 }
 
-func RemoveMemlockRlimit() (func() error, error) {
-	var oldLimit Rlimit
-	newLimit := Rlimit{Cur: RLIM_INFINITY, Max: RLIM_INFINITY}
-
-	// pid 0 affects the current process.
-	if err := linux.Prlimit(0, RLIMIT_MEMLOCK, &newLimit, &oldLimit); err != nil {
-		return nil, fmt.Errorf("failed to set memlock rlimit: %w", err)
-	}
-
-	return func() error {
-		if err := linux.Setrlimit(RLIMIT_MEMLOCK, &oldLimit); err != nil {
-			return fmt.Errorf("failed to revert memlock rlimit: %w", err)
-		}
-		return nil
-	}, nil
+func Prlimit(pid, resource int, new, old *Rlimit) error {
+	return linux.Prlimit(pid, resource, new, old)
 }
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index 6664124..4f50d89 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -262,6 +262,6 @@ func KernelRelease() (string, error) {
 	return "", errNonLinux
 }
 
-func RemoveMemlockRlimit() (func() error, error) {
-	return nil, errNonLinux
+func Prlimit(pid, resource int, new, old *Rlimit) error {
+	return errNonLinux
 }
diff --git a/map.go b/map.go
index e6b58cc..cca387e 100644
--- a/map.go
+++ b/map.go
@@ -197,7 +197,7 @@ func NewMap(spec *MapSpec) (*Map, error) {
 //
 // The caller is responsible for ensuring the process' rlimit is set
 // sufficiently high for locking memory during map creation. This can be done
-// by calling ebpf.RemoveMemlockRlimit() prior to calling NewMapWithOptions.
+// by calling rlimit.RemoveMemlock() prior to calling NewMapWithOptions.
 //
 // May return an error wrapping ErrMapIncompatible.
 func NewMapWithOptions(spec *MapSpec, opts MapOptions) (*Map, error) {
@@ -400,7 +400,7 @@ func (spec *MapSpec) createMap(inner *internal.FD, opts MapOptions, handles *han
 	fd, err := internal.BPFMapCreate(&attr)
 	if err != nil {
 		if errors.Is(err, unix.EPERM) {
-			return nil, fmt.Errorf("map create: RLIMIT_MEMLOCK may be too low: %w", err)
+			return nil, fmt.Errorf("map create: %w (MEMLOCK bay be too low, consider rlimit.RemoveMemlock)", err)
 		}
 		if btfDisabled {
 			return nil, fmt.Errorf("map create without BTF: %w", err)
diff --git a/prog.go b/prog.go
index 714f814..3549a3f 100644
--- a/prog.go
+++ b/prog.go
@@ -316,7 +316,7 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 	if errors.Is(logErr, unix.EPERM) && logBuf[0] == 0 {
 		// EPERM due to RLIMIT_MEMLOCK happens before the verifier, so we can
 		// check that the log is empty to reduce false positives.
-		return nil, fmt.Errorf("load program: RLIMIT_MEMLOCK may be too low: %w", logErr)
+		return nil, fmt.Errorf("load program: %w (MEMLOCK bay be too low, consider rlimit.RemoveMemlock)", logErr)
 	}
 
 	err = internal.ErrorWithLog(err, logBuf, logErr)
diff --git a/rlimit/rlimit.go b/rlimit/rlimit.go
new file mode 100644
index 0000000..22fc817
--- /dev/null
+++ b/rlimit/rlimit.go
@@ -0,0 +1,113 @@
+// Package rlimit allows raising RLIMIT_MEMLOCK if necessary for the use of BPF.
+package rlimit
+
+import (
+	"errors"
+	"fmt"
+	"sync"
+
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/unix"
+)
+
+var (
+	unsupportedMemcgAccounting = &internal.UnsupportedFeatureError{
+		MinimumVersion: internal.Version{5, 11, 0},
+		Name:           "memcg-based accounting for BPF memory",
+	}
+	haveMemcgAccounting error
+
+	rlimitMu sync.Mutex
+)
+
+func init() {
+	// We have to run this feature test at init, since it relies on changing
+	// RLIMIT_MEMLOCK. Doing so is not safe in a concurrent program. Instead,
+	// we rely on the initialization order guaranteed by the Go runtime to
+	// execute the test in a safe environment:
+	//
+	//    the invocation of init functions happens in a single goroutine,
+	//    sequentially, one package at a time.
+	//
+	// This is also the reason why RemoveMemlock is in its own package:
+	// we only want to run the initializer if RemoveMemlock is called
+	// from somewhere.
+	haveMemcgAccounting = detectMemcgAccounting()
+}
+
+func detectMemcgAccounting() error {
+	// Reduce the limit to zero and store the previous limit. This should always succeed.
+	var oldLimit unix.Rlimit
+	zeroLimit := unix.Rlimit{Cur: 0, Max: oldLimit.Max}
+	if err := unix.Prlimit(0, unix.RLIMIT_MEMLOCK, &zeroLimit, &oldLimit); err != nil {
+		return fmt.Errorf("lowering memlock rlimit: %s", err)
+	}
+
+	attr := internal.BPFMapCreateAttr{
+		MapType:    2, /* Array */
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 1,
+	}
+
+	// Creating a map allocates shared (and locked) memory that counts against
+	// the rlimit on pre-5.11 kernels, but against the memory cgroup budget on
+	// kernels 5.11 and over. If this call succeeds with the process' memlock
+	// rlimit set to 0, we can reasonably assume memcg accounting is supported.
+	fd, mapErr := internal.BPFMapCreate(&attr)
+
+	// Restore old limits regardless of what happened.
+	if err := unix.Prlimit(0, unix.RLIMIT_MEMLOCK, &oldLimit, nil); err != nil {
+		return fmt.Errorf("restoring old memlock rlimit: %s", err)
+	}
+
+	// Map creation successful, memcg accounting supported.
+	if mapErr == nil {
+		fd.Close()
+		return nil
+	}
+
+	// EPERM shows up when map creation would exceed the memory budget.
+	if errors.Is(mapErr, unix.EPERM) {
+		return unsupportedMemcgAccounting
+	}
+
+	// This shouldn't happen really.
+	return fmt.Errorf("unexpected error detecting memory cgroup accounting: %s", mapErr)
+}
+
+// RemoveMemlock removes the limit on the amount of memory the current
+// process can lock into RAM, if necessary.
+//
+// This is not required to load eBPF resources on kernel versions 5.11+
+// due to the introduction of cgroup-based memory accounting. On such kernels
+// the function is a no-op.
+//
+// Since the function may change global per-process limits it should be invoked
+// at program start up, in main() or init().
+//
+// This function exists as a convenience and should only be used when
+// permanently raising RLIMIT_MEMLOCK to infinite is appropriate. Consider
+// invoking prlimit(2) directly with a more reasonable limit if desired.
+//
+// Requires CAP_SYS_RESOURCE on kernels < 5.11.
+func RemoveMemlock() error {
+	if haveMemcgAccounting == nil {
+		return nil
+	}
+
+	if !errors.Is(haveMemcgAccounting, unsupportedMemcgAccounting) {
+		return haveMemcgAccounting
+	}
+
+	rlimitMu.Lock()
+	defer rlimitMu.Unlock()
+
+	// pid 0 affects the current process. Requires CAP_SYS_RESOURCE.
+	newLimit := unix.Rlimit{Cur: unix.RLIM_INFINITY, Max: unix.RLIM_INFINITY}
+	if err := unix.Prlimit(0, unix.RLIMIT_MEMLOCK, &newLimit, nil); err != nil {
+		return fmt.Errorf("failed to set memlock rlimit: %w", err)
+	}
+
+	return nil
+}
diff --git a/syscalls.go b/syscalls.go
index cb38e88..f8cb5f0 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -17,16 +17,6 @@ import (
 // Deprecated: use os.ErrNotExist instead.
 var ErrNotExist = os.ErrNotExist
 
-// RemoveMemlockRlimit removes the limit on the amount of memory the current
-// process can lock into RAM. Returns a function that restores the limit to
-// its previous value.
-//
-// This is not required to load eBPF resources on kernel versions 5.11+
-// due to the introduction of cgroup-based memory accounting.
-func RemoveMemlockRlimit() (func() error, error) {
-	return unix.RemoveMemlockRlimit()
-}
-
 // invalidBPFObjNameChar returns true if char may not appear in
 // a BPF object name.
 func invalidBPFObjNameChar(char rune) bool {


Test patch:
diff --git a/internal/testutils/rlimit.go b/internal/testutils/rlimit.go
index 58ae784..971d985 100644
--- a/internal/testutils/rlimit.go
+++ b/internal/testutils/rlimit.go
@@ -4,13 +4,13 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/cilium/ebpf/internal/unix"
+	"github.com/cilium/ebpf/rlimit"
 )
 
 func init() {
 	// Increase the memlock for all tests unconditionally. It's a great source of
 	// weird bugs, since different distros have different default limits.
-	if _, err := unix.RemoveMemlockRlimit(); err != nil {
+	if err := rlimit.RemoveMemlock(); err != nil {
 		fmt.Fprintln(os.Stderr, "WARNING: Failed to adjust rlimit, tests may fail")
 	}
 }
diff --git a/rlimit/rlimit_test.go b/rlimit/rlimit_test.go
new file mode 100644
index 0000000..29312be
--- /dev/null
+++ b/rlimit/rlimit_test.go
@@ -0,0 +1,33 @@
+package rlimit
+
+import (
+	"testing"
+
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/unix"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestRemoveMemlock(t *testing.T) {
+	var before unix.Rlimit
+	qt.Assert(t, unix.Prlimit(0, unix.RLIMIT_MEMLOCK, nil, &before), qt.IsNil)
+
+	err := RemoveMemlock()
+	qt.Assert(t, err, qt.IsNil)
+
+	var after unix.Rlimit
+	qt.Assert(t, unix.Prlimit(0, unix.RLIMIT_MEMLOCK, nil, &after), qt.IsNil)
+
+	// We can't use testutils here due to an import cycle.
+	version, err := internal.KernelVersion()
+	qt.Assert(t, err, qt.IsNil)
+
+	if version.Less(unsupportedMemcgAccounting.MinimumVersion) {
+		qt.Assert(t, after.Cur, qt.Equals, uint64(unix.RLIM_INFINITY), qt.Commentf("cur should be INFINITY"))
+		qt.Assert(t, after.Max, qt.Equals, uint64(unix.RLIM_INFINITY), qt.Commentf("max should be INFINITY"))
+	} else {
+		qt.Assert(t, after.Cur, qt.Equals, before.Cur, qt.Commentf("cur should be unchanged"))
+		qt.Assert(t, after.Max, qt.Equals, before.Max, qt.Commentf("max should be unchanged"))
+	}
+}