Skip to content
Merged
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
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ DOCKERFILE_PATH=./build/Dockerfile
BINARY_NAME=node-agent

IMAGE?=quay.io/kubescape/$(BINARY_NAME)
GADGETS=advise_seccomp trace_capabilities trace_dns trace_exec trace_open
# GADGETS are pulled unmodified from upstream IG. trace_open is intentionally NOT
# here: it is vendored and built from source (see BUILT_GADGETS) so the fpath
# resolver can resolve relative opens against their dirfd/cwd.
GADGETS=advise_seccomp trace_capabilities trace_dns trace_exec
VERSION=v0.48.1
KUBESCAPE_GADGETS=bpf exit fork hardlink http iouring_new iouring_old kmod network ptrace randomx ssh symlink unshare
# BUILT_GADGETS are vendored under pkg/ebpf/gadgets and built under their full
# upstream image name+tag so node-agent's pinned openImageName keeps resolving.
BUILT_GADGETS=trace_open
TAG?=test
# TAG?=v0.0.1

Expand All @@ -26,5 +32,6 @@ docker-push: docker-build

gadgets:
$(foreach img,$(KUBESCAPE_GADGETS),$(MAKE) -C ./pkg/ebpf/gadgets/$(img) build IMAGE=$(img) TAG=latest;)
$(foreach img,$(BUILT_GADGETS),$(MAKE) -C ./pkg/ebpf/gadgets/$(img) build IMAGE=ghcr.io/inspektor-gadget/gadget/$(img) TAG=$(VERSION);)
$(foreach img,$(GADGETS),sudo ig image pull ghcr.io/inspektor-gadget/gadget/$(img):$(VERSION);)
sudo ig image export $(foreach img,$(GADGETS),ghcr.io/inspektor-gadget/gadget/$(img):$(VERSION)) $(foreach img,$(KUBESCAPE_GADGETS),$(img):latest) tracers.tar
sudo ig image export $(foreach img,$(GADGETS) $(BUILT_GADGETS),ghcr.io/inspektor-gadget/gadget/$(img):$(VERSION)) $(foreach img,$(KUBESCAPE_GADGETS),$(img):latest) tracers.tar
11 changes: 11 additions & 0 deletions pkg/ebpf/gadgets/trace_open/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
IMAGE?=ghcr.io/inspektor-gadget/gadget/trace_open
TAG?=v0.48.1
BUILDER_IMAGE?=ghcr.io/inspektor-gadget/gadget-builder@sha256:e0e88543a99bbc4b1263b7f9d07a08fb8f1107426525ef8b777bdd8df15d1ee8

build:
sudo TMPDIR=$$TMPDIR ig image build -t $(IMAGE):$(TAG) . --update-metadata --builder-image $(BUILDER_IMAGE)

run:
sudo ig run $(IMAGE):$(TAG) --verify-image=false -o jsonpretty

build-and-run: build run
260 changes: 260 additions & 0 deletions pkg/ebpf/gadgets/trace_open/filesystem_patched.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Based on
// https://github.com/aquasecurity/tracee/blob/bd80c1d9e69e275f06810f2a0f99414aced14fa8/pkg/ebpf/c/common/filesystem.h

#ifndef __COMMON_FILESYSTEM_H__
#define __COMMON_FILESYSTEM_H__

// clang-format off
#define MAX_PERCPU_BUFSIZE (1 << 15) // set by the kernel as an upper bound
#define GADGET_PATH_MAX 512 // smaller than PATH_MAX to reduce memory consumption
#define PATH_MAX 4096
#define MAX_STR_FILTER_SIZE 16 // bounded to size of the compared values (comm)
#define MAX_BIN_PATH_SIZE 256 // max binary path size
#define FILE_MAGIC_HDR_SIZE 32 // magic_write: bytes to save from a file's header
#define FILE_MAGIC_MASK 31 // magic_write: mask used for verifier boundaries
#define NET_SEQ_OPS_SIZE 4 // print_net_seq_ops: struct size - TODO: replace with uprobe argument
#define NET_SEQ_OPS_TYPES 6 // print_net_seq_ops: argument size - TODO: replace with uprobe argument
#define MAX_KSYM_NAME_SIZE 64
#define UPROBE_MAGIC_NUMBER 20220829
#define ARGS_BUF_SIZE 32000
#define SEND_META_SIZE 24
#define MAX_MEM_DUMP_SIZE 127

