diff --git a/manager.go b/manager.go index 3988adf..f5b4a14 100644 --- a/manager.go +++ b/manager.go @@ -109,10 +109,13 @@ type Options struct { // `RLIMIT_MEMLOCK` If a limit is provided here it will be applied when the manager is initialized. RemoveRlimit bool - // KeepKernelBTF - Defines if the kernel types defined in VerifierOptions.Programs.KernelTypes should be cleaned up - // once the manager is done using them. By default, the manager will clean them up to save up space. DISCLAIMER: if - // your program uses "manager.CloneProgram", you might want to enable "KeepKernelBTF". As a workaround, you can also - // try to strip as much as possible the content of "KernelTypes" to reduce the memory overhead. + // KeepKernelBTF - Defines if the kernel BTF used while loading programs should be cleaned up once the manager is done + // using them. This governs both VerifierOptions.Programs.KernelTypes and the shared VerifierOptions.Cache. By default, + // the manager will clean both up after Start() to save up space: otherwise a long-lived manager keeps the parsed + // kernel BTF (vmlinux) pinned for the whole process lifetime, even though it is only needed while loading programs. + // DISCLAIMER: if your program uses "manager.CloneProgram", you might want to enable "KeepKernelBTF", as cloning + // reloads a program and needs "KernelTypes" for its CO-RE relocations. As a workaround, you can also try to strip as + // much as possible the content of "KernelTypes" to reduce the memory overhead. KeepKernelBTF bool // SkipPerfMapReaderStartup - Perf maps whose name is set to true with this option will not have their reader goroutine started when calling the manager.Start() function. @@ -770,6 +773,18 @@ func (m *Manager) setupBypass() (*Map, error) { return bypassMap, nil } +// releaseKernelBTF drops the references to the kernel BTF that are only needed while loading programs, so a long-lived +// manager does not pin the parsed kernel BTF (vmlinux) for the whole process lifetime. Both the KernelTypes spec passed +// to the verifier and the shared BTF cache are cleared, symmetrically. This is a no-op when KeepKernelBTF is set (for +// example when the caller relies on CloneProgram, which reloads programs and needs KernelTypes for CO-RE relocations). +func (m *Manager) releaseKernelBTF() { + if m.options.KeepKernelBTF { + return + } + m.options.VerifierOptions.Programs.KernelTypes = nil + m.options.VerifierOptions.Cache = nil +} + // Start - Attach eBPF programs, start perf ring readers and apply maps and tail calls routing. func (m *Manager) Start() error { m.stateLock.Lock() @@ -782,10 +797,8 @@ func (m *Manager) Start() error { return nil } - if !m.options.KeepKernelBTF { - // release kernel BTF. It should no longer be needed - m.options.VerifierOptions.Programs.KernelTypes = nil - } + // release kernel BTF: it is only needed while loading programs and should no longer be needed now + m.releaseKernelBTF() // clean up tracefs if err := m.cleanupTraceFS(); err != nil { diff --git a/manager_test.go b/manager_test.go index 0a08eea..41970ee 100644 --- a/manager_test.go +++ b/manager_test.go @@ -10,6 +10,7 @@ import ( "github.com/cilium/ebpf" "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/btf" "github.com/cilium/ebpf/rlimit" ) @@ -274,3 +275,36 @@ func TestLoadELF(t *testing.T) { t.Errorf("LoadELF() error = %v, expected: %v", err, ErrManagerELFLoaded) } } + +func TestReleaseKernelBTF(t *testing.T) { + // releaseKernelBTF (called by Start) must drop both the KernelTypes spec and the shared BTF + // cache once loading is done, unless the caller opted into keeping them via KeepKernelBTF. + // Otherwise a long-lived manager keeps the parsed kernel BTF (vmlinux) pinned for the whole + // process lifetime, which regressed idle memory for a downstream consumer. + tests := []struct { + name string + keepKernelBTF bool + wantCleared bool + }{ + {name: "released by default", keepKernelBTF: false, wantCleared: true}, + {name: "retained when KeepKernelBTF is set", keepKernelBTF: true, wantCleared: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Manager{} + m.options.KeepKernelBTF = tt.keepKernelBTF + m.options.VerifierOptions.Programs.KernelTypes = &btf.Spec{} + m.options.VerifierOptions.Cache = btf.NewCache() + + m.releaseKernelBTF() + + if cleared := m.options.VerifierOptions.Programs.KernelTypes == nil; cleared != tt.wantCleared { + t.Errorf("KernelTypes cleared = %v, want %v", cleared, tt.wantCleared) + } + if cleared := m.options.VerifierOptions.Cache == nil; cleared != tt.wantCleared { + t.Errorf("Cache cleared = %v, want %v", cleared, tt.wantCleared) + } + }) + } +}