mirror of
https://github.com/imjasonh/snoop
synced 2026-07-15 20:57:08 +00:00
Add remaining syscall tracepoints for file access tracking
Trace statx, newfstatat, faccessat, faccessat2, readlinkat, openat2, and execveat. Signed-off-by: Jason Hall <jason@chainguard.dev>
This commit is contained in:
parent
f793a98161
commit
23d94e3049
4 changed files with 243 additions and 32 deletions
|
|
@ -131,11 +131,9 @@
|
|||
- [x] Files created list
|
||||
- [x] Current status description
|
||||
|
||||
## What's NOT Done (Expected) ⏳
|
||||
## Milestone 2 Progress ⏳
|
||||
|
||||
These are intentionally deferred to Milestone 2:
|
||||
|
||||
- [ ] Additional syscalls (stat, access, readlink)
|
||||
- [x] Additional syscalls (statx, newfstatat, faccessat, faccessat2, readlinkat, openat2, execveat)
|
||||
- [ ] Path normalization logic
|
||||
- [ ] Deduplication data structures
|
||||
- [ ] JSON report output
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ int trace_openat(struct trace_event_raw_sys_enter *ctx) {
|
|||
}
|
||||
|
||||
// Tracepoint for execve syscall
|
||||
// execve(const char *pathname, char *const argv[], char *const envp[])
|
||||
SEC("tracepoint/syscalls/sys_enter_execve")
|
||||
int trace_execve(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
|
|
@ -106,4 +107,193 @@ int trace_execve(struct trace_event_raw_sys_enter *ctx) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Tracepoint for execveat syscall
|
||||
// execveat(int dirfd, const char *pathname, char *const argv[], char *const envp[], int flags)
|
||||
SEC("tracepoint/syscalls/sys_enter_execveat")
|
||||
int trace_execveat(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 zero = 0;
|
||||
struct event *e = bpf_map_lookup_elem(&heap, &zero);
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
e->cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
e->syscall_nr = ctx->id;
|
||||
|
||||
const char *pathname = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user_str(&e->path, MAX_PATH_LEN, pathname);
|
||||
|
||||
bpf_ringbuf_output(&events, e, sizeof(*e), 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Tracepoint for openat2 syscall (kernel 5.6+)
|
||||
// openat2(int dirfd, const char *pathname, struct open_how *how, size_t size)
|
||||
SEC("tracepoint/syscalls/sys_enter_openat2")
|
||||
int trace_openat2(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 zero = 0;
|
||||
struct event *e = bpf_map_lookup_elem(&heap, &zero);
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
e->cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
e->syscall_nr = ctx->id;
|
||||
|
||||
const char *pathname = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user_str(&e->path, MAX_PATH_LEN, pathname);
|
||||
|
||||
bpf_ringbuf_output(&events, e, sizeof(*e), 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Tracepoint for statx syscall (kernel 4.11+)
|
||||
// statx(int dirfd, const char *pathname, int flags, unsigned int mask, struct statx *statxbuf)
|
||||
SEC("tracepoint/syscalls/sys_enter_statx")
|
||||
int trace_statx(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 zero = 0;
|
||||
struct event *e = bpf_map_lookup_elem(&heap, &zero);
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
e->cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
e->syscall_nr = ctx->id;
|
||||
|
||||
const char *pathname = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user_str(&e->path, MAX_PATH_LEN, pathname);
|
||||
|
||||
bpf_ringbuf_output(&events, e, sizeof(*e), 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Tracepoint for newfstatat syscall
|
||||
// newfstatat(int dirfd, const char *pathname, struct stat *statbuf, int flags)
|
||||
SEC("tracepoint/syscalls/sys_enter_newfstatat")
|
||||
int trace_newfstatat(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 zero = 0;
|
||||
struct event *e = bpf_map_lookup_elem(&heap, &zero);
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
e->cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
e->syscall_nr = ctx->id;
|
||||
|
||||
const char *pathname = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user_str(&e->path, MAX_PATH_LEN, pathname);
|
||||
|
||||
bpf_ringbuf_output(&events, e, sizeof(*e), 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Tracepoint for faccessat syscall
|
||||
// faccessat(int dirfd, const char *pathname, int mode)
|
||||
SEC("tracepoint/syscalls/sys_enter_faccessat")
|
||||
int trace_faccessat(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 zero = 0;
|
||||
struct event *e = bpf_map_lookup_elem(&heap, &zero);
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
e->cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
e->syscall_nr = ctx->id;
|
||||
|
||||
const char *pathname = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user_str(&e->path, MAX_PATH_LEN, pathname);
|
||||
|
||||
bpf_ringbuf_output(&events, e, sizeof(*e), 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Tracepoint for faccessat2 syscall (kernel 5.8+)
|
||||
// faccessat2(int dirfd, const char *pathname, int mode, int flags)
|
||||
SEC("tracepoint/syscalls/sys_enter_faccessat2")
|
||||
int trace_faccessat2(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 zero = 0;
|
||||
struct event *e = bpf_map_lookup_elem(&heap, &zero);
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
e->cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
e->syscall_nr = ctx->id;
|
||||
|
||||
const char *pathname = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user_str(&e->path, MAX_PATH_LEN, pathname);
|
||||
|
||||
bpf_ringbuf_output(&events, e, sizeof(*e), 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Tracepoint for readlinkat syscall
|
||||
// readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz)
|
||||
SEC("tracepoint/syscalls/sys_enter_readlinkat")
|
||||
int trace_readlinkat(struct trace_event_raw_sys_enter *ctx) {
|
||||
if (!should_trace()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 zero = 0;
|
||||
struct event *e = bpf_map_lookup_elem(&heap, &zero);
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
e->cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
e->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
e->syscall_nr = ctx->id;
|
||||
|
||||
const char *pathname = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user_str(&e->path, MAX_PATH_LEN, pathname);
|
||||
|
||||
bpf_ringbuf_output(&events, e, sizeof(*e), 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
char __license[] SEC("license") = "GPL";
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ func NewProbe() (*Probe, error) {
|
|||
|
||||
// attachTracepoints attaches the eBPF programs to syscall tracepoints
|
||||
func (p *Probe) attachTracepoints() error {
|
||||
// Required tracepoints (must exist on all supported kernels)
|
||||
// Attach openat tracepoint
|
||||
l, err := link.Tracepoint("syscalls", "sys_enter_openat", p.objs.TraceOpenat, nil)
|
||||
if err != nil {
|
||||
|
|
@ -71,6 +72,48 @@ func (p *Probe) attachTracepoints() error {
|
|||
}
|
||||
p.links = append(p.links, l)
|
||||
|
||||
// Attach newfstatat tracepoint (fstatat/stat)
|
||||
l, err = link.Tracepoint("syscalls", "sys_enter_newfstatat", p.objs.TraceNewfstatat, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attaching newfstatat tracepoint: %w", err)
|
||||
}
|
||||
p.links = append(p.links, l)
|
||||
|
||||
// Attach faccessat tracepoint (access)
|
||||
l, err = link.Tracepoint("syscalls", "sys_enter_faccessat", p.objs.TraceFaccessat, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attaching faccessat tracepoint: %w", err)
|
||||
}
|
||||
p.links = append(p.links, l)
|
||||
|
||||
// Attach readlinkat tracepoint (readlink)
|
||||
l, err = link.Tracepoint("syscalls", "sys_enter_readlinkat", p.objs.TraceReadlinkat, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attaching readlinkat tracepoint: %w", err)
|
||||
}
|
||||
p.links = append(p.links, l)
|
||||
|
||||
// Optional tracepoints (may not exist on older kernels)
|
||||
// execveat - exec with dirfd
|
||||
if l, err = link.Tracepoint("syscalls", "sys_enter_execveat", p.objs.TraceExecveat, nil); err == nil {
|
||||
p.links = append(p.links, l)
|
||||
}
|
||||
|
||||
// openat2 - kernel 5.6+
|
||||
if l, err = link.Tracepoint("syscalls", "sys_enter_openat2", p.objs.TraceOpenat2, nil); err == nil {
|
||||
p.links = append(p.links, l)
|
||||
}
|
||||
|
||||
// statx - kernel 4.11+
|
||||
if l, err = link.Tracepoint("syscalls", "sys_enter_statx", p.objs.TraceStatx, nil); err == nil {
|
||||
p.links = append(p.links, l)
|
||||
}
|
||||
|
||||
// faccessat2 - kernel 5.8+
|
||||
if l, err = link.Tracepoint("syscalls", "sys_enter_faccessat2", p.objs.TraceFaccessat2, nil); err == nil {
|
||||
p.links = append(p.links, l)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
36
plan.md
36
plan.md
|
|
@ -1,18 +1,18 @@
|
|||
# Snoop: Production File Access Observer
|
||||
|
||||
## 🚧 Current Status: Milestone 1 - Proof of Concept Complete
|
||||
## 🚧 Current Status: Milestone 2 - Core Functionality In Progress
|
||||
|
||||
**Last Updated**: 2026-01-14
|
||||
|
||||
The foundational infrastructure for Snoop has been implemented:
|
||||
- ✅ eBPF program with `openat` and `execve` tracing
|
||||
Milestone 1 complete, now working on Milestone 2:
|
||||
- ✅ eBPF program with full syscall coverage (openat, execve, stat, access, readlink variants)
|
||||
- ✅ Cgroup-based filtering for targeted container monitoring
|
||||
- ✅ Ring buffer event delivery from kernel to userspace
|
||||
- ✅ Go userspace loader using cilium/ebpf
|
||||
- ✅ Build infrastructure (Dockerfile, Makefile, CI)
|
||||
- ⏳ **Next**: Test on Linux system with Docker containers
|
||||
- ⏳ **Next**: Path normalization, deduplication, JSON reporting
|
||||
|
||||
See [Milestone 1](#milestone-1-ebpf-proof-of-concept--in-progress) for details.
|
||||
See [Milestone 2](#milestone-2-core-functionality) for details.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -302,7 +302,7 @@ Volume mounts:
|
|||
|
||||
## Milestones
|
||||
|
||||
### Milestone 1: eBPF Proof of Concept ✅ IN PROGRESS
|
||||
### Milestone 1: eBPF Proof of Concept ✅ COMPLETE
|
||||
|
||||
**Goal**: Prove we can trace file syscalls and filter by cgroup from a container.
|
||||
|
||||
|
|
@ -314,11 +314,6 @@ Volume mounts:
|
|||
- [x] Docker Compose file to test locally with a sample app
|
||||
- [x] Cgroup discovery utilities
|
||||
- [x] Helper scripts for finding container cgroups
|
||||
- [ ] vmlinux.h generation (requires Linux system)
|
||||
- [ ] End-to-end testing on Linux
|
||||
|
||||
**Current Status**:
|
||||
The core infrastructure is complete. The eBPF program (pkg/ebpf/bpf/snoop.c) traces `openat` and `execve` syscalls with cgroup filtering. The userspace Go loader uses cilium/ebpf to load the program and read events from a ring buffer. The main limitation is that eBPF development requires Linux, so the code needs to be tested on a Linux system.
|
||||
|
||||
**Files Created**:
|
||||
- `cmd/snoop/main.go` - Main entry point with signal handling
|
||||
|
|
@ -331,29 +326,14 @@ The core infrastructure is complete. The eBPF program (pkg/ebpf/bpf/snoop.c) tra
|
|||
- `Makefile` - Build automation
|
||||
- `.github/workflows/build.yaml` - CI pipeline
|
||||
|
||||
**Testing** (requires Linux):
|
||||
- Run snoop alongside `alpine` container running `cat /etc/passwd`
|
||||
- Verify `/etc/passwd` appears in snoop output
|
||||
- Verify snoop's own file accesses do NOT appear (cgroup filtering works)
|
||||
|
||||
**Success criteria**:
|
||||
- See file access events from target container
|
||||
- Filter out events from snoop itself
|
||||
- No kernel panics or container crashes
|
||||
|
||||
**Technical risks**:
|
||||
- BTF (BPF Type Format) availability in container environments → Addressed with CO-RE support
|
||||
- Cgroup v1 vs v2 differences → Currently targets cgroup v2
|
||||
- Kernel version compatibility (target: 5.4+) → Uses stable tracepoints
|
||||
|
||||
---
|
||||
|
||||
### Milestone 2: Core Functionality
|
||||
### Milestone 2: Core Functionality ⏳ IN PROGRESS
|
||||
|
||||
**Goal**: Complete syscall coverage, deduplication, and JSON output.
|
||||
|
||||
**Deliverables**:
|
||||
- [ ] All syscalls traced (openat, execve, stat variants, etc.)
|
||||
- [x] All syscalls traced (openat, openat2, execve, execveat, statx, newfstatat, faccessat, faccessat2, readlinkat)
|
||||
- [ ] Path normalization (resolve `.`, `..`, relative paths)
|
||||
- [ ] Configurable path exclusions
|
||||
- [ ] In-memory deduplication with efficient data structure
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue