1
0
Fork 0
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:
Jason Hall 2026-01-14 10:00:10 -05:00
parent f793a98161
commit 23d94e3049
4 changed files with 243 additions and 32 deletions

View file

@ -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

View file

@ -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";

View file

@ -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
View file

@ -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