#define MAX_PATH_COMPONENTS 80

// memory related
enum buf_idx_e
{
STRING_BUF_IDX,
//FILE_BUF_IDX,
MAX_BUFFERS
};

typedef struct simple_buf {
u8 buf[MAX_PERCPU_BUFSIZE];
} buf_t;

#define BPF_MAP(_name, _type, _key_type, _value_type, _max_entries) \
struct { \
__uint(type, _type); \
__uint(max_entries, _max_entries); \
__type(key, _key_type); \
__type(value, _value_type); \
} _name SEC(".maps");


#define BPF_PERCPU_ARRAY(_name, _value_type, _max_entries) \
BPF_MAP(_name, BPF_MAP_TYPE_PERCPU_ARRAY, u32, _value_type, _max_entries)

BPF_PERCPU_ARRAY(bufs, buf_t, MAX_BUFFERS); // percpu global buffer variables

// undef as we don't want to use this in our gadgets. yet?
#undef BPF_MAP
#undef BPF_PERCPU_ARRAY

static __always_inline buf_t *get_buf(int idx)
{
return bpf_map_lookup_elem(&bufs, &idx);
}

static __always_inline struct dentry *get_mnt_root_ptr_from_vfsmnt(struct vfsmount *vfsmnt)
{
return BPF_CORE_READ(vfsmnt, mnt_root);
}

static __always_inline struct dentry *get_d_parent_ptr_from_dentry(struct dentry *dentry)
{
return BPF_CORE_READ(dentry, d_parent);
}

static inline struct mount *real_mount(struct vfsmount *mnt)
{
return container_of(mnt, struct mount, mnt);
}

static __always_inline struct qstr get_d_name_from_dentry(struct dentry *dentry)
{
return BPF_CORE_READ(dentry, d_name);
}

static __always_inline void *get_path_str(struct path *path)
{
struct path f_path;
bpf_probe_read(&f_path, sizeof(struct path), path);
char slash = '/';
int zero = 0;
struct dentry *dentry = f_path.dentry;
struct vfsmount *vfsmnt = f_path.mnt;
struct mount *mnt_parent_p;

struct mount *mnt_p = real_mount(vfsmnt);
bpf_probe_read(&mnt_parent_p, sizeof(struct mount *), &mnt_p->mnt_parent);

u32 buf_off = (MAX_PERCPU_BUFSIZE >> 1);
struct dentry *mnt_root;
struct dentry *d_parent;
struct qstr d_name;
unsigned int len;
unsigned int off;
int sz;

// Get per-cpu string buffer
buf_t *string_p = get_buf(STRING_BUF_IDX);
if (string_p == NULL)
return NULL;

#pragma unroll
for (int i = 0; i < MAX_PATH_COMPONENTS; i++) {
mnt_root = get_mnt_root_ptr_from_vfsmnt(vfsmnt);
d_parent = get_d_parent_ptr_from_dentry(dentry);
if (dentry == mnt_root || dentry == d_parent) {
if (dentry != mnt_root) {
// We reached root, but not mount root - escaped?
break;
}
if (mnt_p != mnt_parent_p) {
// We reached root, but not global root - continue with mount point path
bpf_probe_read(&dentry, sizeof(struct dentry *), &mnt_p->mnt_mountpoint);
bpf_probe_read(&mnt_p, sizeof(struct mount *), &mnt_p->mnt_parent);
bpf_probe_read(&mnt_parent_p, sizeof(struct mount *), &mnt_p->mnt_parent);
vfsmnt = &mnt_p->mnt;
continue;
}
// Global root - path fully parsed
break;
}
// Add this dentry name to path
d_name = get_d_name_from_dentry(dentry);
len = (d_name.len + 1) & (PATH_MAX - 1);
off = buf_off - len;

// Is string buffer big enough for dentry name?
sz = 0;
if (off <= buf_off) { // verify no wrap occurred
len = len & ((MAX_PERCPU_BUFSIZE >> 1) - 1);
sz = bpf_probe_read_str(
&(string_p->buf[off & ((MAX_PERCPU_BUFSIZE >> 1) - 1)]), len, (void *) d_name.name);
} else
break;
if (sz > 1) {
buf_off -= 1; // remove null byte termination with slash sign
bpf_probe_read(&(string_p->buf[buf_off & (MAX_PERCPU_BUFSIZE - 1)]), 1, &slash);
buf_off -= sz - 1;
} else {
// If sz is 0 or 1 we have an error (path can't be null nor an empty string)
break;
}
dentry = d_parent;
}

if (buf_off == (MAX_PERCPU_BUFSIZE >> 1)) {
buf_off = 0;
d_name = get_d_name_from_dentry(dentry);
sz = bpf_probe_read_str(&(string_p->buf[0]), PATH_MAX, (void *) d_name.name);
if (sz <= 1)
return NULL;
} else {
// Add leading slash
buf_off -= 1;
bpf_probe_read(&(string_p->buf[buf_off & (MAX_PERCPU_BUFSIZE - 1)]), 1, &slash);
// Null terminate the path string
bpf_probe_read(&(string_p->buf[(MAX_PERCPU_BUFSIZE >> 1) - 1]), 1, &zero);
}

return &string_p->buf[buf_off];
}

