Skip to content
Open
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
35 changes: 35 additions & 0 deletions pkg/trace/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import (
"os"
"sync"
"sync/atomic"
"syscall"
"unsafe"

"github.com/pkg/errors"
"golang.org/x/sys/unix"

"github.com/maxgio92/xcover/internal/settings"
"github.com/maxgio92/xcover/internal/utils"
Expand Down Expand Up @@ -117,6 +120,11 @@ func (t *UserTracer) Run(ctx context.Context) error {
t.logger.Debug().Msg("attaching trace to selected functions")
t.attachProbe(ctx)

// Drop elevated capabilities after BPF programs are loaded and attached.
if err := dropElevatedCapabilities(); err != nil {
t.logger.Warn().Err(err).Msg("failed to drop elevated capabilities after BPF attach")
}

feedCh := make(chan []byte, feedChBufSize)

eventsCh, err := t.probe.InitEventBuf(ctx)
Expand Down Expand Up @@ -281,3 +289,30 @@ func (t *UserTracer) writeReport(reportPath string) error {

return report.WriteReport(file)
}

func dropElevatedCapabilities() error {

@maxgio92 maxgio92 Jun 4, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work with the Go scheduler, since it is going to apply only to the current thread - it is persistent with goroutine preemption or new ones scheduled on different runtime threads - unless probably GOMAXPROCS=1

hdr := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3}
var data [2]unix.CapUserData
if err := unix.Capget(&hdr, &data[0]); err != nil {
return err
}

data[0].Effective &^= uint32(1) << unix.CAP_SYS_ADMIN
data[0].Permitted &^= uint32(1) << unix.CAP_SYS_ADMIN
data[1].Effective &^= uint32(1) << (unix.CAP_BPF - 32)
data[1].Permitted &^= uint32(1) << (unix.CAP_BPF - 32)

// Apply to ALL OS threads in the process, not just the calling thread.
// This is necessary because Go's scheduler can move goroutines between
// threads, and capset() alone only affects the current thread.
_, _, errno := syscall.AllThreadsSyscall6(
unix.SYS_CAPSET,
uintptr(unsafe.Pointer(&hdr)),
uintptr(unsafe.Pointer(&data[0])),
0, 0, 0, 0,
)
if errno != 0 {
return errno
}
return nil
}