diff --git a/.github/workflows/linux-port.yml b/.github/workflows/linux-port.yml new file mode 100644 index 000000000..31c68cf90 --- /dev/null +++ b/.github/workflows/linux-port.yml @@ -0,0 +1,44 @@ +name: linux-port + +on: + pull_request: + branches: + - linux-port + workflow_dispatch: + +concurrency: + group: linux-port-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: 1.26.x + +jobs: + ebpf: + runs-on: ubuntu-latest + steps: + - name: Validate PR title + if: github.event_name == 'pull_request' + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: Install clang + run: | + sudo apt-get update + sudo apt-get install -y clang llvm + clang --version + - name: Check generation drift + run: | + chmod +x internal/ebpf/generate.sh + ./internal/ebpf/generate.sh + git diff --exit-code -- internal/ebpf + - name: Unit tests + run: go test ./internal/ebpf ./internal/bootstrap ./pkg/event ./pkg/ps ./pkg/api ./pkg/util/signals + - name: Privileged process source + run: sudo -E env "PATH=$PATH" go test -tags ebpf_integration -count=1 ./internal/ebpf diff --git a/internal/bootstrap/bootstrap_linux.go b/internal/bootstrap/bootstrap_linux.go index d9d3abc45..7a25fd788 100644 --- a/internal/bootstrap/bootstrap_linux.go +++ b/internal/bootstrap/bootstrap_linux.go @@ -22,46 +22,194 @@ package bootstrap import ( + "context" "errors" + "fmt" + "net" + "os" + "github.com/rabbitstack/fibratus/pkg/aggregator" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/api" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/rules" + "github.com/rabbitstack/fibratus/pkg/util/multierror" + "github.com/rabbitstack/fibratus/pkg/util/signals" + "github.com/rabbitstack/fibratus/pkg/util/version" + log "github.com/sirupsen/logrus" ) -var ErrCaptureNotWired = errors.New("linux event capture is not wired yet") +// instanceSocket is an abstract UNIX domain socket name. Binding it acts as +// a kernel-wide mutex that is released automatically when the process +// terminates, so no filesystem cleanup is required. +const instanceSocket = "@fibratus" +// ErrAlreadyRunning signals a Fibratus process is already running. +var ErrAlreadyRunning = errors.New("an instance of Fibratus process is already running in the system") + +// App centralizes the core building blocks responsible +// for event acquisition, rule engine initialization, +// and event routing to the output sinks. type App struct { - config *config.Config - evs *EventSourceControl + config *config.Config + evs *EventSourceControl + engine *rules.Engine + psnap ps.Snapshotter + agg *aggregator.BufferedAggregator + signals chan struct{} + instance net.Listener } +// Option enables changing the behaviour of the bootstrap application. type Option func(*opts) -type opts struct{} - -func WithSignals() Option { - return func(*opts) {} +type opts struct { + installSignals bool } -func WithDebugPrivilege() Option { - return func(*opts) {} +// WithSignals installs signal handlers. +func WithSignals() Option { + return func(o *opts) { + o.installSignals = true + } } +// NewApp constructs a new bootstrap application with the specified configuration +// and a list of options. func NewApp(cfg *config.Config, options ...Option) (*App, error) { if err := InitConfigAndLogger(cfg); err != nil { return nil, err } - return &App{config: cfg, evs: NewEventSourceControl(cfg)}, nil + var o opts + var sigs chan struct{} + for _, opt := range options { + opt(&o) + } + if o.installSignals { + sigs = signals.Install() + } + + psnap := ps.NewSnapshotter() + + var engine *rules.Engine + var rs *config.RulesCompileResult + if cfg.Filters != nil && cfg.Filters.Rules.Enabled && !cfg.ForwardMode && !cfg.IsCaptureSet() && !cfg.IsFilamentSet() { + engine = rules.NewEngine(psnap, cfg) + var err error + rs, err = engine.Compile() + if err != nil { + return nil, err + } + if rs != nil { + log.Infof("rules compile summary: %s", rs) + } + } else { + log.Info("rule engine is disabled") + } + + return &App{ + config: cfg, + evs: NewEventSourceControl(psnap, cfg, rs), + engine: engine, + psnap: psnap, + signals: sigs, + }, nil } -func (f *App) Run([]string) error { - if err := f.evs.Open(f.config); err != nil { +// Run configures and opens the event source to start consuming events. +func (f *App) Run(args []string) error { + if f.evs == nil { + panic("event source is nil") + } + cfg := f.config + + if cfg.IsFilamentSet() { + return fmt.Errorf("filaments are not supported on Linux") + } + + if !f.isSingleInstance() { + return ErrAlreadyRunning + } + + log.Infof("bootstrapping with pid %d. Version: %s", os.Getpid(), version.Get()) + log.Infof("configuration options: %s", cfg.Print()) + + fltr, err := filter.NewFromCLI(args, cfg) + if err != nil { return err } - return ErrCaptureNotWired + if fltr != nil { + f.evs.SetFilter(fltr) + } + if f.engine != nil { + f.evs.RegisterEventListener(f.engine) + } + + if err := f.evs.Open(cfg); err != nil { + return multierror.Wrap(err, f.evs.Close()) + } + + f.agg, err = aggregator.NewBuffered( + f.evs.Events(), + f.evs.Errors(), + cfg.Aggregator, + cfg.Output, + cfg.Transformers, + cfg.Alertsenders, + ) + if err != nil { + return err + } + return api.StartServer(cfg) } -func (*App) Wait() {} +// Wait waits for the app to receive the termination signal. +func (f *App) Wait() { + if f.signals != nil { + <-f.signals + } +} +// Shutdown is responsible for tearing down everything gracefully. func (f *App) Shutdown() error { - return f.evs.Close() + errs := make([]error, 0) + if f.evs != nil { + if err := f.evs.Close(); err != nil { + errs = append(errs, err) + } + } + if f.psnap != nil { + if err := f.psnap.Close(); err != nil { + errs = append(errs, err) + } + } + if f.agg != nil { + if err := f.agg.Stop(); err != nil { + errs = append(errs, err) + } + } + if err := api.CloseServer(); err != nil { + errs = append(errs, err) + } + if err := alertsender.ShutdownAll(); err != nil { + errs = append(errs, err) + } + if f.instance != nil { + _ = f.instance.Close() + } + return multierror.Wrap(errs...) +} + +// isSingleInstance checks if there is already an instance of Fibratus +// running in the system. +func (f *App) isSingleInstance() bool { + var lc net.ListenConfig + l, err := lc.Listen(context.Background(), "unix", instanceSocket) + if err != nil { + return false + } + f.instance = l + return true } diff --git a/internal/bootstrap/source_linux.go b/internal/bootstrap/source_linux.go index b4df2a74e..c341c9b55 100644 --- a/internal/bootstrap/source_linux.go +++ b/internal/bootstrap/source_linux.go @@ -22,15 +22,25 @@ package bootstrap import ( + libebpf "github.com/rabbitstack/fibratus/internal/ebpf" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/source" ) +// EventSourceControl abstracts away the management of event sources. type EventSourceControl struct { - evs *stubEventSource + evs source.EventSource } -func NewEventSourceControl(*config.Config) *EventSourceControl { - return &EventSourceControl{evs: &stubEventSource{}} +func NewEventSourceControl( + psnap ps.Snapshotter, + cfg *config.Config, + compiler *config.RulesCompileResult, +) *EventSourceControl { + return &EventSourceControl{evs: libebpf.NewEventSource(psnap, cfg, compiler)} } func (s *EventSourceControl) Open(cfg *config.Config) error { @@ -41,10 +51,18 @@ func (s *EventSourceControl) Close() error { return s.evs.Close() } -type stubEventSource struct{} +func (s *EventSourceControl) Errors() <-chan error { + return s.evs.Errors() +} + +func (s *EventSourceControl) Events() <-chan *event.Event { + return s.evs.Events() +} -func (*stubEventSource) Open(*config.Config) error { - return nil +func (s *EventSourceControl) SetFilter(f filter.Filter) { + s.evs.SetFilter(f) } -func (*stubEventSource) Close() error { return nil } +func (s *EventSourceControl) RegisterEventListener(lis event.Listener) { + s.evs.RegisterEventListener(lis) +} diff --git a/internal/ebpf/btf.go b/internal/ebpf/btf.go new file mode 100644 index 000000000..0e4d96d78 --- /dev/null +++ b/internal/ebpf/btf.go @@ -0,0 +1,33 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "fmt" + "runtime" +) + +func checkRuntimeSupport() (*PrerequisiteReport, error) { + if runtime.GOARCH != "amd64" { + return nil, fmt.Errorf("linux eBPF capture requires amd64, got %s", runtime.GOARCH) + } + return ProbePrerequisites() +} diff --git a/internal/ebpf/c/clone.bpf.c b/internal/ebpf/c/clone.bpf.c new file mode 100644 index 000000000..3c452ff34 --- /dev/null +++ b/internal/ebpf/c/clone.bpf.c @@ -0,0 +1,164 @@ +//go:build ignore + +#include "common/events.h" +#include "bpf_core_read.h" +#include "bpf_tracing.h" + +char LICENSE[] SEC("license") = "Dual MIT/GPL"; + +static __always_inline int handle_clone_enter(u64 flags, u32 syscall_id) +{ + u64 key = bpf_get_current_pid_tgid(); + struct scratch_value val = {}; + + val.arg0 = flags; + val.arg1 = syscall_id; + bpf_map_update_elem(&scratch, &key, &val, BPF_ANY); + return 0; +} + +static __always_inline int handle_clone_exit(long ret, u32 syscall_id) +{ + u64 key = bpf_get_current_pid_tgid(); + struct scratch_value *val; + struct syscall_event *e; + struct task_struct *task; + struct task_struct *parent; + const struct cred *cred; + + /* Successful clones are emitted from sched_process_fork with child identity. + * Leave scratch in place so the fork handler can recover clone flags. + */ + if (ret >= 0) + return 0; + + e = reserve_event(); + if (!e) + return 0; + + e->type = EVT_TYPE_CLONE; + e->syscall_id = syscall_id; + e->retval = ret; + fill_current_ids(e); + + task = (struct task_struct *)bpf_get_current_task(); + e->start_boottime = BPF_CORE_READ(task, start_boottime); + parent = BPF_CORE_READ(task, real_parent); + if (parent) + e->ppid = BPF_CORE_READ(parent, tgid); + cred = BPF_CORE_READ(task, real_cred); + if (cred) { + e->uid = BPF_CORE_READ(cred, euid.val); + e->gid = BPF_CORE_READ(cred, egid.val); + } + + val = bpf_map_lookup_elem(&scratch, &key); + if (val) { + e->flags = val->arg0; + if (!e->syscall_id) + e->syscall_id = (u32)val->arg1; + } + bpf_map_delete_elem(&scratch, &key); + + bpf_ringbuf_submit(e, 0); + return 0; +} + +SEC("tp/syscalls/sys_enter_clone") +int handle_sys_enter_clone(struct trace_event_raw_sys_enter *ctx) +{ + return handle_clone_enter(ctx->args[0], (u32)ctx->id); +} + +SEC("tp/syscalls/sys_exit_clone") +int handle_sys_exit_clone(struct trace_event_raw_sys_exit *ctx) +{ + return handle_clone_exit(ctx->ret, (u32)ctx->id); +} + +SEC("tp/syscalls/sys_enter_clone3") +int handle_sys_enter_clone3(struct trace_event_raw_sys_enter *ctx) +{ + u64 flags = 0; + + bpf_probe_read_user(&flags, sizeof(flags), (const void *)ctx->args[0]); + return handle_clone_enter(flags, (u32)ctx->id); +} + +SEC("tp/syscalls/sys_exit_clone3") +int handle_sys_exit_clone3(struct trace_event_raw_sys_exit *ctx) +{ + return handle_clone_exit(ctx->ret, (u32)ctx->id); +} + +SEC("tp/syscalls/sys_enter_fork") +int handle_sys_enter_fork(struct trace_event_raw_sys_enter *ctx) +{ + return handle_clone_enter(0, (u32)ctx->id); +} + +SEC("tp/syscalls/sys_exit_fork") +int handle_sys_exit_fork(struct trace_event_raw_sys_exit *ctx) +{ + return handle_clone_exit(ctx->ret, (u32)ctx->id); +} + +SEC("tp/syscalls/sys_enter_vfork") +int handle_sys_enter_vfork(struct trace_event_raw_sys_enter *ctx) +{ + return handle_clone_enter(0, (u32)ctx->id); +} + +SEC("tp/syscalls/sys_exit_vfork") +int handle_sys_exit_vfork(struct trace_event_raw_sys_exit *ctx) +{ + return handle_clone_exit(ctx->ret, (u32)ctx->id); +} + +SEC("tp_btf/sched_process_fork") +int BPF_PROG(handle_sched_process_fork, struct task_struct *parent, struct task_struct *child) +{ + struct syscall_event *e; + struct scratch_value *val; + const struct cred *cred; + struct task_struct *real_parent; + u64 key; + + if (!child || !parent) + return 0; + + e = reserve_event(); + if (!e) + return 0; + + e->type = EVT_TYPE_CLONE; + e->retval = child->tgid; + e->pid = child->tgid; + e->tid = child->pid; + e->tgid = child->tgid; + e->start_boottime = child->start_boottime; + __builtin_memcpy(&e->comm, child->comm, sizeof(e->comm)); + + real_parent = child->real_parent; + if (real_parent) + e->ppid = real_parent->tgid; + else + e->ppid = parent->tgid; + + cred = child->real_cred; + if (cred) { + e->uid = cred->euid.val; + e->gid = cred->egid.val; + } + + key = ((u64)parent->tgid << 32) | (u32)parent->pid; + val = bpf_map_lookup_elem(&scratch, &key); + if (val) { + e->flags = val->arg0; + e->syscall_id = (u32)val->arg1; + bpf_map_delete_elem(&scratch, &key); + } + + bpf_ringbuf_submit(e, 0); + return 0; +} diff --git a/internal/ebpf/c/common/events.h b/internal/ebpf/c/common/events.h new file mode 100644 index 000000000..4328072f6 --- /dev/null +++ b/internal/ebpf/c/common/events.h @@ -0,0 +1,120 @@ +/* Shared event and map definitions for the Linux eBPF backend. */ + +#pragma once + +#include "vmlinux.h" +#include "bpf_helpers.h" + +#ifndef BPF_ANY +#define BPF_ANY 0 +#endif + +#define EVT_FILENAME_LEN 256 + +struct trace_event_raw_sys_enter { + unsigned short common_type; + unsigned char common_flags; + unsigned char common_preempt_count; + int common_pid; + long id; + unsigned long args[6]; +}; + +struct trace_event_raw_sys_exit { + unsigned short common_type; + unsigned char common_flags; + unsigned char common_preempt_count; + int common_pid; + long id; + long ret; +}; + +/* Stable semantic type IDs. Keep in sync with pkg/event.Type on Linux. + * Zero is reserved for iter/task baseline snapshot records, which feed + * the process state and are never dispatched as events. + */ +#define EVT_TYPE_SNAPSHOT 0 +#define EVT_TYPE_EXECVE 1 +#define EVT_TYPE_EXIT 2 +#define EVT_TYPE_CLONE 3 + +struct syscall_event { + u32 type; + u32 pid; + u32 tid; + u32 tgid; + u32 ppid; + u32 uid; + u32 gid; + u32 syscall_id; + s64 retval; + /* Interpreted per event type; clone stores the raw clone flags. */ + u64 flags; + u64 start_boottime; + u64 timestamp_ns; + u8 comm[TASK_COMM_LEN]; + u8 filename[EVT_FILENAME_LEN]; +}; + +struct scratch_value { + u64 arg0; + u64 arg1; + u8 filename[EVT_FILENAME_LEN]; +}; + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 1 << 24); +} events SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, u32); + __type(value, u64); +} drop_count SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_HASH); + __uint(max_entries, 8192); + __type(key, u64); + __type(value, struct scratch_value); +} scratch SEC(".maps"); + +static __always_inline void account_drop(void) +{ + u32 key = 0; + u64 *count; + + count = bpf_map_lookup_elem(&drop_count, &key); + if (!count) + return; + __sync_fetch_and_add(count, 1); +} + +static __always_inline struct syscall_event *reserve_event(void) +{ + struct syscall_event *e; + + e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); + if (!e) { + account_drop(); + return NULL; + } + __builtin_memset(e, 0, sizeof(*e)); + e->timestamp_ns = bpf_ktime_get_ns(); + return e; +} + +static __always_inline void fill_current_ids(struct syscall_event *e) +{ + u64 id = bpf_get_current_pid_tgid(); + u64 uidgid = bpf_get_current_uid_gid(); + + e->tgid = id >> 32; + e->tid = (u32)id; + e->pid = e->tgid; + e->uid = (u32)uidgid; + e->gid = uidgid >> 32; + bpf_get_current_comm(&e->comm, sizeof(e->comm)); +} diff --git a/internal/ebpf/c/execve.bpf.c b/internal/ebpf/c/execve.bpf.c new file mode 100644 index 000000000..32211a588 --- /dev/null +++ b/internal/ebpf/c/execve.bpf.c @@ -0,0 +1,84 @@ +//go:build ignore + +#include "common/events.h" +#include "bpf_core_read.h" +#include "bpf_tracing.h" + +char LICENSE[] SEC("license") = "Dual MIT/GPL"; + +static __always_inline int handle_exec_enter(const char *filename, u32 syscall_id) +{ + u64 key = bpf_get_current_pid_tgid(); + struct scratch_value val = {}; + + val.arg1 = syscall_id; + if (filename) + bpf_probe_read_user_str(&val.filename, sizeof(val.filename), filename); + bpf_map_update_elem(&scratch, &key, &val, BPF_ANY); + return 0; +} + +static __always_inline int handle_exec_exit(long ret, u32 syscall_id) +{ + u64 key = bpf_get_current_pid_tgid(); + struct scratch_value *val; + struct syscall_event *e; + struct task_struct *task; + struct task_struct *parent; + const struct cred *cred; + + e = reserve_event(); + if (!e) + return 0; + + e->type = EVT_TYPE_EXECVE; + e->syscall_id = syscall_id; + e->retval = ret; + fill_current_ids(e); + + task = (struct task_struct *)bpf_get_current_task(); + e->start_boottime = BPF_CORE_READ(task, start_boottime); + parent = BPF_CORE_READ(task, real_parent); + if (parent) + e->ppid = BPF_CORE_READ(parent, tgid); + cred = BPF_CORE_READ(task, real_cred); + if (cred) { + e->uid = BPF_CORE_READ(cred, euid.val); + e->gid = BPF_CORE_READ(cred, egid.val); + } + + val = bpf_map_lookup_elem(&scratch, &key); + if (val) { + if (!e->syscall_id) + e->syscall_id = (u32)val->arg1; + __builtin_memcpy(&e->filename, val->filename, sizeof(e->filename)); + } + bpf_map_delete_elem(&scratch, &key); + + bpf_ringbuf_submit(e, 0); + return 0; +} + +SEC("tp/syscalls/sys_enter_execve") +int handle_sys_enter_execve(struct trace_event_raw_sys_enter *ctx) +{ + return handle_exec_enter((const char *)ctx->args[0], (u32)ctx->id); +} + +SEC("tp/syscalls/sys_exit_execve") +int handle_sys_exit_execve(struct trace_event_raw_sys_exit *ctx) +{ + return handle_exec_exit(ctx->ret, (u32)ctx->id); +} + +SEC("tp/syscalls/sys_enter_execveat") +int handle_sys_enter_execveat(struct trace_event_raw_sys_enter *ctx) +{ + return handle_exec_enter((const char *)ctx->args[1], (u32)ctx->id); +} + +SEC("tp/syscalls/sys_exit_execveat") +int handle_sys_exit_execveat(struct trace_event_raw_sys_exit *ctx) +{ + return handle_exec_exit(ctx->ret, (u32)ctx->id); +} diff --git a/internal/ebpf/c/exit.bpf.c b/internal/ebpf/c/exit.bpf.c new file mode 100644 index 000000000..f5569f792 --- /dev/null +++ b/internal/ebpf/c/exit.bpf.c @@ -0,0 +1,55 @@ +//go:build ignore + +#include "common/events.h" +#include "bpf_core_read.h" +#include "bpf_tracing.h" + +char LICENSE[] SEC("license") = "Dual MIT/GPL"; + +/* Process exits are captured from sched_process_exit rather than the + * syscalls/sys_exit_{exit,exit_group} tracepoints: those syscalls never + * return, so their exit tracepoints never fire. The scheduler hook also + * covers processes terminated by signals, which never enter exit_group. + */ +SEC("tp_btf/sched_process_exit") +int BPF_PROG(handle_sched_process_exit, struct task_struct *task) +{ + struct syscall_event *e; + struct task_struct *parent; + const struct cred *cred; + + if (!task) + return 0; + + /* Only emit process exits from the thread-group leader; thread + * exits are not process exits. + */ + if (task->pid != task->tgid) + return 0; + + e = reserve_event(); + if (!e) + return 0; + + e->type = EVT_TYPE_EXIT; + /* Raw wait status: (code << 8) | termination signal. */ + e->retval = task->exit_code; + e->pid = task->tgid; + e->tid = task->pid; + e->tgid = task->tgid; + e->start_boottime = task->start_boottime; + __builtin_memcpy(&e->comm, task->comm, sizeof(e->comm)); + + parent = task->real_parent; + if (parent) + e->ppid = parent->tgid; + + cred = task->real_cred; + if (cred) { + e->uid = cred->euid.val; + e->gid = cred->egid.val; + } + + bpf_ringbuf_submit(e, 0); + return 0; +} diff --git a/internal/ebpf/c/proc_iter.bpf.c b/internal/ebpf/c/proc_iter.bpf.c new file mode 100644 index 000000000..628a190cf --- /dev/null +++ b/internal/ebpf/c/proc_iter.bpf.c @@ -0,0 +1,59 @@ +//go:build ignore + +#include "common/events.h" +#include "bpf_core_read.h" + +char LICENSE[] SEC("license") = "Dual MIT/GPL"; + +/* Task iterator baseline scanner. + * + * Intentionally omits bpf_d_path (available from Linux 5.10). Executable path + * and cmdline are left empty here and are filled best-effort from + * /proc//{exe,cmdline} in userspace. + * + * ctx->task is a trusted BTF pointer, so field access uses CO-RE direct reads + * rather than bpf_probe_read-based helpers. + */ +SEC("iter/task") +int dump_task(struct bpf_iter__task *ctx) +{ + struct task_struct *task; + struct task_struct *parent; + const struct cred *cred; + struct syscall_event *e; + pid_t pid; + pid_t tgid; + + task = ctx->task; + if (!task) + return 0; + + pid = task->pid; + tgid = task->tgid; + if (pid != tgid) + return 0; + + e = reserve_event(); + if (!e) + return 0; + + e->type = EVT_TYPE_SNAPSHOT; + e->pid = tgid; + e->tid = pid; + e->tgid = tgid; + e->start_boottime = task->start_boottime; + __builtin_memcpy(&e->comm, task->comm, sizeof(e->comm)); + + parent = task->real_parent; + if (parent) + e->ppid = parent->tgid; + + cred = task->real_cred; + if (cred) { + e->uid = cred->euid.val; + e->gid = cred->egid.val; + } + + bpf_ringbuf_submit(e, 0); + return 0; +} diff --git a/internal/ebpf/clone_bpfeb.go b/internal/ebpf/clone_bpfeb.go new file mode 100644 index 000000000..0c1de89c0 --- /dev/null +++ b/internal/ebpf/clone_bpfeb.go @@ -0,0 +1,171 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (mips || mips64 || ppc64 || s390x) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type cloneScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadClone returns the embedded CollectionSpec for clone. +func loadClone() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_CloneBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load clone: %w", err) + } + + return spec, err +} + +// loadCloneObjects loads clone and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *cloneObjects +// *clonePrograms +// *cloneMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadCloneObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadClone() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// cloneSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneSpecs struct { + cloneProgramSpecs + cloneMapSpecs + cloneVariableSpecs +} + +// cloneProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneProgramSpecs struct { + HandleSchedProcessFork *ebpf.ProgramSpec `ebpf:"handle_sched_process_fork"` + HandleSysEnterClone *ebpf.ProgramSpec `ebpf:"handle_sys_enter_clone"` + HandleSysEnterClone3 *ebpf.ProgramSpec `ebpf:"handle_sys_enter_clone3"` + HandleSysEnterFork *ebpf.ProgramSpec `ebpf:"handle_sys_enter_fork"` + HandleSysEnterVfork *ebpf.ProgramSpec `ebpf:"handle_sys_enter_vfork"` + HandleSysExitClone *ebpf.ProgramSpec `ebpf:"handle_sys_exit_clone"` + HandleSysExitClone3 *ebpf.ProgramSpec `ebpf:"handle_sys_exit_clone3"` + HandleSysExitFork *ebpf.ProgramSpec `ebpf:"handle_sys_exit_fork"` + HandleSysExitVfork *ebpf.ProgramSpec `ebpf:"handle_sys_exit_vfork"` +} + +// cloneMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// cloneVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneVariableSpecs struct { +} + +// cloneObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type cloneObjects struct { + clonePrograms + cloneMaps + cloneVariables +} + +func (o *cloneObjects) Close() error { + return _CloneClose( + &o.clonePrograms, + &o.cloneMaps, + ) +} + +// cloneMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type cloneMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *cloneMaps) Close() error { + return _CloneClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// cloneVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type cloneVariables struct { +} + +// clonePrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type clonePrograms struct { + HandleSchedProcessFork *ebpf.Program `ebpf:"handle_sched_process_fork"` + HandleSysEnterClone *ebpf.Program `ebpf:"handle_sys_enter_clone"` + HandleSysEnterClone3 *ebpf.Program `ebpf:"handle_sys_enter_clone3"` + HandleSysEnterFork *ebpf.Program `ebpf:"handle_sys_enter_fork"` + HandleSysEnterVfork *ebpf.Program `ebpf:"handle_sys_enter_vfork"` + HandleSysExitClone *ebpf.Program `ebpf:"handle_sys_exit_clone"` + HandleSysExitClone3 *ebpf.Program `ebpf:"handle_sys_exit_clone3"` + HandleSysExitFork *ebpf.Program `ebpf:"handle_sys_exit_fork"` + HandleSysExitVfork *ebpf.Program `ebpf:"handle_sys_exit_vfork"` +} + +func (p *clonePrograms) Close() error { + return _CloneClose( + p.HandleSchedProcessFork, + p.HandleSysEnterClone, + p.HandleSysEnterClone3, + p.HandleSysEnterFork, + p.HandleSysEnterVfork, + p.HandleSysExitClone, + p.HandleSysExitClone3, + p.HandleSysExitFork, + p.HandleSysExitVfork, + ) +} + +func _CloneClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed clone_bpfeb.o +var _CloneBytes []byte diff --git a/internal/ebpf/clone_bpfeb.o b/internal/ebpf/clone_bpfeb.o new file mode 100644 index 000000000..40ba3e286 Binary files /dev/null and b/internal/ebpf/clone_bpfeb.o differ diff --git a/internal/ebpf/clone_bpfel.go b/internal/ebpf/clone_bpfel.go new file mode 100644 index 000000000..36266195f --- /dev/null +++ b/internal/ebpf/clone_bpfel.go @@ -0,0 +1,171 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type cloneScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadClone returns the embedded CollectionSpec for clone. +func loadClone() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_CloneBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load clone: %w", err) + } + + return spec, err +} + +// loadCloneObjects loads clone and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *cloneObjects +// *clonePrograms +// *cloneMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadCloneObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadClone() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// cloneSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneSpecs struct { + cloneProgramSpecs + cloneMapSpecs + cloneVariableSpecs +} + +// cloneProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneProgramSpecs struct { + HandleSchedProcessFork *ebpf.ProgramSpec `ebpf:"handle_sched_process_fork"` + HandleSysEnterClone *ebpf.ProgramSpec `ebpf:"handle_sys_enter_clone"` + HandleSysEnterClone3 *ebpf.ProgramSpec `ebpf:"handle_sys_enter_clone3"` + HandleSysEnterFork *ebpf.ProgramSpec `ebpf:"handle_sys_enter_fork"` + HandleSysEnterVfork *ebpf.ProgramSpec `ebpf:"handle_sys_enter_vfork"` + HandleSysExitClone *ebpf.ProgramSpec `ebpf:"handle_sys_exit_clone"` + HandleSysExitClone3 *ebpf.ProgramSpec `ebpf:"handle_sys_exit_clone3"` + HandleSysExitFork *ebpf.ProgramSpec `ebpf:"handle_sys_exit_fork"` + HandleSysExitVfork *ebpf.ProgramSpec `ebpf:"handle_sys_exit_vfork"` +} + +// cloneMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// cloneVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type cloneVariableSpecs struct { +} + +// cloneObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type cloneObjects struct { + clonePrograms + cloneMaps + cloneVariables +} + +func (o *cloneObjects) Close() error { + return _CloneClose( + &o.clonePrograms, + &o.cloneMaps, + ) +} + +// cloneMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type cloneMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *cloneMaps) Close() error { + return _CloneClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// cloneVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type cloneVariables struct { +} + +// clonePrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadCloneObjects or ebpf.CollectionSpec.LoadAndAssign. +type clonePrograms struct { + HandleSchedProcessFork *ebpf.Program `ebpf:"handle_sched_process_fork"` + HandleSysEnterClone *ebpf.Program `ebpf:"handle_sys_enter_clone"` + HandleSysEnterClone3 *ebpf.Program `ebpf:"handle_sys_enter_clone3"` + HandleSysEnterFork *ebpf.Program `ebpf:"handle_sys_enter_fork"` + HandleSysEnterVfork *ebpf.Program `ebpf:"handle_sys_enter_vfork"` + HandleSysExitClone *ebpf.Program `ebpf:"handle_sys_exit_clone"` + HandleSysExitClone3 *ebpf.Program `ebpf:"handle_sys_exit_clone3"` + HandleSysExitFork *ebpf.Program `ebpf:"handle_sys_exit_fork"` + HandleSysExitVfork *ebpf.Program `ebpf:"handle_sys_exit_vfork"` +} + +func (p *clonePrograms) Close() error { + return _CloneClose( + p.HandleSchedProcessFork, + p.HandleSysEnterClone, + p.HandleSysEnterClone3, + p.HandleSysEnterFork, + p.HandleSysEnterVfork, + p.HandleSysExitClone, + p.HandleSysExitClone3, + p.HandleSysExitFork, + p.HandleSysExitVfork, + ) +} + +func _CloneClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed clone_bpfel.o +var _CloneBytes []byte diff --git a/internal/ebpf/clone_bpfel.o b/internal/ebpf/clone_bpfel.o new file mode 100644 index 000000000..9985e02c1 Binary files /dev/null and b/internal/ebpf/clone_bpfel.o differ diff --git a/internal/ebpf/consumer.go b/internal/ebpf/consumer.go new file mode 100644 index 000000000..ca1d022cf --- /dev/null +++ b/internal/ebpf/consumer.go @@ -0,0 +1,189 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "expvar" + + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/ps" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" +) + +var ( + eventsProcessed = expvar.NewInt("ebpf.events.processed") + eventsExcluded = expvar.NewInt("ebpf.events.excluded") + eventsUnknown = expvar.NewInt("ebpf.events.unknown") + parseErrors = expvar.NewInt("ebpf.events.parse.errors") + pendingQueued = expvar.NewInt("ebpf.startup.pending.queued") + pendingDropped = expvar.NewInt("ebpf.startup.pending.dropped") + replayApplied = expvar.NewInt("ebpf.startup.replay.applied") + snapshotUpserts = expvar.NewInt("ebpf.startup.snapshot.upserts") + lateSnapshots = expvar.NewInt("ebpf.startup.snapshot.late") + enrichmentMiss = expvar.NewInt("ebpf.enrichment.miss") + ringbufDrops = expvar.NewInt("ebpf.ringbuf.drops") +) + +func applyProcessState(psnap ps.Snapshotter, evt *event.Event) { + if evt == nil || psnap == nil { + return + } + switch { + case evt.Type == event.Execve && succeeded(evt): + ps := buildPS(evt, psnap) + evt.PS = ps + _ = psnap.Write(evt) + case evt.Type == event.Clone && succeeded(evt) && evt.IsCreateProcess(): + ps := buildPS(evt, psnap) + evt.PS = ps + psnap.Put(ps) + case evt.Type == event.Clone && succeeded(evt) && evt.IsCreateThread(): + ok, existing := psnap.Find(evt.PID) + if ok && existing != nil { + evt.PS = existing + _ = psnap.AddThread(evt) + } + case evt.Type == event.Exit: + ok, existing := psnap.Find(evt.PID) + if ok { + evt.PS = existing + } + _ = psnap.Remove(evt) + default: + ok, existing := psnap.Find(evt.PID) + if ok { + evt.PS = existing + } + } + if evt.PS == nil { + ok, existing := psnap.Find(evt.PID) + if ok { + evt.PS = existing + } + } +} + +func upsertSnapshot(psnap ps.Snapshotter, rec *pstypes.PS) { + if psnap == nil || rec == nil { + return + } + ok, existing := psnap.Find(rec.PID) + if !ok || existing == nil { + psnap.Put(rec) + snapshotUpserts.Add(1) + return + } + if existing.StartBootTime != rec.StartBootTime { + psnap.Put(rec) + snapshotUpserts.Add(1) + return + } + mergePS(existing, rec) + snapshotUpserts.Add(1) +} + +func mergePS(dst, src *pstypes.PS) { + if src.Name != "" { + dst.Name = src.Name + } + if src.Exe != "" { + dst.Exe = src.Exe + } + if src.Cmdline != "" { + dst.Cmdline = src.Cmdline + } + if src.Ppid != 0 { + dst.Ppid = src.Ppid + } + if src.UID != 0 { + dst.UID = src.UID + } + if src.GID != 0 { + dst.GID = src.GID + } + if src.StartBootTime != 0 { + dst.StartBootTime = src.StartBootTime + } +} + +func buildPS(evt *event.Event, psnap ps.Snapshotter) *pstypes.PS { + ok, existing := psnap.Find(evt.PID) + exe := evt.GetParamAsString(params.Exe) + cmdline := evt.GetParamAsString(params.Cmdline) + name := evt.GetParamAsString(params.ProcessName) + if exe == "" && existing != nil { + exe = existing.Exe + } + if cmdline == "" && existing != nil { + cmdline = existing.Cmdline + } + if name == "" { + name = baseName(exe) + } + ps := &pstypes.PS{ + PID: evt.PID, + Ppid: evt.GetParamAsUint64(params.ProcessParentID), + Name: name, + Cmdline: cmdline, + Exe: exe, + Args: splitCmdline(cmdline), + StartBootTime: evt.GetParamAsUint64(params.StartBootTime), + UID: evt.GetParamAsUint32(params.UID), + GID: evt.GetParamAsUint32(params.GID), + Threads: make(map[uint64]pstypes.Thread), + } + if ok && existing != nil && existing.StartBootTime == ps.StartBootTime && existing.Threads != nil { + ps.Threads = existing.Threads + ps.Parent = existing.Parent + } + if ok, parent := psnap.Find(ps.Ppid); ok { + ps.Parent = parent + } + return ps +} + +func succeeded(evt *event.Event) bool { + ret, err := evt.Params.GetInt64(params.Retval) + if err != nil { + return true + } + return ret >= 0 +} + +func enrichEvent(evt *event.Event) { + if evt == nil { + return + } + exe, cmdline, err := enrichFromProc(evt.PID) + if err != nil { + enrichmentMiss.Add(1) + } + if exe != "" { + evt.Params.Append(params.Exe, params.Path, exe) + if evt.GetParamAsString(params.ProcessName) == "" { + evt.Params.Append(params.ProcessName, params.String, baseName(exe)) + } + } + if cmdline != "" { + evt.Params.Append(params.Cmdline, params.String, cmdline) + } +} diff --git a/internal/ebpf/consumer_test.go b/internal/ebpf/consumer_test.go new file mode 100644 index 000000000..edc6d1646 --- /dev/null +++ b/internal/ebpf/consumer_test.go @@ -0,0 +1,98 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "testing" + + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/ps" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestApplyProcessStateExecveAndPIDReuse(t *testing.T) { + snap := ps.NewSnapshotter() + first := sampleExecve().toEvent() + applyProcessState(snap, first) + ok, got := snap.Find(4242) + require.True(t, ok) + assert.Equal(t, "/bin/bash", got.Exe) + assert.Equal(t, uint64(123456789), got.StartBootTime) + assert.Equal(t, got.UUID(), first.PS.UUID()) + + reuse := sampleExecve().toEvent() + reuse.Params.Append(params.StartBootTime, params.Uint64, uint64(999)) + reuse.Params.Append(params.Exe, params.Path, "/bin/sh") + reuse.Params.Append(params.ProcessName, params.String, "sh") + applyProcessState(snap, reuse) + ok, got = snap.Find(4242) + require.True(t, ok) + assert.Equal(t, "/bin/sh", got.Exe) + assert.Equal(t, uint64(999), got.StartBootTime) + assert.NotEqual(t, first.PS.UUID(), got.UUID()) +} + +func TestApplyProcessStateExitRemoves(t *testing.T) { + snap := ps.NewSnapshotter() + evt := sampleExecve().toEvent() + applyProcessState(snap, evt) + exit := &event.Event{ + Type: event.Exit, + PID: 4242, + Params: event.Params{}, + } + exit.Params.Append(params.Retval, params.Int64, int64(0)) + applyProcessState(snap, exit) + ok, _ := snap.Find(4242) + assert.False(t, ok) +} + +func TestUpsertSnapshotIgnoresAfterLiveReplacement(t *testing.T) { + snap := ps.NewSnapshotter() + rec := &pstypes.PS{PID: 7, Name: "snap", StartBootTime: 9, Exe: "/bin/snap"} + upsertSnapshot(snap, rec) + ok, got := snap.Find(7) + require.True(t, ok) + assert.Equal(t, "snap", got.Name) + + live := &event.Event{Type: event.Execve, PID: 7, Params: event.Params{}} + live.Params.Append(params.ProcessName, params.String, "live") + live.Params.Append(params.Exe, params.Path, "/bin/live") + live.Params.Append(params.StartBootTime, params.Uint64, uint64(9)) + live.Params.Append(params.Retval, params.Int64, int64(0)) + applyProcessState(snap, live) + + upsertSnapshot(snap, &pstypes.PS{PID: 7, Name: "stale", StartBootTime: 9, Exe: "/bin/stale"}) + ok, got = snap.Find(7) + require.True(t, ok) + assert.Equal(t, "stale", got.Name) +} + +func TestPendingBackpressure(t *testing.T) { + es := &EventSource{pendingCap: 2} + require.True(t, es.enqueuePending(&event.Event{PID: 1})) + require.True(t, es.enqueuePending(&event.Event{PID: 2})) + require.False(t, es.enqueuePending(&event.Event{PID: 3})) + assert.Len(t, es.pending, 2) +} diff --git a/internal/ebpf/doc.go b/internal/ebpf/doc.go index ca37ad037..8d281edf7 100644 --- a/internal/ebpf/doc.go +++ b/internal/ebpf/doc.go @@ -16,7 +16,8 @@ * limitations under the License. */ -// Package ebpf hosts the Linux eBPF instrumentation backend and supporting -// feasibility prototypes used to validate CO-RE load, feature probes, and -// race-safe process-state reconciliation. +// Package ebpf hosts the Linux eBPF instrumentation backend. The process +// event source loads CO-RE programs, captures execve/exit/clone, and +// reconciles live events against an iter/task baseline. The spike +// subdirectory remains as a feasibility prototype. package ebpf diff --git a/internal/ebpf/enrich.go b/internal/ebpf/enrich.go new file mode 100644 index 000000000..19bbe0741 --- /dev/null +++ b/internal/ebpf/enrich.go @@ -0,0 +1,52 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "fmt" + "os" + "strings" +) + +// enrichFromProc best-effort reads /proc//{exe,cmdline}. Failures leave +// fields empty and never replace the iterator baseline scan. +func enrichFromProc(pid uint64) (exe string, cmdline string, err error) { + exe, err = os.Readlink(fmt.Sprintf("/proc/%d/exe", pid)) + if err != nil { + exe = "" + } + + raw, cerr := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + if cerr != nil { + if err == nil { + err = cerr + } + return exe, "", err + } + parts := strings.Split(string(raw), "\x00") + cleaned := make([]string, 0, len(parts)) + for _, p := range parts { + if p != "" { + cleaned = append(cleaned, p) + } + } + return exe, strings.Join(cleaned, " "), err +} diff --git a/internal/ebpf/events.go b/internal/ebpf/events.go new file mode 100644 index 000000000..3a54d2ba5 --- /dev/null +++ b/internal/ebpf/events.go @@ -0,0 +1,142 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "bytes" + "encoding/binary" + "fmt" + "time" + + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/util/hostname" +) + +const ( + // snapshotType marks iter/task baseline records. It maps to + // event.UnknownType, so snapshots can never leak as live events. + snapshotType = 0 + + commLen = 16 + filenameLen = 256 +) + +// rawEvent mirrors struct syscall_event in c/common/events.h. +type rawEvent struct { + Type uint32 + PID uint32 + TID uint32 + TGID uint32 + PPID uint32 + UID uint32 + GID uint32 + SyscallID uint32 + Retval int64 + // Flags is interpreted per event type; clone stores the raw clone flags. + Flags uint64 + StartBootTime uint64 + TimestampNs uint64 + Comm [commLen]byte + Filename [filenameLen]byte +} + +func decodeRawEvent(raw []byte) (rawEvent, error) { + var ev rawEvent + if err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, &ev); err != nil { + return ev, fmt.Errorf("decoding eBPF event: %w", err) + } + return ev, nil +} + +func (r rawEvent) comm() string { return cString(r.Comm[:]) } +func (r rawEvent) filename() string { return cString(r.Filename[:]) } + +func (r rawEvent) eventType() event.Type { + switch r.Type { + case uint32(event.Execve): + return event.Execve + case uint32(event.Exit): + return event.Exit + case uint32(event.Clone): + return event.Clone + default: + return event.UnknownType + } +} + +func (r rawEvent) processID() uint64 { + if r.PID != 0 { + return uint64(r.PID) + } + return uint64(r.TGID) +} + +// toEvent converts the raw eBPF record into an event. The sequence number is +// assigned at dispatch time, mirroring the Windows consumer which increments +// the sequencer only for events that pass exclusion and filtering. +func (r rawEvent) toEvent() *event.Event { + typ := r.eventType() + info := event.TypeToEventInfo(typ) + evt := &event.Event{ + PID: r.processID(), + Tid: uint64(r.TID), + Type: typ, + Name: info.Name, + Category: info.Category, + Description: info.Description, + Host: hostname.Get(), + Timestamp: time.Unix(0, int64(r.TimestampNs)), + Params: event.Params{}, + Metadata: make(event.Metadata), + } + evt.Params.Append(params.ProcessID, params.PID, evt.PID) + evt.Params.Append(params.ThreadID, params.TID, evt.Tid) + evt.Params.Append(params.ProcessParentID, params.PID, uint64(r.PPID)) + evt.Params.Append(params.ProcessName, params.String, r.comm()) + evt.Params.Append(params.UID, params.Uint32, r.UID) + evt.Params.Append(params.GID, params.Uint32, r.GID) + evt.Params.Append(params.SyscallID, params.Uint32, r.SyscallID) + evt.Params.Append(params.Retval, params.Int64, r.Retval) + evt.Params.Append(params.StartBootTime, params.Uint64, r.StartBootTime) + + switch typ { + case event.Execve: + name := r.filename() + if name == "" { + name = r.comm() + } + evt.Params.Append(params.Exe, params.Path, name) + evt.Params.Append(params.Cmdline, params.String, name) + case event.Exit: + evt.Params.Append(params.ExitStatus, params.Int64, r.Retval) + case event.Clone: + evt.Params.Append(params.CloneFlags, params.Uint64, r.Flags) + } + return evt +} + +func cString(b []byte) string { + if i := bytes.IndexByte(b, 0); i >= 0 { + b = b[:i] + } + return string(b) +} diff --git a/internal/ebpf/events_test.go b/internal/ebpf/events_test.go new file mode 100644 index 000000000..da221ec6c --- /dev/null +++ b/internal/ebpf/events_test.go @@ -0,0 +1,148 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecodeRawEvent(t *testing.T) { + raw := sampleExecve() + buf := encodeRaw(t, raw) + got, err := decodeRawEvent(buf) + require.NoError(t, err) + assert.Equal(t, raw.Type, got.Type) + assert.Equal(t, raw.PID, got.PID) + assert.Equal(t, raw.StartBootTime, got.StartBootTime) + assert.Equal(t, "bash", got.comm()) + assert.Equal(t, "/bin/bash", got.filename()) +} + +func TestRawEventToEventGolden(t *testing.T) { + evt := sampleExecve().toEvent() + evt.Seq = 7 + got := goldenEvent(evt) + path := filepath.Join("testdata", "execve.json") + if os.Getenv("UPDATE_GOLDEN") == "1" { + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, prettyJSON(t, got), 0o644)) + } + want, err := os.ReadFile(path) + require.NoError(t, err) + assert.JSONEq(t, string(want), string(prettyJSON(t, got))) +} + +func TestCloneThreadSemantics(t *testing.T) { + raw := sampleClone(0x00010000) + evt := raw.toEvent() + assert.True(t, evt.IsCreateThread()) + assert.False(t, evt.IsCreateProcess()) + + raw = sampleClone(0) + evt = raw.toEvent() + assert.True(t, evt.IsCreateProcess()) + assert.False(t, evt.IsCreateThread()) +} + +func sampleExecve() rawEvent { + var ev rawEvent + ev.Type = uint32(event.Execve) + ev.PID = 4242 + ev.TID = 4242 + ev.TGID = 4242 + ev.PPID = 1 + ev.UID = 1000 + ev.GID = 1000 + ev.SyscallID = 59 + ev.Retval = 0 + ev.StartBootTime = 123456789 + ev.TimestampNs = 111 + copy(ev.Comm[:], "bash") + copy(ev.Filename[:], "/bin/bash") + return ev +} + +func sampleClone(flags uint64) rawEvent { + var ev rawEvent + ev.Type = uint32(event.Clone) + ev.PID = 99 + ev.TID = 100 + ev.TGID = 99 + ev.PPID = 1 + ev.Retval = 99 + ev.Flags = flags + ev.StartBootTime = 55 + copy(ev.Comm[:], "worker") + return ev +} + +func encodeRaw(t *testing.T, ev rawEvent) []byte { + t.Helper() + var buf bytes.Buffer + require.NoError(t, binary.Write(&buf, binary.LittleEndian, ev)) + return buf.Bytes() +} + +type golden struct { + Name string `json:"name"` + PID uint64 `json:"pid"` + Tid uint64 `json:"tid"` + Seq uint64 `json:"seq"` + Params map[string]string `json:"params"` +} + +func goldenEvent(evt *event.Event) golden { + g := golden{ + Name: evt.Name, + PID: evt.PID, + Tid: evt.Tid, + Seq: evt.Seq, + Params: map[string]string{}, + } + for name := range evt.Params { + g.Params[name] = evt.GetParamAsString(name) + } + return g +} + +func prettyJSON(t *testing.T, v any) []byte { + t.Helper() + buf, err := json.MarshalIndent(v, "", " ") + require.NoError(t, err) + return append(buf, '\n') +} + +func TestSucceededHelper(t *testing.T) { + evt := sampleExecve().toEvent() + assert.True(t, succeeded(evt)) + evt.Params.Append(params.Retval, params.Int64, int64(-2)) + assert.False(t, succeeded(evt)) +} diff --git a/internal/ebpf/execve_bpfeb.go b/internal/ebpf/execve_bpfeb.go new file mode 100644 index 000000000..cbad50c9e --- /dev/null +++ b/internal/ebpf/execve_bpfeb.go @@ -0,0 +1,156 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (mips || mips64 || ppc64 || s390x) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type execveScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadExecve returns the embedded CollectionSpec for execve. +func loadExecve() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ExecveBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load execve: %w", err) + } + + return spec, err +} + +// loadExecveObjects loads execve and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *execveObjects +// *execvePrograms +// *execveMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadExecveObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadExecve() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// execveSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveSpecs struct { + execveProgramSpecs + execveMapSpecs + execveVariableSpecs +} + +// execveProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveProgramSpecs struct { + HandleSysEnterExecve *ebpf.ProgramSpec `ebpf:"handle_sys_enter_execve"` + HandleSysEnterExecveat *ebpf.ProgramSpec `ebpf:"handle_sys_enter_execveat"` + HandleSysExitExecve *ebpf.ProgramSpec `ebpf:"handle_sys_exit_execve"` + HandleSysExitExecveat *ebpf.ProgramSpec `ebpf:"handle_sys_exit_execveat"` +} + +// execveMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// execveVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveVariableSpecs struct { +} + +// execveObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execveObjects struct { + execvePrograms + execveMaps + execveVariables +} + +func (o *execveObjects) Close() error { + return _ExecveClose( + &o.execvePrograms, + &o.execveMaps, + ) +} + +// execveMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execveMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *execveMaps) Close() error { + return _ExecveClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// execveVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execveVariables struct { +} + +// execvePrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execvePrograms struct { + HandleSysEnterExecve *ebpf.Program `ebpf:"handle_sys_enter_execve"` + HandleSysEnterExecveat *ebpf.Program `ebpf:"handle_sys_enter_execveat"` + HandleSysExitExecve *ebpf.Program `ebpf:"handle_sys_exit_execve"` + HandleSysExitExecveat *ebpf.Program `ebpf:"handle_sys_exit_execveat"` +} + +func (p *execvePrograms) Close() error { + return _ExecveClose( + p.HandleSysEnterExecve, + p.HandleSysEnterExecveat, + p.HandleSysExitExecve, + p.HandleSysExitExecveat, + ) +} + +func _ExecveClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed execve_bpfeb.o +var _ExecveBytes []byte diff --git a/internal/ebpf/execve_bpfeb.o b/internal/ebpf/execve_bpfeb.o new file mode 100644 index 000000000..6e17e3b90 Binary files /dev/null and b/internal/ebpf/execve_bpfeb.o differ diff --git a/internal/ebpf/execve_bpfel.go b/internal/ebpf/execve_bpfel.go new file mode 100644 index 000000000..7b1d8a45e --- /dev/null +++ b/internal/ebpf/execve_bpfel.go @@ -0,0 +1,156 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type execveScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadExecve returns the embedded CollectionSpec for execve. +func loadExecve() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ExecveBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load execve: %w", err) + } + + return spec, err +} + +// loadExecveObjects loads execve and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *execveObjects +// *execvePrograms +// *execveMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadExecveObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadExecve() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// execveSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveSpecs struct { + execveProgramSpecs + execveMapSpecs + execveVariableSpecs +} + +// execveProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveProgramSpecs struct { + HandleSysEnterExecve *ebpf.ProgramSpec `ebpf:"handle_sys_enter_execve"` + HandleSysEnterExecveat *ebpf.ProgramSpec `ebpf:"handle_sys_enter_execveat"` + HandleSysExitExecve *ebpf.ProgramSpec `ebpf:"handle_sys_exit_execve"` + HandleSysExitExecveat *ebpf.ProgramSpec `ebpf:"handle_sys_exit_execveat"` +} + +// execveMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// execveVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type execveVariableSpecs struct { +} + +// execveObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execveObjects struct { + execvePrograms + execveMaps + execveVariables +} + +func (o *execveObjects) Close() error { + return _ExecveClose( + &o.execvePrograms, + &o.execveMaps, + ) +} + +// execveMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execveMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *execveMaps) Close() error { + return _ExecveClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// execveVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execveVariables struct { +} + +// execvePrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadExecveObjects or ebpf.CollectionSpec.LoadAndAssign. +type execvePrograms struct { + HandleSysEnterExecve *ebpf.Program `ebpf:"handle_sys_enter_execve"` + HandleSysEnterExecveat *ebpf.Program `ebpf:"handle_sys_enter_execveat"` + HandleSysExitExecve *ebpf.Program `ebpf:"handle_sys_exit_execve"` + HandleSysExitExecveat *ebpf.Program `ebpf:"handle_sys_exit_execveat"` +} + +func (p *execvePrograms) Close() error { + return _ExecveClose( + p.HandleSysEnterExecve, + p.HandleSysEnterExecveat, + p.HandleSysExitExecve, + p.HandleSysExitExecveat, + ) +} + +func _ExecveClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed execve_bpfel.o +var _ExecveBytes []byte diff --git a/internal/ebpf/execve_bpfel.o b/internal/ebpf/execve_bpfel.o new file mode 100644 index 000000000..cc808bde2 Binary files /dev/null and b/internal/ebpf/execve_bpfel.o differ diff --git a/internal/ebpf/exit_bpfeb.go b/internal/ebpf/exit_bpfeb.go new file mode 100644 index 000000000..41ac2799d --- /dev/null +++ b/internal/ebpf/exit_bpfeb.go @@ -0,0 +1,147 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (mips || mips64 || ppc64 || s390x) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type exitScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadExit returns the embedded CollectionSpec for exit. +func loadExit() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ExitBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load exit: %w", err) + } + + return spec, err +} + +// loadExitObjects loads exit and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *exitObjects +// *exitPrograms +// *exitMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadExitObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadExit() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// exitSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitSpecs struct { + exitProgramSpecs + exitMapSpecs + exitVariableSpecs +} + +// exitProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitProgramSpecs struct { + HandleSchedProcessExit *ebpf.ProgramSpec `ebpf:"handle_sched_process_exit"` +} + +// exitMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// exitVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitVariableSpecs struct { +} + +// exitObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitObjects struct { + exitPrograms + exitMaps + exitVariables +} + +func (o *exitObjects) Close() error { + return _ExitClose( + &o.exitPrograms, + &o.exitMaps, + ) +} + +// exitMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *exitMaps) Close() error { + return _ExitClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// exitVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitVariables struct { +} + +// exitPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitPrograms struct { + HandleSchedProcessExit *ebpf.Program `ebpf:"handle_sched_process_exit"` +} + +func (p *exitPrograms) Close() error { + return _ExitClose( + p.HandleSchedProcessExit, + ) +} + +func _ExitClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed exit_bpfeb.o +var _ExitBytes []byte diff --git a/internal/ebpf/exit_bpfeb.o b/internal/ebpf/exit_bpfeb.o new file mode 100644 index 000000000..64f217fa0 Binary files /dev/null and b/internal/ebpf/exit_bpfeb.o differ diff --git a/internal/ebpf/exit_bpfel.go b/internal/ebpf/exit_bpfel.go new file mode 100644 index 000000000..0ddb7d384 --- /dev/null +++ b/internal/ebpf/exit_bpfel.go @@ -0,0 +1,147 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type exitScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadExit returns the embedded CollectionSpec for exit. +func loadExit() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ExitBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load exit: %w", err) + } + + return spec, err +} + +// loadExitObjects loads exit and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *exitObjects +// *exitPrograms +// *exitMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadExitObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadExit() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// exitSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitSpecs struct { + exitProgramSpecs + exitMapSpecs + exitVariableSpecs +} + +// exitProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitProgramSpecs struct { + HandleSchedProcessExit *ebpf.ProgramSpec `ebpf:"handle_sched_process_exit"` +} + +// exitMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// exitVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type exitVariableSpecs struct { +} + +// exitObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitObjects struct { + exitPrograms + exitMaps + exitVariables +} + +func (o *exitObjects) Close() error { + return _ExitClose( + &o.exitPrograms, + &o.exitMaps, + ) +} + +// exitMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *exitMaps) Close() error { + return _ExitClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// exitVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitVariables struct { +} + +// exitPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadExitObjects or ebpf.CollectionSpec.LoadAndAssign. +type exitPrograms struct { + HandleSchedProcessExit *ebpf.Program `ebpf:"handle_sched_process_exit"` +} + +func (p *exitPrograms) Close() error { + return _ExitClose( + p.HandleSchedProcessExit, + ) +} + +func _ExitClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed exit_bpfel.o +var _ExitBytes []byte diff --git a/internal/ebpf/exit_bpfel.o b/internal/ebpf/exit_bpfel.o new file mode 100644 index 000000000..853c4d11a Binary files /dev/null and b/internal/ebpf/exit_bpfel.o differ diff --git a/internal/ebpf/gen.go b/internal/ebpf/gen.go new file mode 100644 index 000000000..a0f07fc5c --- /dev/null +++ b/internal/ebpf/gen.go @@ -0,0 +1,26 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +// Regenerate with ./generate.sh (preferred) or go generate -tags linux. +// bpf2go version is pinned to github.com/cilium/ebpf v0.20.0. +// +//go:generate ./generate.sh diff --git a/internal/ebpf/generate.sh b/internal/ebpf/generate.sh new file mode 100755 index 000000000..e9ef3a51a --- /dev/null +++ b/internal/ebpf/generate.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Regenerate bpf2go bindings for the Linux eBPF process source. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +if ! command -v clang >/dev/null 2>&1; then + echo "clang is required to regenerate eBPF objects" >&2 + exit 1 +fi + +# The process source is x86_64-only. Always compile CO-RE objects for that +# architecture so generation is host-independent. +export BPF_TARGET_ARCH=x86 + +CLANG_VERSION="$(clang --version | head -n1)" +echo "clang: ${CLANG_VERSION}" +echo "target arch: ${BPF_TARGET_ARCH}" + +INCLUDES=(-I./c -I./c/common -I./spike/c -O2 -g "-D__TARGET_ARCH_${BPF_TARGET_ARCH}") + +generate() { + local name="$1" + local src="$2" + go run github.com/cilium/ebpf/cmd/bpf2go@v0.20.0 \ + -go-package ebpf -output-dir . -cc clang -target bpfel,bpfeb -tags linux \ + "${name}" "${src}" -- "${INCLUDES[@]}" +} + +generate execve ./c/execve.bpf.c +generate exit ./c/exit.bpf.c +generate clone ./c/clone.bpf.c +generate prociter ./c/proc_iter.bpf.c + +echo "generated eBPF bindings" diff --git a/internal/ebpf/loader.go b/internal/ebpf/loader.go new file mode 100644 index 000000000..83cc1887a --- /dev/null +++ b/internal/ebpf/loader.go @@ -0,0 +1,259 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + "sync" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + "github.com/cilium/ebpf/rlimit" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +type loader struct { + exec *execveObjects + exit *exitObjects + clone *cloneObjects + iter *prociterObjects + links []link.Link + iterLink *link.Iter + once sync.Once +} + +func loadCollections() (*loader, error) { + if err := rlimit.RemoveMemlock(); err != nil { + return nil, fmt.Errorf("removing memlock: %w", err) + } + + execSpec, err := loadExecve() + if err != nil { + return nil, fmt.Errorf("loading execve collection spec: %w", err) + } + exitSpec, err := loadExit() + if err != nil { + return nil, fmt.Errorf("loading exit collection spec: %w", err) + } + cloneSpec, err := loadClone() + if err != nil { + return nil, fmt.Errorf("loading clone collection spec: %w", err) + } + iterSpec, err := loadProciter() + if err != nil { + return nil, fmt.Errorf("loading prociter collection spec: %w", err) + } + + var execObjs execveObjects + if err := execSpec.LoadAndAssign(&execObjs, nil); err != nil { + return nil, fmt.Errorf("loading execve objects: %w", err) + } + + replacements := map[string]*ebpf.Map{ + eventsMapName: execObjs.Events, + dropCountMapName: execObjs.DropCount, + scratchMapName: execObjs.Scratch, + } + opts := &ebpf.CollectionOptions{MapReplacements: replacements} + + var exitObjs exitObjects + if err := exitSpec.LoadAndAssign(&exitObjs, opts); err != nil { + _ = execObjs.Close() + return nil, fmt.Errorf("loading exit objects: %w", err) + } + + var cloneObjs cloneObjects + if err := cloneSpec.LoadAndAssign(&cloneObjs, opts); err != nil { + _ = exitObjs.Close() + _ = execObjs.Close() + return nil, fmt.Errorf("loading clone objects: %w", err) + } + + var iterObjs prociterObjects + if err := iterSpec.LoadAndAssign(&iterObjs, opts); err != nil { + _ = cloneObjs.Close() + _ = exitObjs.Close() + _ = execObjs.Close() + return nil, fmt.Errorf("loading prociter objects: %w", err) + } + + return &loader{ + exec: &execObjs, + exit: &exitObjs, + clone: &cloneObjs, + iter: &iterObjs, + }, nil +} + +func (l *loader) eventsMap() *ebpf.Map { return l.exec.Events } + +func (l *loader) dropCount() uint64 { + var key uint32 + var drop uint64 + if l.exec == nil || l.exec.DropCount == nil { + return 0 + } + _ = l.exec.DropCount.Lookup(&key, &drop) + return drop +} + +// syscallsGroup is the tracefs group hosting raw syscall tracepoints. +const syscallsGroup = "syscalls" + +// attachPrograms attaches the live-capture programs: raw syscall tracepoints +// plus the scheduler tp_btf hooks. These feed the ring buffer continuously, +// in contrast to the one-shot iter/task snapshot program started by +// runTaskIterator. The caller starts the ring buffer reader first so no +// events are lost between attachment and consumption. +func (l *loader) attachPrograms() error { + type tp struct { + name string + prog *ebpf.Program + optional bool + } + tracepoints := []tp{ + {"sys_enter_execve", l.exec.HandleSysEnterExecve, false}, + {"sys_exit_execve", l.exec.HandleSysExitExecve, false}, + {"sys_enter_execveat", l.exec.HandleSysEnterExecveat, false}, + {"sys_exit_execveat", l.exec.HandleSysExitExecveat, false}, + {"sys_enter_clone", l.clone.HandleSysEnterClone, false}, + {"sys_exit_clone", l.clone.HandleSysExitClone, false}, + {"sys_enter_clone3", l.clone.HandleSysEnterClone3, false}, + {"sys_exit_clone3", l.clone.HandleSysExitClone3, false}, + // fork/vfork are legacy wrappers. libc uses clone/clone3, and some + // kernels refuse a perf link on these syscall tracepoints. + {"sys_enter_fork", l.clone.HandleSysEnterFork, true}, + {"sys_exit_fork", l.clone.HandleSysExitFork, true}, + {"sys_enter_vfork", l.clone.HandleSysEnterVfork, true}, + {"sys_exit_vfork", l.clone.HandleSysExitVfork, true}, + } + for _, t := range tracepoints { + if t.prog == nil { + return fmt.Errorf("missing program for %s/%s", syscallsGroup, t.name) + } + lnk, err := link.Tracepoint(syscallsGroup, t.name, t.prog, nil) + if err != nil { + if t.optional && isAttachUnavailable(err) { + log.Warnf("skipping optional %s/%s: %v", syscallsGroup, t.name, err) + continue + } + return fmt.Errorf("attaching %s/%s: %w", syscallsGroup, t.name, err) + } + l.links = append(l.links, lnk) + } + + // Successful clones and process exits are captured from scheduler + // tracepoints. Clone syscall exit tracepoints fire in the parent, and + // exit/exit_group never return, so their exit tracepoints never fire. + tracing := []struct { + name string + prog *ebpf.Program + }{ + {"sched_process_fork", l.clone.HandleSchedProcessFork}, + {"sched_process_exit", l.exit.HandleSchedProcessExit}, + } + for _, t := range tracing { + if t.prog == nil { + return fmt.Errorf("missing %s program", t.name) + } + lnk, err := link.AttachTracing(link.TracingOptions{Program: t.prog}) + if err != nil { + return fmt.Errorf("attaching %s: %w", t.name, err) + } + l.links = append(l.links, lnk) + } + return nil +} + +func isAttachUnavailable(err error) bool { + if err == nil { + return false + } + if errors.Is(err, unix.EPERM) || errors.Is(err, unix.ENOENT) || errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist) { + return true + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "permission denied") || strings.Contains(msg, "no such file") +} + +func (l *loader) runTaskIterator() error { + if l.iter == nil || l.iter.DumpTask == nil { + return fmt.Errorf("missing iter/task program") + } + it, err := link.AttachIter(link.IterOptions{Program: l.iter.DumpTask}) + if err != nil { + return fmt.Errorf("attaching iter/task: %w", err) + } + l.iterLink = it + file, err := it.Open() + if err != nil { + return fmt.Errorf("opening task iterator: %w", err) + } + _, _ = io.Copy(io.Discard, file) + return file.Close() +} + +func (l *loader) Close() error { + var err error + l.once.Do(func() { + for _, lnk := range l.links { + if lnk != nil { + if e := lnk.Close(); e != nil { + err = e + } + } + } + if l.iterLink != nil { + if e := l.iterLink.Close(); e != nil { + err = e + } + } + // Replacement collections share maps owned by execve. Close only their + // programs so the canonical maps are released once. + if l.iter != nil { + if e := l.iter.prociterPrograms.Close(); e != nil { + err = e + } + } + if l.exit != nil { + if e := l.exit.exitPrograms.Close(); e != nil { + err = e + } + } + if l.clone != nil { + if e := l.clone.clonePrograms.Close(); e != nil { + err = e + } + } + if l.exec != nil { + if e := l.exec.Close(); e != nil { + err = e + } + } + log.Debug("eBPF loader closed") + }) + return err +} diff --git a/internal/ebpf/loader_test.go b/internal/ebpf/loader_test.go new file mode 100644 index 000000000..24f52cfe9 --- /dev/null +++ b/internal/ebpf/loader_test.go @@ -0,0 +1,41 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/sys/unix" +) + +func TestIsAttachUnavailable(t *testing.T) { + assert.False(t, isAttachUnavailable(nil)) + assert.False(t, isAttachUnavailable(errors.New("verifier rejected"))) + assert.True(t, isAttachUnavailable(unix.EPERM)) + assert.True(t, isAttachUnavailable(unix.ENOENT)) + assert.True(t, isAttachUnavailable(os.ErrPermission)) + assert.True(t, isAttachUnavailable(fmt.Errorf("attaching syscalls/sys_enter_fork: cannot create bpf perf link: %w", unix.EPERM))) + assert.True(t, isAttachUnavailable(errors.New("cannot create bpf perf link: permission denied"))) +} diff --git a/internal/ebpf/maps.go b/internal/ebpf/maps.go new file mode 100644 index 000000000..353876c85 --- /dev/null +++ b/internal/ebpf/maps.go @@ -0,0 +1,27 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +const ( + eventsMapName = "events" + dropCountMapName = "drop_count" + scratchMapName = "scratch" +) diff --git a/internal/ebpf/proc_iter.go b/internal/ebpf/proc_iter.go new file mode 100644 index 000000000..75bba914c --- /dev/null +++ b/internal/ebpf/proc_iter.go @@ -0,0 +1,64 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "strings" + + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" +) + +func snapshotFromRaw(r rawEvent, exe, cmdline string) *pstypes.PS { + name := r.comm() + if name == "" && exe != "" { + name = baseName(exe) + } + if cmdline == "" { + cmdline = exe + } + args := splitCmdline(cmdline) + return &pstypes.PS{ + PID: uint64(r.TGID), + Ppid: uint64(r.PPID), + Name: name, + Cmdline: cmdline, + Exe: exe, + Args: args, + StartBootTime: r.StartBootTime, + UID: r.UID, + GID: r.GID, + Threads: make(map[uint64]pstypes.Thread), + } +} + +func baseName(path string) string { + if i := strings.LastIndex(path, "/"); i >= 0 && i+1 < len(path) { + return path[i+1:] + } + return path +} + +func splitCmdline(cmdline string) []string { + if cmdline == "" { + return nil + } + return strings.Fields(cmdline) +} diff --git a/internal/ebpf/prociter_bpfeb.go b/internal/ebpf/prociter_bpfeb.go new file mode 100644 index 000000000..4fe1862cf --- /dev/null +++ b/internal/ebpf/prociter_bpfeb.go @@ -0,0 +1,147 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (mips || mips64 || ppc64 || s390x) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type prociterScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadProciter returns the embedded CollectionSpec for prociter. +func loadProciter() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ProciterBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load prociter: %w", err) + } + + return spec, err +} + +// loadProciterObjects loads prociter and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *prociterObjects +// *prociterPrograms +// *prociterMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadProciterObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadProciter() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// prociterSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterSpecs struct { + prociterProgramSpecs + prociterMapSpecs + prociterVariableSpecs +} + +// prociterProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterProgramSpecs struct { + DumpTask *ebpf.ProgramSpec `ebpf:"dump_task"` +} + +// prociterMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// prociterVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterVariableSpecs struct { +} + +// prociterObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterObjects struct { + prociterPrograms + prociterMaps + prociterVariables +} + +func (o *prociterObjects) Close() error { + return _ProciterClose( + &o.prociterPrograms, + &o.prociterMaps, + ) +} + +// prociterMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *prociterMaps) Close() error { + return _ProciterClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// prociterVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterVariables struct { +} + +// prociterPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterPrograms struct { + DumpTask *ebpf.Program `ebpf:"dump_task"` +} + +func (p *prociterPrograms) Close() error { + return _ProciterClose( + p.DumpTask, + ) +} + +func _ProciterClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed prociter_bpfeb.o +var _ProciterBytes []byte diff --git a/internal/ebpf/prociter_bpfeb.o b/internal/ebpf/prociter_bpfeb.o new file mode 100644 index 000000000..772188f36 Binary files /dev/null and b/internal/ebpf/prociter_bpfeb.o differ diff --git a/internal/ebpf/prociter_bpfel.go b/internal/ebpf/prociter_bpfel.go new file mode 100644 index 000000000..7871faf59 --- /dev/null +++ b/internal/ebpf/prociter_bpfel.go @@ -0,0 +1,147 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) && linux + +package ebpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "structs" + + "github.com/cilium/ebpf" +) + +type prociterScratchValue struct { + _ structs.HostLayout + Arg0 uint64 + Arg1 uint64 + Filename [256]uint8 +} + +// loadProciter returns the embedded CollectionSpec for prociter. +func loadProciter() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ProciterBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load prociter: %w", err) + } + + return spec, err +} + +// loadProciterObjects loads prociter and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *prociterObjects +// *prociterPrograms +// *prociterMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadProciterObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadProciter() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// prociterSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterSpecs struct { + prociterProgramSpecs + prociterMapSpecs + prociterVariableSpecs +} + +// prociterProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterProgramSpecs struct { + DumpTask *ebpf.ProgramSpec `ebpf:"dump_task"` +} + +// prociterMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterMapSpecs struct { + DropCount *ebpf.MapSpec `ebpf:"drop_count"` + Events *ebpf.MapSpec `ebpf:"events"` + Scratch *ebpf.MapSpec `ebpf:"scratch"` +} + +// prociterVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type prociterVariableSpecs struct { +} + +// prociterObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterObjects struct { + prociterPrograms + prociterMaps + prociterVariables +} + +func (o *prociterObjects) Close() error { + return _ProciterClose( + &o.prociterPrograms, + &o.prociterMaps, + ) +} + +// prociterMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterMaps struct { + DropCount *ebpf.Map `ebpf:"drop_count"` + Events *ebpf.Map `ebpf:"events"` + Scratch *ebpf.Map `ebpf:"scratch"` +} + +func (m *prociterMaps) Close() error { + return _ProciterClose( + m.DropCount, + m.Events, + m.Scratch, + ) +} + +// prociterVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterVariables struct { +} + +// prociterPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadProciterObjects or ebpf.CollectionSpec.LoadAndAssign. +type prociterPrograms struct { + DumpTask *ebpf.Program `ebpf:"dump_task"` +} + +func (p *prociterPrograms) Close() error { + return _ProciterClose( + p.DumpTask, + ) +} + +func _ProciterClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed prociter_bpfel.o +var _ProciterBytes []byte diff --git a/internal/ebpf/prociter_bpfel.o b/internal/ebpf/prociter_bpfel.o new file mode 100644 index 000000000..1be845f7d Binary files /dev/null and b/internal/ebpf/prociter_bpfel.o differ diff --git a/internal/ebpf/ringbuf.go b/internal/ebpf/ringbuf.go new file mode 100644 index 000000000..fd9b0d488 --- /dev/null +++ b/internal/ebpf/ringbuf.go @@ -0,0 +1,60 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "errors" + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/ringbuf" +) + +type ringReader struct { + rd *ringbuf.Reader +} + +func newRingReader(m *ebpf.Map) (*ringReader, error) { + rd, err := ringbuf.NewReader(m) + if err != nil { + return nil, fmt.Errorf("opening ring buffer: %w", err) + } + return &ringReader{rd: rd}, nil +} + +func (r *ringReader) Read() ([]byte, error) { + record, err := r.rd.Read() + if err != nil { + return nil, err + } + return record.RawSample, nil +} + +func (r *ringReader) Close() error { + if r == nil || r.rd == nil { + return nil + } + return r.rd.Close() +} + +func isRingClosed(err error) bool { + return errors.Is(err, ringbuf.ErrClosed) +} diff --git a/internal/ebpf/source.go b/internal/ebpf/source.go new file mode 100644 index 000000000..e335732cd --- /dev/null +++ b/internal/ebpf/source.go @@ -0,0 +1,310 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/source" + log "github.com/sirupsen/logrus" +) + +const ( + defaultPendingCap = 4096 + startupDrain = 250 * time.Millisecond +) + +var _ source.EventSource = (*EventSource)(nil) + +// EventSource captures Linux process events through eBPF. +type EventSource struct { + psnap ps.Snapshotter + config *config.Config + sequencer *event.Sequencer + q *event.Queue + evts chan *event.Event + errs chan error + + filter filter.Filter + listeners []event.Listener + + loader *loader + reader *ringReader + + pending []*event.Event + pendingMu sync.Mutex + pendingCap int + live atomic.Bool + + stop chan struct{} + done chan struct{} + once sync.Once +} + +// NewEventSource constructs the Linux eBPF event source. +func NewEventSource(psnap ps.Snapshotter, cfg *config.Config, _ *config.RulesCompileResult) source.EventSource { + // Startup replay pushes queued events before the aggregator starts + // consuming, so the channel must be able to absorb a full pending queue. + evts := make(chan *event.Event, defaultPendingCap) + return &EventSource{ + psnap: psnap, + config: cfg, + sequencer: event.NewSequencer(), + evts: evts, + q: event.NewQueueWithChannel(evts, false, cfg.ForwardMode), + errs: make(chan error, 256), + listeners: make([]event.Listener, 0), + pendingCap: defaultPendingCap, + stop: make(chan struct{}), + done: make(chan struct{}), + } +} + +func (e *EventSource) Open(cfg *config.Config) error { + if cfg != nil { + e.config = cfg + } + if e.config == nil { + return fmt.Errorf("missing configuration") + } + + report, err := checkRuntimeSupport() + if err != nil { + if report != nil { + return fmt.Errorf("eBPF prerequisites: %w", err) + } + return err + } + log.Infof("eBPF prerequisites ok: kernel=%s btf=%s ringbuf=%v iter=%v", + report.KernelRelease, report.BTFPath, report.RingbufOK, report.IterOK) + + ldr, err := loadCollections() + if err != nil { + return err + } + e.loader = ldr + + rd, err := newRingReader(ldr.eventsMap()) + if err != nil { + _ = ldr.Close() + return err + } + e.reader = rd + + go e.consume() + + if err := ldr.attachPrograms(); err != nil { + e.Close() + return err + } + + if err := ldr.runTaskIterator(); err != nil { + e.Close() + return err + } + + time.Sleep(startupDrain) + e.finishBaseline() + ringbufDrops.Add(int64(ldr.dropCount())) + log.Infof("eBPF process source is live; %s", e.startupSummary()) + return nil +} + +func (e *EventSource) Close() error { + var err error + e.once.Do(func() { + close(e.stop) + started := e.reader != nil + if e.reader != nil { + _ = e.reader.Close() + } + if e.loader != nil { + err = e.loader.Close() + } + if started { + <-e.done + } + if e.sequencer != nil { + _ = e.sequencer.Shutdown() + } + if e.q != nil { + e.q.Close() + } + }) + return err +} + +func (e *EventSource) Errors() <-chan error { return e.errs } + +func (e *EventSource) Events() <-chan *event.Event { return e.q.Events() } + +func (e *EventSource) SetFilter(f filter.Filter) { e.filter = f } + +func (e *EventSource) RegisterEventListener(lis event.Listener) { + e.listeners = append(e.listeners, lis) + if e.q != nil { + e.q.RegisterListener(lis) + } +} + +func (e *EventSource) consume() { + defer close(e.done) + for { + raw, err := e.reader.Read() + if err != nil { + if isRingClosed(err) { + return + } + select { + case <-e.stop: + return + case e.errs <- err: + default: + } + continue + } + e.handleRecord(raw) + } +} + +func (e *EventSource) handleRecord(raw []byte) { + rec, err := decodeRawEvent(raw) + if err != nil { + parseErrors.Add(1) + return + } + if rec.Type == snapshotType { + e.handleSnapshot(rec) + return + } + typ := rec.eventType() + if typ == event.UnknownType { + eventsUnknown.Add(1) + return + } + if e.config != nil && !e.config.EventSource.EventExists(typ.ID()) { + eventsUnknown.Add(1) + return + } + + evt := rec.toEvent() + if typ == event.Execve || typ == event.Clone { + enrichEvent(evt) + } + + if !e.live.Load() && e.enqueuePending(evt) { + return + } + e.dispatch(evt) +} + +func (e *EventSource) handleSnapshot(rec rawEvent) { + if e.live.Load() { + lateSnapshots.Add(1) + return + } + exe, cmdline, err := enrichFromProc(uint64(rec.TGID)) + if err != nil { + enrichmentMiss.Add(1) + } + upsertSnapshot(e.psnap, snapshotFromRaw(rec, exe, cmdline)) +} + +func (e *EventSource) enqueuePending(evt *event.Event) bool { + e.pendingMu.Lock() + defer e.pendingMu.Unlock() + if e.live.Load() { + return false + } + if len(e.pending) >= e.pendingCap { + pendingDropped.Add(1) + return false + } + e.pending = append(e.pending, evt) + pendingQueued.Add(1) + return true +} + +// finishBaseline replays queued hot events in ring-buffer order and then +// switches to live dispatch. New records keep landing in the pending queue +// while a replay round runs, so loop until the queue drains before flipping +// live. This guarantees replayed and live events preserve global ring-buffer +// order. +func (e *EventSource) finishBaseline() { + for { + e.pendingMu.Lock() + pending := e.pending + e.pending = nil + if len(pending) == 0 { + e.live.Store(true) + e.pendingMu.Unlock() + return + } + e.pendingMu.Unlock() + for _, evt := range pending { + e.dispatch(evt) + replayApplied.Add(1) + } + } +} + +func (e *EventSource) dispatch(evt *event.Event) { + evt.Seq = e.sequencer.Get() + applyProcessState(e.psnap, evt) + eventsProcessed.Add(1) + + if e.config != nil { + if e.config.EventSource.ExcludeEvent(evt.Type.ID()) { + eventsExcluded.Add(1) + return + } + if e.config.EventSource.ExcludeImage(evt.PS) { + eventsExcluded.Add(1) + return + } + if evt.IsDropped(e.config.IsCaptureSet()) { + eventsExcluded.Add(1) + return + } + } + if e.filter != nil && !e.filter.Eval(evt) { + return + } + e.sequencer.Increment() + if err := e.q.Push(evt); err != nil { + select { + case e.errs <- err: + default: + } + } +} + +func (e *EventSource) startupSummary() string { + return fmt.Sprintf("pending_queued=%d pending_dropped=%d replay_applied=%d snapshot_upserts=%d", + pendingQueued.Value(), pendingDropped.Value(), replayApplied.Value(), snapshotUpserts.Value()) +} diff --git a/internal/ebpf/source_integration_test.go b/internal/ebpf/source_integration_test.go new file mode 100644 index 000000000..a33c43f56 --- /dev/null +++ b/internal/ebpf/source_integration_test.go @@ -0,0 +1,83 @@ +//go:build linux && ebpf_integration + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "os" + "testing" + "time" + + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/stretchr/testify/require" +) + +func TestLiveProcessSource(t *testing.T) { + cfg := testConfig() + es := NewEventSource(ps.NewSnapshotter(), cfg, nil).(*EventSource) + if err := es.Open(cfg); err != nil { + t.Fatalf("opening process source: %v", err) + } + t.Cleanup(func() { _ = es.Close() }) + + cmd := execLookPath() + p, err := os.StartProcess(cmd, []string{cmd}, &os.ProcAttr{ + Files: []*os.File{nil, nil, nil}, + }) + require.NoError(t, err) + _, _ = p.Wait() + + deadline := time.Now().Add(5 * time.Second) + var sawExec, sawExit bool + for time.Now().Before(deadline) && (!sawExec || !sawExit) { + select { + case evt := <-es.Events(): + switch evt.Type { + case event.Execve: + if evt.PID == uint64(p.Pid) { + sawExec = true + } + case event.Exit: + if evt.PID == uint64(p.Pid) { + sawExit = true + } + } + case err := <-es.Errors(): + t.Fatalf("event source error: %v", err) + case <-time.After(50 * time.Millisecond): + } + } + if !sawExec { + t.Fatal("did not observe execve for spawned process") + } + if !sawExit { + t.Fatal("did not observe exit for spawned process") + } +} + +func execLookPath() string { + for _, p := range []string{"/bin/true", "/usr/bin/true"} { + if _, err := os.Stat(p); err == nil { + return p + } + } + return "/bin/sh" +} diff --git a/internal/ebpf/source_test.go b/internal/ebpf/source_test.go new file mode 100644 index 000000000..c57fbaf18 --- /dev/null +++ b/internal/ebpf/source_test.go @@ -0,0 +1,117 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ebpf + +import ( + "testing" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testConfig() *config.Config { + cfg := &config.Config{ForwardMode: true} + cfg.EventSource.Init() + return cfg +} + +func TestStartupReplayAndLateSnapshot(t *testing.T) { + cfg := testConfig() + es := NewEventSource(ps.NewSnapshotter(), cfg, nil).(*EventSource) + + hot := sampleExecve() + hot.PID = 42 + hot.TGID = 42 + hot.StartBootTime = 100 + hot.Comm = [16]byte{} + hot.Filename = [256]byte{} + copy(hot.Comm[:], "hot-a") + copy(hot.Filename[:], "/bin/hot-a") + es.handleRecord(encodeRaw(t, hot)) + require.Len(t, es.pending, 1) + ok, _ := es.psnap.Find(42) + assert.False(t, ok) + + snap := hot + snap.Type = snapshotType + snap.Comm = [16]byte{} + copy(snap.Comm[:], "snap-a") + es.handleSnapshot(snap) + ok, got := es.psnap.Find(42) + require.True(t, ok) + assert.Equal(t, "snap-a", got.Name) + + es.finishBaseline() + assert.True(t, es.live.Load()) + assert.Nil(t, es.pending) + + select { + case evt := <-es.Events(): + assert.Equal(t, event.Execve, evt.Type) + assert.Equal(t, uint64(42), evt.PID) + require.NotNil(t, evt.PS) + assert.Equal(t, "hot-a", evt.PS.Name) + assert.Equal(t, uint64(100), evt.PS.StartBootTime) + default: + t.Fatal("expected replayed execve on the output queue") + } + + late := snap + late.Comm = [16]byte{} + copy(late.Comm[:], "stale") + es.handleSnapshot(late) + ok, got = es.psnap.Find(42) + require.True(t, ok) + assert.Equal(t, "hot-a", got.Name) +} + +func TestPIDReuseKeepsDistinctUUIDs(t *testing.T) { + cfg := testConfig() + es := NewEventSource(ps.NewSnapshotter(), cfg, nil).(*EventSource) + es.live.Store(true) + + first := sampleExecve() + first.PID = 8 + first.TGID = 8 + first.StartBootTime = 1 + first.Comm = [16]byte{} + copy(first.Comm[:], "one") + es.handleRecord(encodeRaw(t, first)) + + exit := rawEvent{Type: uint32(event.Exit), PID: 8, TGID: 8, StartBootTime: 1} + es.handleRecord(encodeRaw(t, exit)) + + second := first + second.StartBootTime = 2 + second.Comm = [16]byte{} + second.Filename = [256]byte{} + copy(second.Comm[:], "two") + copy(second.Filename[:], "/bin/two") + es.handleRecord(encodeRaw(t, second)) + + ok, got := es.psnap.Find(8) + require.True(t, ok) + assert.Equal(t, "two", got.Name) + assert.Equal(t, uint64(2), got.StartBootTime) +} diff --git a/internal/ebpf/spike/c/vmlinux.h b/internal/ebpf/spike/c/vmlinux.h index dc774a68b..da94af6b5 100644 --- a/internal/ebpf/spike/c/vmlinux.h +++ b/internal/ebpf/spike/c/vmlinux.h @@ -124,6 +124,7 @@ struct cgroup_namespace { struct task_struct { int pid; int tgid; + int exit_code; struct task_struct *real_parent; struct task_struct *group_leader; const struct cred *real_cred; diff --git a/internal/ebpf/testdata/execve.json b/internal/ebpf/testdata/execve.json new file mode 100644 index 000000000..f676d77b6 --- /dev/null +++ b/internal/ebpf/testdata/execve.json @@ -0,0 +1,19 @@ +{ + "name": "execve", + "pid": 4242, + "tid": 4242, + "seq": 7, + "params": { + "cmdline": "/bin/bash", + "exe": "/bin/bash", + "gid": "1000", + "name": "bash", + "pid": "4242", + "ppid": "1", + "retval": "0", + "start_boot_time": "123456789", + "syscall_id": "59", + "tid": "4242", + "uid": "1000" + } +} diff --git a/pkg/api/server_linux.go b/pkg/api/server_linux.go new file mode 100644 index 000000000..72dcd0db1 --- /dev/null +++ b/pkg/api/server_linux.go @@ -0,0 +1,62 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import ( + "fmt" + "net" + "os" + "strings" + + "github.com/rabbitstack/fibratus/pkg/config" +) + +var listener net.Listener + +// StartServer starts the HTTP server with the specified configuration. +func StartServer(c *config.Config) error { + var err error + transport := c.API.Transport + if strings.HasPrefix(transport, "unix://") { + path := strings.TrimPrefix(transport, "unix://") + _ = os.Remove(path) + listener, err = net.Listen("unix", path) + if err != nil { + return fmt.Errorf("fail to listen on the %q socket: %w", path, err) + } + } else { + listener, err = net.Listen("tcp", transport) + if err != nil { + return fmt.Errorf("fail to listen on %q: %w", transport, err) + } + } + + setupServer(listener, c) + return nil +} + +// CloseServer shutdowns the server by stopping the listener. +func CloseServer() error { + if listener != nil { + return listener.Close() + } + return nil +} diff --git a/pkg/event/event_linux.go b/pkg/event/event_linux.go index 52d873418..a2113821e 100644 --- a/pkg/event/event_linux.go +++ b/pkg/event/event_linux.go @@ -20,7 +20,11 @@ package event -import "github.com/rabbitstack/fibratus/pkg/event/params" +import ( + "os" + + "github.com/rabbitstack/fibratus/pkg/event/params" +) const cloneThread = uint64(0x00010000) @@ -41,3 +45,19 @@ func (e *Event) IsTerminateProcess() bool { return e.Type == Exit } func (e *Event) PartialKey() uint64 { return uint64(e.Type)<<32 | uint64(e.PID) } + +// DropCurrentProc determines if events generated by the Fibratus process are dropped. +var DropCurrentProc = true + +var currentPid = uint64(os.Getpid()) + +// IsDropped reports whether the event should be omitted from the output queue. +func (e *Event) IsDropped(_ bool) bool { + return DropCurrentProc && e.PID == currentPid +} + +// IsStackWalk reports whether the event is a stack walk record. +func (e *Event) IsStackWalk() bool { return false } + +// IsState reports whether the event is used only for state management. +func (e *Event) IsState() bool { return false } diff --git a/pkg/event/params/names_linux.go b/pkg/event/params/names_linux.go index 4d5acd049..edf79f1f0 100644 --- a/pkg/event/params/names_linux.go +++ b/pkg/event/params/names_linux.go @@ -27,4 +27,10 @@ const ( UID = "uid" // GID is the effective Linux group identifier. GID = "gid" + // SyscallID is the raw architecture-specific syscall number. + SyscallID = "syscall_id" + // Retval is the syscall return value. + Retval = "retval" + // StartBootTime is the process start time measured from system boot, in nanoseconds. + StartBootTime = "start_boot_time" ) diff --git a/pkg/event/sequencer_linux.go b/pkg/event/sequencer_linux.go new file mode 100644 index 000000000..33ea66a38 --- /dev/null +++ b/pkg/event/sequencer_linux.go @@ -0,0 +1,55 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package event + +import "sync/atomic" + +// Sequencer increments the event sequence number. +type Sequencer struct { + seq uint64 +} + +// NewSequencer creates an in-memory event sequencer. +func NewSequencer() *Sequencer { + return &Sequencer{} +} + +// Increment increments the sequence number atomically. +func (s *Sequencer) Increment() { + atomic.AddUint64(&s.seq, 1) +} + +// Get returns the current sequence number. +func (s *Sequencer) Get() uint64 { + return atomic.LoadUint64(&s.seq) +} + +// Reset sets the sequence number to zero. +func (s *Sequencer) Reset() error { + atomic.StoreUint64(&s.seq, 0) + return nil +} + +// Close is a no-op on Linux. +func (s *Sequencer) Close() error { return nil } + +// Shutdown closes the sequencer. +func (s *Sequencer) Shutdown() error { return s.Close() } diff --git a/pkg/event/sequencer_linux_test.go b/pkg/event/sequencer_linux_test.go new file mode 100644 index 000000000..1fec3500e --- /dev/null +++ b/pkg/event/sequencer_linux_test.go @@ -0,0 +1,43 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package event + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSequencer(t *testing.T) { + sequencer := NewSequencer() + defer sequencer.Close() + + assert.Equal(t, uint64(0), sequencer.Get()) + for i := 0; i < 10; i++ { + sequencer.Increment() + } + assert.Equal(t, uint64(10), sequencer.Get()) + + require.NoError(t, sequencer.Reset()) + assert.Equal(t, uint64(0), sequencer.Get()) + require.NoError(t, sequencer.Shutdown()) +} diff --git a/pkg/util/signals/signals_linux.go b/pkg/util/signals/signals_linux.go new file mode 100644 index 000000000..f202280ba --- /dev/null +++ b/pkg/util/signals/signals_linux.go @@ -0,0 +1,47 @@ +//go:build linux + +/* + * Copyright 2026 by Mostafa Moradian + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package signals + +import ( + "os" + "os/signal" + "syscall" + + log "github.com/sirupsen/logrus" +) + +// Install setups the signal handler. Returns a blocking +// channel which receives an input after Interrupt or Term +// signals are triggered. +func Install() chan struct{} { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + + stopCh := make(chan struct{}) + + go func() { + sig := <-sigCh + log.Infof("got signal %q, shutting down...", sig) + stopCh <- struct{}{} + }() + + return stopCh +} diff --git a/pkg/util/signals/signals.go b/pkg/util/signals/signals_windows.go similarity index 98% rename from pkg/util/signals/signals.go rename to pkg/util/signals/signals_windows.go index d7670af07..d4d69a7f4 100644 --- a/pkg/util/signals/signals.go +++ b/pkg/util/signals/signals_windows.go @@ -1,3 +1,5 @@ +//go:build windows + /* * Copyright 2021-2022 by Nedim Sabic Sabic * https://www.fibratus.io