// Function to extract file structure from a user space file descriptor
static __always_inline struct file * get_struct_file_for_fd(int fd_num)
{
if (fd_num < 0) {
return NULL;
}

struct task_struct *task = (struct task_struct *) bpf_get_current_task();
if (task == NULL) {
return NULL;
}

// extract the file vector from the task_struct
struct file **fd = BPF_CORE_READ(task, files, fdt, fd);

// extract the file pointer from the file vector
struct file *f = NULL;
uint max_fds = BPF_CORE_READ(task, files, fdt, max_fds);
if (fd_num < max_fds) {
bpf_core_read((void *) &f, sizeof(f), &fd[fd_num]);
}

return f;
}

static __always_inline long read_full_path_of_open_file_fd(int fd_num, char *buf, u64 buf_len)
{
struct file *file = get_struct_file_for_fd(fd_num);
if (file == NULL) {
return -1;
}

struct path f_path = BPF_CORE_READ(file, f_path);

// Extract the full path string
char* c_path = get_path_str(&f_path);
if (!c_path) {
return -1;
}
return bpf_probe_read_kernel_str(buf, buf_len, c_path);
}


#ifndef AT_FDCWD
#define AT_FDCWD -100
#endif

// buf is always GADGET_PATH_MAX bytes: the verifier needs a compile-time bound
// for the masked writes below, so the size is not a runtime parameter.
static __always_inline long read_full_path_of_dfd_rel(int dfd, const char *user_fname,
char *buf)
{
struct path base;

if (dfd == AT_FDCWD) {
struct task_struct *task = (struct task_struct *) bpf_get_current_task();
if (!task)
return -1;
base = BPF_CORE_READ(task, fs, pwd);
} else {
struct file *f = get_struct_file_for_fd(dfd);
if (!f)
return -1;
base = BPF_CORE_READ(f, f_path);
}

char *bstr = get_path_str(&base);
if (!bstr)
return -1;

long n = bpf_probe_read_kernel_str(buf, GADGET_PATH_MAX, bstr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: read_full_path_of_dfd_rel takes a buf_len parameter but ignores it, hardcoding GADGET_PATH_MAX here and at lines 244-245 instead. Not a live bug — the only call site (program.bpf.c) always passes sizeof(event->fpath), which is exactly GADGET_PATH_MAX — but it's a landmine if this helper ever gets reused with a differently-sized buffer, since the masking arithmetic (& (GADGET_PATH_MAX - 1)) would silently wrap into the caller's buffer using the wrong size instead of bounding against the real buf_len. Worth using buf_len (and rejecting if it doesn't fit) to make the signature honest.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by dropping the parameter: buf is always the GADGET_PATH_MAX-sized fpath, and wiring a runtime length into the masked append is rejected by the verifier (invalid access to memory, mem_size=912 off=911 size=255) — the masks need a compile-time-constant bound. Using the true buffer size (GADGET_PATH_MAX) throughout keeps the signature honest without the dead parameter.

if (n <= 1)
return -1;

u32 off = (u32) (n - 1);
#define REL_NAME_MAX 256
// Base too long to append the name within GADGET_PATH_MAX: fail closed
// rather than truncate the base into a plausible but wrong absolute path.
if (off >= GADGET_PATH_MAX - REL_NAME_MAX)
return -1;
// Redundant given the check above, but the verifier needs the constant
// mask to prove the REL_NAME_MAX write below stays in bounds.
off &= (GADGET_PATH_MAX - REL_NAME_MAX - 1);
buf[off & (GADGET_PATH_MAX - 1)] = '/';
long m = bpf_probe_read_user_str(&buf[(off + 1) & (GADGET_PATH_MAX - 1)],
REL_NAME_MAX - 1, user_fname);
if (m <= 1) {
buf[off & (GADGET_PATH_MAX - 1)] = '\0';
return -1;
}
return off + m;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

#endif
91 changes: 91 additions & 0 deletions pkg/ebpf/gadgets/trace_open/gadget.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: trace open
description: trace open files
homepageURL: https://inspektor-gadget.io/
documentationURL: https://www.inspektor-gadget.io/docs/latest/gadgets/trace_open
sourceURL: https://github.com/inspektor-gadget/inspektor-gadget/tree/main/gadgets/trace_open
datasources:
open:
fields:
error_raw:
annotations:
columns.hidden: "true"
fd:
annotations:
columns.alignment: right
columns.maxwidth: "3"
columns.minwidth: "2"
description: File descriptor. 0 in case of error
flags:
annotations:
columns.hidden: "true"
columns.width: "10"
flags_raw:
annotations:
columns.hidden: "true"
fname:
annotations:
columns.minwidth: "24"
columns.width: "32"
description: File name as given in the open syscall
fpath:
annotations:
columns.minwidth: "24"
columns.width: "32"
description: Full file path after symlink resolution (require --paths flag)
mode:
annotations:
description: File access mode
mode_raw:
annotations:
columns.hidden: "true"
proc:
annotations:
description: 'TODO: Fill field description'
timestamp_raw:
annotations:
description: 'TODO: Fill field description'
ustack:
annotations:
description: 'TODO: Fill field description'
params:
ebpf:
collect_build_id:
key: collect_build_id
defaultValue: ""
description: 'TODO: Fill parameter description'
collect_ustack:
key: collect_ustack
defaultValue: ""
description: 'TODO: Fill parameter description'
ig_build_id_max_entries:
key: ig_build_id_max_entries
defaultValue: ""
description: 'TODO: Fill parameter description'
paths:
key: paths
defaultValue: "false"
description: Show file path after symlink resolution
targ_comm:
key: targ_comm
defaultValue: ""
description: 'TODO: Fill parameter description'
targ_failed:
key: failed
defaultValue: "false"
description: Show only failed events
targ_gid:
key: targ_gid
defaultValue: ""
description: 'TODO: Fill parameter description'
targ_pid:
key: targ_pid
defaultValue: ""
description: 'TODO: Fill parameter description'
targ_tid:
key: targ_tid
defaultValue: ""
description: 'TODO: Fill parameter description'
targ_uid:
key: targ_uid
defaultValue: ""
description: 'TODO: Fill parameter description'
Loading
Loading