From 653f6f535eb2216985b90857ee6777800f2bdda5 Mon Sep 17 00:00:00 2001 From: Rodrigo Campos Date: Tue, 16 Jun 2026 11:36:18 +0200 Subject: [PATCH 1/4] devices: Close old program fds on return Closing the fd just drops our user-space reference to the object. The fds are not returned to the caller, we can just defer the close. Signed-off-by: Rodrigo Campos --- devices/ebpf_linux.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devices/ebpf_linux.go b/devices/ebpf_linux.go index 6a41aff..538d637 100644 --- a/devices/ebpf_linux.go +++ b/devices/ebpf_linux.go @@ -168,6 +168,12 @@ func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd if err != nil { return nilCloser, err } + defer func() { + for _, p := range oldProgs { + p.Close() + } + }() + useReplaceProg := haveBpfProgReplace() && len(oldProgs) == 1 // Generate new program. From b4865f47253edbe69e4f59fa40d0f44d93c64f4d Mon Sep 17 00:00:00 2001 From: Rodrigo Campos Date: Tue, 16 Jun 2026 16:57:49 +0200 Subject: [PATCH 2/4] devices: Remove unused return value The function is not exported and the only caller was not using the closer() function returned. Let's just remove it. This also simplifies the fd leak fixes on the next patch. The semantics of this closer function are not nice: * If we return the closer function, we need to keep the fd open and close it when the closer is called (it uses the prog.Fd() to search for the BPF program, so the fd needs to be open). * If the closer function fails, it's not clear when we should close the prog fd to avoid issues on retries. Let's just remove this closer function that is not used. It simplifies things significantly. Signed-off-by: Rodrigo Campos --- devices/ebpf_linux.go | 29 ++++++----------------------- devices/v2.go | 2 +- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/devices/ebpf_linux.go b/devices/ebpf_linux.go index 538d637..d4cda6d 100644 --- a/devices/ebpf_linux.go +++ b/devices/ebpf_linux.go @@ -15,10 +15,6 @@ import ( "golang.org/x/sys/unix" ) -func nilCloser() error { - return nil -} - func findAttachedCgroupDeviceFilters(dirFd int) ([]*ebpf.Program, error) { type bpfAttrQuery struct { TargetFd uint32 @@ -154,7 +150,7 @@ func haveBpfProgReplace() bool { // Requires the system to be running in cgroup2 unified-mode with kernel >= 4.15 . // // https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92 -func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd int) (func() error, error) { +func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd int) error { // Increase `ulimit -l` limit to avoid BPF_PROG_LOAD error (#2167). // This limit is not inherited into the container. memlockLimit := &unix.Rlimit{ @@ -166,7 +162,7 @@ func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd // Get the list of existing programs. oldProgs, err := findAttachedCgroupDeviceFilters(dirFd) if err != nil { - return nilCloser, err + return err } defer func() { for _, p := range oldProgs { @@ -184,7 +180,7 @@ func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd } prog, err := ebpf.NewProgram(spec) if err != nil { - return nilCloser, err + return err } // If there is only one old program, we can just replace it directly. @@ -201,20 +197,7 @@ func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd } err = link.RawAttachProgram(attachProgramOptions) if err != nil { - return nilCloser, fmt.Errorf("failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI): %w", err) - } - closer := func() error { - err = link.RawDetachProgram(link.RawDetachProgramOptions{ - Target: dirFd, - Program: prog, - Attach: ebpf.AttachCGroupDevice, - }) - if err != nil { - return fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE): %w", err) - } - // TODO: Should we attach the old filters back in this case? Otherwise - // we fail-open on a security feature, which is a bit scary. - return nil + return fmt.Errorf("failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI): %w", err) } if !useReplaceProg { logLevel := logrus.DebugLevel @@ -254,9 +237,9 @@ func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd Attach: ebpf.AttachCGroupDevice, }) if err != nil { - return closer, fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE) on old filter program: %w", err) + return fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE) on old filter program: %w", err) } } } - return closer, nil + return nil } diff --git a/devices/v2.go b/devices/v2.go index d54298f..508f3dd 100644 --- a/devices/v2.go +++ b/devices/v2.go @@ -64,7 +64,7 @@ func setV2(dirPath string, r *cgroups.Resources) error { return fmt.Errorf("cannot get dir FD for %s", dirPath) } defer unix.Close(dirFD) - if _, err := loadAttachCgroupDeviceFilter(insts, license, dirFD); err != nil { + if err := loadAttachCgroupDeviceFilter(insts, license, dirFD); err != nil { if !canSkipEBPFError(r) { return err } From 7bbbeee9eca6ef731487cc1c3e3f6b3bb70c7dca Mon Sep 17 00:00:00 2001 From: Rodrigo Campos Date: Tue, 16 Jun 2026 17:02:04 +0200 Subject: [PATCH 3/4] devices: Don't leak prog fd The program is either attached and the kernel keeps this, we can close the fd, or we fail and we should also close the fd. Let's just defer the close to it. Signed-off-by: Rodrigo Campos --- devices/ebpf_linux.go | 1 + 1 file changed, 1 insertion(+) diff --git a/devices/ebpf_linux.go b/devices/ebpf_linux.go index d4cda6d..f1595a7 100644 --- a/devices/ebpf_linux.go +++ b/devices/ebpf_linux.go @@ -182,6 +182,7 @@ func loadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd if err != nil { return err } + defer prog.Close() // If there is only one old program, we can just replace it directly. From f1e56e1140d55f5fc0af5acd64e6b27057220bac Mon Sep 17 00:00:00 2001 From: Rodrigo Campos Date: Tue, 16 Jun 2026 11:26:42 +0200 Subject: [PATCH 4/4] devices: Close open fds on error On error we were not closing the program fds. Let's add a defer to handle that case. Signed-off-by: Rodrigo Campos --- devices/ebpf_linux.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/devices/ebpf_linux.go b/devices/ebpf_linux.go index f1595a7..cfc36f7 100644 --- a/devices/ebpf_linux.go +++ b/devices/ebpf_linux.go @@ -15,7 +15,7 @@ import ( "golang.org/x/sys/unix" ) -func findAttachedCgroupDeviceFilters(dirFd int) ([]*ebpf.Program, error) { +func findAttachedCgroupDeviceFilters(dirFd int) (_ []*ebpf.Program, retErr error) { type bpfAttrQuery struct { TargetFd uint32 AttachType uint32 @@ -54,8 +54,17 @@ func findAttachedCgroupDeviceFilters(dirFd int) ([]*ebpf.Program, error) { } // Convert the ids to program handles. + // On error we don't return the programs slice, so close the fds stored there. progIds = progIds[:size] programs := make([]*ebpf.Program, 0, len(progIds)) + defer func() { + if retErr != nil { + for _, p := range programs { + p.Close() + } + } + }() + for _, progId := range progIds { program, err := ebpf.NewProgramFromID(ebpf.ProgramID(progId)) if err != nil {