// SPDX-License-Identifier: GPL-3.0-or-later package ebpfsource import ( "bytes" "encoding/binary" "errors" "fmt" "io" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/perf" "github.com/cilium/ebpf/rlimit" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/events" ) // ExecSource owns the Go-native exec tracepoint and perf reader. Load errors // are returned to the caller so startup can log and fall back to polling. type ExecSource struct { objects execObjects link link.Link reader *perf.Reader } // execEventRaw mirrors struct exec_event in bpf/exec.bpf.c. Keeping the wire // layout explicit makes changes to the kernel/user-space ABI reviewable. type execEventRaw struct { PID uint32 PPID uint32 UID uint32 ParentComm [16]int8 Filename [128]int8 Arg1 [128]int8 Arg2 [128]int8 } // LostSamplesError reports perf-buffer pressure without making it impossible // for the caller to continue reading later events. type LostSamplesError struct { Count uint64 } func (e LostSamplesError) Error() string { return fmt.Sprintf("exec perf buffer lost %d samples", e.Count) } func OpenExecSource() (*ExecSource, error) { // Required by older kernels. On newer memcg-accounted kernels this may fail // in an otherwise capable service sandbox, so attempt the load regardless // and preserve both errors only when loading also fails. memlockErr := rlimit.RemoveMemlock() var objects execObjects if err := loadExecObjects(&objects, nil); err != nil { if memlockErr != nil { return nil, fmt.Errorf("load exec BPF objects: %w (memlock adjustment: %v)", err, memlockErr) } return nil, fmt.Errorf("load exec BPF objects: %w", err) } attached, err := link.Tracepoint("syscalls", "sys_enter_execve", objects.TraceExecve, nil) if err != nil { objects.Close() return nil, fmt.Errorf("attach exec tracepoint: %w", err) } reader, err := perf.NewReader(objects.Events, 64*4096) if err != nil { attached.Close() objects.Close() return nil, fmt.Errorf("open exec perf reader: %w", err) } return &ExecSource{objects: objects, link: attached, reader: reader}, nil } func (s *ExecSource) Read() (events.ExecEvent, error) { record, err := s.reader.Read() if err != nil { return events.ExecEvent{}, err } if record.LostSamples != 0 { return events.ExecEvent{}, LostSamplesError{Count: record.LostSamples} } return decodeExecSample(record.RawSample) } func decodeExecSample(sample []byte) (events.ExecEvent, error) { var raw execEventRaw if len(sample) != binary.Size(raw) { return events.ExecEvent{}, fmt.Errorf("exec event size %d, want %d", len(sample), binary.Size(raw)) } if err := binary.Read(bytes.NewReader(sample), binary.NativeEndian, &raw); err != nil { return events.ExecEvent{}, fmt.Errorf("decode exec event: %w", err) } argv := make([]string, 0, 2) if value := cString(raw.Arg1[:]); value != "" { argv = append(argv, value) } if value := cString(raw.Arg2[:]); value != "" { argv = append(argv, value) } return events.ExecEvent{ PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID), ParentComm: cString(raw.ParentComm[:]), Filename: cString(raw.Filename[:]), Argv: argv, }, nil } func (s *ExecSource) Close() error { var failures []error if s.reader != nil { failures = append(failures, s.reader.Close()) } if s.link != nil { failures = append(failures, s.link.Close()) } failures = append(failures, s.objects.Close()) return errors.Join(failures...) } func cString(raw []int8) string { value := make([]byte, 0, len(raw)) for _, char := range raw { if char == 0 { break } value = append(value, byte(char)) } return string(value) } var _ io.Closer = (*ExecSource)(nil)