From df952c39a4e3a154db42e12bfd76b5fbfe656ff9 Mon Sep 17 00:00:00 2001 From: Bertrone Matteo Date: Thu, 23 Jul 2026 13:40:30 +0200 Subject: [PATCH] Release the shared kernel BTF cache after Start() Manager.Start() releases VerifierOptions.Programs.KernelTypes once loading is done (unless KeepKernelBTF is set), but it did not release VerifierOptions.Cache, which was added alongside the cilium/ebpf v0.22.0 upgrade. A long-lived manager therefore keeps the shared *btf.Cache -- and the parsed kernel vmlinux BTF it holds -- pinned for the entire process lifetime, regressing idle memory for downstream consumers. Clear VerifierOptions.Cache symmetrically with KernelTypes, extracted into a small releaseKernelBTF helper. The cache is only consumed while loading the collection (before Start); nothing reads it afterwards: CloneProgram reloads through the exported NewProgramWithOptions, which allocates its own fresh cache and relies on KernelTypes for CO-RE relocations. Gated on KeepKernelBTF so callers that clone programs are unaffected. Add TestReleaseKernelBTF covering both the release and retain paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- manager.go | 29 +++++++++++++++++++++-------- manager_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) 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) + } + }) + } +}