Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Stack traces #4347

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 46 additions & 24 deletions pkg/containers/path_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/aquasecurity/tracee/pkg/bucketscache"
"github.com/aquasecurity/tracee/pkg/errfmt"
"github.com/aquasecurity/tracee/pkg/logger"
"github.com/aquasecurity/tracee/pkg/utils/proc"
)

// ContainerPathResolver generates an accessible absolute path from the root
Expand Down Expand Up @@ -45,36 +46,57 @@ func (cPathRes *ContainerPathResolver) GetHostAbsPath(mountNSAbsolutePath string
pids := cPathRes.mountNSPIDsCache.GetBucket(uint32(mountNS))

for _, pid := range pids {
// cap.SYS_PTRACE is needed here. Instead of raising privileges, since
// this is called too frequently, if the needed event is being traced,
// the needed capabilities are added to the Base ring and are always set
// as effective.
//
// (Note: To change this behavior we need a privileged process/server)

procRootPath := fmt.Sprintf("/proc/%d/root", int(pid))

// fs.FS interface requires relative paths, so the '/' prefix should be trimmed.
entries, err := fs.ReadDir(cPathRes.fs, strings.TrimPrefix(procRootPath, "/"))
procRoot, err := cPathRes.getProcessFSRoot(uint(pid))
if err != nil {
// This process is either not alive or we don't have permissions to access.
// Try next pid in mount ns to find accessible path to mount ns files.
logger.Debugw(
"Finding mount NS path",
"Unreachable proc root path", procRootPath,
"error", err.Error(),
)
logger.Debugw("Could not access process FS", "pid", pid, "error", err)
continue
}
if len(entries) == 0 {
return "", errfmt.Errorf("empty directory")
}
if err == nil {
return fmt.Sprintf("%s%s", procRootPath, mountNSAbsolutePath), nil
}

return fmt.Sprintf("%s%s", procRoot, mountNSAbsolutePath), nil
}

// No PIDs registered in this namespace, or couldn't access FS root of any of the PIDs found.
// Try finding one in procfs.
pid, err := proc.GetAnyProcessInNS("mnt", mountNS)
if err != nil {
// Couldn't find a process in this namespace using procfs
return "", ErrContainerFSUnreachable
}

procRoot, err := cPathRes.getProcessFSRoot(pid)
if err != nil {
logger.Debugw("Could not access process FS", "pid", pid, "error", err)
return "", ErrContainerFSUnreachable
}

// Register this process in the mount namespace
cPathRes.mountNSPIDsCache.AddBucketItem(uint32(mountNS), uint32(pid))

return fmt.Sprintf("%s%s", procRoot, mountNSAbsolutePath), nil
}

func (cPathRes *ContainerPathResolver) getProcessFSRoot(pid uint) (string, error) {
// cap.SYS_PTRACE is needed here. Instead of raising privileges, since
// this is called too frequently, if the needed event is being traced,
// the needed capabilities are added to the Base ring and are always set
// as effective.
//
// (Note: To change this behavior we need a privileged process/server)

procRootPath := fmt.Sprintf("/proc/%d/root", pid)

// fs.FS interface requires relative paths, so the '/' prefix should be trimmed.
entries, err := fs.ReadDir(cPathRes.fs, strings.TrimPrefix(procRootPath, "/"))
if err != nil {
// This process is either not alive or we don't have permissions to access.
return "", errfmt.Errorf("failed accessing process FS root %s: %v", procRootPath, err)
}
if len(entries) == 0 {
return "", errfmt.Errorf("process FS root (%s) is empty", procRootPath)
}

return "", ErrContainerFSUnreachable
return procRootPath, nil
}

var (
Expand Down
8 changes: 0 additions & 8 deletions pkg/ebpf/c/common/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -467,14 +467,6 @@ statfunc int events_perf_submit(program_data_t *p, long ret)
// keep task_info updated
bpf_probe_read_kernel(&p->task_info->context, sizeof(task_context_t), &p->event->context.task);

// Get Stack trace
if (p->config->options & OPT_CAPTURE_STACK_TRACES) {
int stack_id = bpf_get_stackid(p->ctx, &stack_addresses, BPF_F_USER_STACK);
if (stack_id >= 0) {
p->event->context.stack_id = stack_id;
}
}

u32 size = sizeof(event_context_t) + sizeof(u8) +
p->event->args_buf.offset; // context + argnum + arg buffer size

Expand Down
12 changes: 0 additions & 12 deletions pkg/ebpf/c/maps.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,18 +265,6 @@ struct sys_exit_init_tail {

typedef struct sys_exit_init_tail sys_exit_init_tail_t;

// store stack traces
#define MAX_STACK_ADDRESSES 1024 // max amount of diff stack trace addrs to buffer

struct stack_addresses {
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
__uint(max_entries, MAX_STACK_ADDRESSES);
__type(key, u32);
__type(value, stack_trace_t); // 1 big byte array of the stack addresses
} stack_addresses SEC(".maps");

typedef struct stack_addresses stack_addresses_t;

// store fds paths by timestamp
struct fd_arg_path_map {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
Expand Down
13 changes: 7 additions & 6 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -2139,17 +2139,18 @@ int BPF_KPROBE(trace_security_bprm_check)
unsigned long inode_nr = get_inode_nr_from_file(file);
void *file_path = get_path_str(__builtin_preserve_access_index(&file->f_path));

syscall_data_t *sys = &p.task_info->syscall_data;
struct pt_regs *task_regs = get_current_task_pt_regs();

const char *const *argv = NULL;
const char *const *envp = NULL;
switch (sys->id) {
switch (get_current_task_syscall_id()) {
case SYSCALL_EXECVE:
argv = (const char *const *) sys->args.args[1];
envp = (const char *const *) sys->args.args[2];
argv = (const char *const *) get_syscall_arg2(p.event->task, task_regs, false);
envp = (const char *const *) get_syscall_arg3(p.event->task, task_regs, false);
break;
case SYSCALL_EXECVEAT:
argv = (const char *const *) sys->args.args[2];
envp = (const char *const *) sys->args.args[3];
argv = (const char *const *) get_syscall_arg3(p.event->task, task_regs, false);
envp = (const char *const *) get_syscall_arg4(p.event->task, task_regs, false);
break;
default:
break;
Expand Down
51 changes: 51 additions & 0 deletions pkg/ebpf/event_filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package ebpf

import (
"fmt"

"github.com/aquasecurity/tracee/pkg/events"
"github.com/aquasecurity/tracee/pkg/filters"
"github.com/aquasecurity/tracee/pkg/logger"
)

type eventFilterHandler func(t *Tracee, eventFilters []map[string]filters.Filter[*filters.StringFilter]) error

var eventFilterHandlers = map[events.ID]eventFilterHandler{}

// handleEventFilters performs eBPF related actions according to event filters.
// For example, an event can use one of its filters to populate eBPF maps, or perhaps
// attach eBPF programs according to the filters.
func (t *Tracee) handleEventFilters() error {
// Iterate through registerd event filter handlers
for eventID, handler := range eventFilterHandlers {
// Make sure this event is selected
if _, err := t.eventsDependencies.GetEvent(eventID); err != nil {
continue
}

// Construct filters for this event
eventFilters := make([]map[string]filters.Filter[*filters.StringFilter], 0)
for iterator := t.policyManager.CreateAllIterator(); iterator.HasNext(); {
policy := iterator.Next()
policyFilters := policy.DataFilter.GetEventFilters(eventID)
if len(policyFilters) == 0 {
continue
}
eventFilters = append(eventFilters, policyFilters)
}
if len(eventFilters) == 0 {
// No filters for this event
continue
}

// Call handler
if err := handler(t, eventFilters); err != nil {
if err := t.eventsDependencies.RemoveEvent(eventID); err != nil {
logger.Warnw("Failed to remove event from dependencies manager", "remove reason", "failed handling event filters", "error", err)
}
return fmt.Errorf("failed to handle filters for event %s: %v", events.Core.GetDefinitionByID(eventID).GetName(), err)
}
}

return nil
}
56 changes: 1 addition & 55 deletions pkg/ebpf/events_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@ package ebpf
import (
"bytes"
"context"
"encoding/binary"
"slices"
"strconv"
"sync"
"unsafe"

"github.com/aquasecurity/tracee/pkg/bufferdecoder"
"github.com/aquasecurity/tracee/pkg/capabilities"
"github.com/aquasecurity/tracee/pkg/errfmt"
"github.com/aquasecurity/tracee/pkg/events"
"github.com/aquasecurity/tracee/pkg/logger"
Expand Down Expand Up @@ -191,12 +187,6 @@ func (t *Tracee) decodeEvents(ctx context.Context, sourceChan chan []byte) (<-ch
continue
}

// Add stack trace if needed
var stackAddresses []uint64
if t.config.Output.StackAddresses {
stackAddresses = t.getStackAddresses(eCtx.StackID)
}

containerInfo := t.containers.GetCgroupInfo(eCtx.CgroupID).Container
containerData := trace.Container{
ID: containerInfo.ContainerId,
Expand Down Expand Up @@ -262,7 +252,7 @@ func (t *Tracee) decodeEvents(ctx context.Context, sourceChan chan []byte) (<-ch
evt.ArgsNum = int(argnum)
evt.ReturnValue = int(eCtx.Retval)
evt.Args = args
evt.StackAddresses = stackAddresses
evt.StackAddresses = nil
evt.ContextFlags = flags
evt.Syscall = syscall
evt.Metadata = nil
Expand Down Expand Up @@ -641,50 +631,6 @@ func (t *Tracee) sinkEvents(ctx context.Context, in <-chan *trace.Event) <-chan
return errc
}

// getStackAddresses returns the stack addresses for a given StackID
func (t *Tracee) getStackAddresses(stackID uint32) []uint64 {
stackAddresses := make([]uint64, maxStackDepth)
stackFrameSize := (strconv.IntSize / 8)

// Lookup the StackID in the map
// The ID could have aged out of the Map, as it only holds a finite number of
// Stack IDs in it's Map
var stackBytes []byte
err := capabilities.GetInstance().EBPF(func() error {
bytes, e := t.StackAddressesMap.GetValue(unsafe.Pointer(&stackID))
if e != nil {
stackBytes = bytes
}
return e
})
if err != nil {
logger.Debugw("failed to get StackAddress", "error", err)
return stackAddresses[0:0]
}

stackCounter := 0
for i := 0; i < len(stackBytes); i += stackFrameSize {
stackAddresses[stackCounter] = 0
stackAddr := binary.LittleEndian.Uint64(stackBytes[i : i+stackFrameSize])
if stackAddr == 0 {
break
}
stackAddresses[stackCounter] = stackAddr
stackCounter++
}

// Attempt to remove the ID from the map so we don't fill it up
// But if this fails continue on
err = capabilities.GetInstance().EBPF(func() error {
return t.StackAddressesMap.DeleteKey(unsafe.Pointer(&stackID))
})
if err != nil {
logger.Debugw("failed to delete stack address from eBPF map", "error", err)
}

return stackAddresses[0:stackCounter]
}

// WaitForPipeline waits for results from all error channels.
func (t *Tracee) WaitForPipeline(errs ...<-chan error) error {
errc := MergeErrors(errs...)
Expand Down
3 changes: 2 additions & 1 deletion pkg/ebpf/processor_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func processKernelReadFile(event *trace.Event) error {
// processSchedProcessExec processes a sched_process_exec event by capturing the executed file.
func (t *Tracee) processSchedProcessExec(event *trace.Event) error {
// cache this pid by it's mnt ns
// TODO: don't do this if Tracee is not in the root PID NS?
if event.ProcessID == 1 {
t.pidsInMntns.ForceAddBucketItem(uint32(event.MountNS), uint32(event.HostProcessID))
} else {
Expand Down Expand Up @@ -467,7 +468,7 @@ func (t *Tracee) removeContext(event *trace.Event) error {
event.Container = trace.Container{}
event.Kubernetes = trace.Kubernetes{}
event.Syscall = ""
event.StackAddresses = []uint64{}
event.StackAddresses = nil
event.ContextFlags = trace.ContextFlags{}
event.ThreadEntityId = 0
event.ProcessEntityId = 0
Expand Down
22 changes: 11 additions & 11 deletions pkg/ebpf/tracee.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ type Tracee struct {
bpfModule *bpf.Module
probes *probes.ProbeGroup
// BPF Maps
StackAddressesMap *bpf.BPFMap
FDArgPathMap *bpf.BPFMap
FDArgPathMap *bpf.BPFMap
// Perf Buffers
eventsPerfMap *bpf.PerfBuffer // perf buffer for events
fileWrPerfMap *bpf.PerfBuffer // perf buffer for file writes
Expand Down Expand Up @@ -483,15 +482,6 @@ func (t *Tracee) Init(ctx gocontext.Context) error {
return errfmt.Errorf("error initializing network capture: %v", err)
}

// Get reference to stack trace addresses map

stackAddressesMap, err := t.bpfModule.GetMap("stack_addresses")
if err != nil {
t.Close()
return errfmt.Errorf("error getting access to 'stack_addresses' eBPF Map %v", err)
}
t.StackAddressesMap = stackAddressesMap

// Get reference to fd arg path map

fdArgPathMap, err := t.bpfModule.GetMap("fd_arg_path_map")
Expand All @@ -518,6 +508,16 @@ func (t *Tracee) Init(ctx gocontext.Context) error {
},
}

// Perform extra initializtion steps required by specific events according to their arguments
err = capabilities.GetInstance().EBPF(
func() error {
return t.handleEventFilters()
},
)
if err != nil {
return errfmt.WrapError(err)
}

return nil
}

Expand Down
10 changes: 0 additions & 10 deletions pkg/events/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -11447,16 +11447,6 @@ var CoreEvents = map[ID]Definition{
dependencies: Dependencies{
probes: []Probe{
{handle: probes.SecurityBPRMCheck, required: true},
{handle: probes.SyscallEnter__Internal, required: true},
},
tailCalls: []TailCall{
{
"sys_enter_init_tail",
"sys_enter_init",
[]uint32{
uint32(Execve), uint32(Execveat),
},
},
},
},
sets: []string{"lsm_hooks", "proc", "proc_life"},
Expand Down
28 changes: 28 additions & 0 deletions pkg/utils/proc/ns.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,31 @@ func extractNSFromLink(link string) (int, error) {
}
return ns, nil
}

// GetAnyProcessInNS returns the PID of any process in the given namespace type and number.
// It returns the first process it finds when iterating over /proc that satisfies the request.
// To do so, it requires access to the /proc file system, and CAP_SYS_PTRACE capability.
func GetAnyProcessInNS(nsName string, nsNum int) (uint, error) {
entries, err := os.ReadDir("/proc")
if err != nil {
return 0, errfmt.Errorf("could not read proc dir: %v", err)
}

for _, entry := range entries {
pid, err := strconv.ParseUint(entry.Name(), 10, 32)
if err != nil {
// Not a PID directory
continue
}
ns, err := GetProcNS(uint(pid), nsName)
if err != nil {
logger.Infow("Failed fetching process namespace", "pid", pid, "namespace", nsName, "error", err)
continue
}
if ns == nsNum {
return uint(pid), nil
}
}

return 0, errfmt.Errorf("could not find any process in %s namesapce %d", nsName, nsNum)
}
Loading