Skip to content
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
23 changes: 23 additions & 0 deletions fact-ebpf/src/bpf/bound_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,29 @@ __always_inline static struct bound_path_t* _path_read(struct path* path, bound_
return bound_path;
}

/**
* Read a filesystem-relative path from a bare dentry.
*
* This is for hooks that only provide a struct dentry without a
* struct path (e.g. inode_set_acl). The resulting path can be used
* for LPM trie matching and inode monitoring checks.
*/
__always_inline static struct bound_path_t* dentry_read(struct dentry* dentry) {
struct bound_path_t* bound_path = get_bound_path(BOUND_PATH_MAIN);
if (bound_path == NULL) {
return NULL;
}

bound_path->len = __d_path_from_dentry(dentry, bound_path->path, PATH_MAX);
if (bound_path->len <= 0) {
return NULL;
}

bound_path->len = PATH_LEN_CLAMP(bound_path->len);

return bound_path;
}

__always_inline static struct bound_path_t* path_read_unchecked(struct path* path) {
return _path_read(path, BOUND_PATH_MAIN, true);
}
Expand Down
108 changes: 83 additions & 25 deletions fact-ebpf/src/bpf/d_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
*/
#define PATH_LEN_CLAMP(len) ((len) & PATH_MAX_MASK)

// Context for __d_path_inner.
//
// Supports two modes:
// Full path mode: mnt and root must both be set. Crosses mount
// boundaries and terminates at the process root.
// Dentry-only mode: mnt and root must both be NULL. Walks the
// dentry chain to the filesystem root, producing a
// filesystem-relative path.
struct d_path_ctx {
struct helper_t* helper;
struct path* root;
Expand All @@ -38,39 +46,49 @@ static long __d_path_inner(uint32_t index, void* _ctx) {
struct d_path_ctx* ctx = (struct d_path_ctx*)_ctx;
struct dentry* dentry = ctx->dentry;
struct dentry* parent = BPF_CORE_READ(dentry, d_parent);
struct mount* mnt = ctx->mnt;
struct dentry* mnt_root = BPF_CORE_READ(mnt, mnt.mnt_root);

if (dentry == ctx->root->dentry && &mnt->mnt == ctx->root->mnt) {
// Found the root of the process, we are done
ctx->success = true;
return 1;
}
if (ctx->mnt != NULL) {
// Full path mode: we have mount context and can cross mount
// boundaries and detect the process root.
struct mount* mnt = ctx->mnt;
struct dentry* mnt_root = BPF_CORE_READ(mnt, mnt.mnt_root);

if (dentry == mnt_root) {
struct mount* m = BPF_CORE_READ(mnt, mnt_parent);
if (m != mnt) {
// Current dentry is a mount root different to the previous one we
// had (to prevent looping), switch over to that mount position
// and keep walking up the path.
ctx->dentry = BPF_CORE_READ(mnt, mnt_mountpoint);
ctx->mnt = m;
return 0;
if (dentry == ctx->root->dentry && &mnt->mnt == ctx->root->mnt) {
// Found the root of the process, we are done
ctx->success = true;
return 1;
}

// Ended up in a global root, the path might need re-processing or
// the root is not attached yet, we are not getting a better path,
// so we assume we are correct and stop iterating.
ctx->success = true;
return 1;
if (dentry == mnt_root) {
struct mount* m = BPF_CORE_READ(mnt, mnt_parent);
if (m != mnt) {
// Current dentry is a mount root different to the previous one we
// had (to prevent looping), switch over to that mount position
// and keep walking up the path.
ctx->dentry = BPF_CORE_READ(mnt, mnt_mountpoint);
ctx->mnt = m;
return 0;
}

// Ended up in a global root, the path might need re-processing or
// the root is not attached yet, we are not getting a better path,
// so we assume we are correct and stop iterating.
ctx->success = true;
return 1;
}
}

if (dentry == parent) {
// We escaped the mounts and ended up at (most likely) the root of
// the device, the path we formed will be wrong.
// Reached the root of the filesystem's dentry tree.
//
// In full path mode (mnt != NULL) this means we escaped the mounts
// and the path may be wrong due to a race condition.
//
// This may happen in race conditions where some dentries go away
// while we are iterating.
// In dentry-only mode (mnt == NULL) this is the expected
// termination: we've reached the filesystem root and have a
// filesystem-relative path. This is correct for overlayfs
// (containers) and for files on the root filesystem.
ctx->success = (ctx->mnt == NULL);
return 1;
}

Expand Down Expand Up @@ -140,6 +158,46 @@ __always_inline static long __d_path(const struct path* path, char* buf, int buf
return buflen - ctx.offset;
}

/**
* Resolve a filesystem-relative path from a bare dentry.
*
* This is used when no struct path is available (e.g. inode_set_acl).
* It walks the dentry chain up to the filesystem root, producing a
* path relative to the filesystem's root dentry. This is correct for
* overlayfs (containers) and for files on the root filesystem. It
* cannot cross mount boundaries, so paths on nested host mounts (e.g.
* a separate /var partition) will be relative to that mount's root.
*/
__always_inline static long __d_path_from_dentry(struct dentry* dentry, char* buf, int buflen) {
if (buflen <= 0) {
return -1;
}

int offset = PATH_LEN_CLAMP(buflen - 1);
struct d_path_ctx ctx = {
.buflen = buflen,
.helper = get_helper(),
.offset = offset,
.mnt = NULL,
.root = NULL,
};

if (ctx.helper == NULL) {
return -1;
}

ctx.helper->buf[offset] = '\0';
ctx.dentry = dentry;

long res = bpf_loop(PATH_MAX, __d_path_inner, &ctx, 0);
if (res <= 0 || !ctx.success) {
return -1;
}

bpf_probe_read_str(buf, buflen, &ctx.helper->buf[PATH_LEN_CLAMP(ctx.offset)]);
return buflen - ctx.offset;
}

__always_inline static long d_path(struct path* path, char* buf, int buflen, bool use_bpf_helper) {
if (use_bpf_helper) {
return bpf_d_path(path, buf, buflen);
Expand Down
42 changes: 42 additions & 0 deletions fact-ebpf/src/bpf/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,45 @@ __always_inline static void submit_rmdir_event(struct submit_event_args_t* args)

__submit_event(args, path_hooks_support_bpf_d_path);
}

__always_inline static void submit_acl_event(struct submit_event_args_t* args,
const char* acl_name,
struct posix_acl* kacl) {
if (!reserve_event(args)) {
return;
}

args->event->type = FILE_ACTIVITY_ACL_SET;

// Determine ACL type from the xattr name.
// "system.posix_acl_access" vs "system.posix_acl_default"
char name_buf[32] = {0};
long name_len = bpf_probe_read_kernel_str(name_buf, sizeof(name_buf), acl_name);
if (name_len == 25 && __builtin_memcmp(name_buf, "system.posix_acl_default", 24) == 0) {
args->event->acl.acl_type = FACT_ACL_TYPE_DEFAULT;
} else {
args->event->acl.acl_type = FACT_ACL_TYPE_ACCESS;
}

if (kacl == NULL) {
args->event->acl.count = 0;
} else {
unsigned int count = 0;
bpf_probe_read_kernel(&count, sizeof(count), &kacl->a_count);
if (count > FACT_MAX_ACL_ENTRIES) {
count = FACT_MAX_ACL_ENTRIES;
}
args->event->acl.count = count;

for (unsigned int i = 0; i < FACT_MAX_ACL_ENTRIES && i < count; i++) {
struct posix_acl_entry entry = {0};
bpf_probe_read_kernel(&entry, sizeof(entry), &kacl->a_entries[i]);
args->event->acl.entries[i].e_tag = entry.e_tag;
args->event->acl.entries[i].e_perm = entry.e_perm;
args->event->acl.entries[i].e_id = entry.e_uid.val;
}
}

// inode_set_acl does not support bpf_d_path (no struct path available)
__submit_event(args, false);
}
32 changes: 32 additions & 0 deletions fact-ebpf/src/bpf/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,38 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) {
return 0;
}

SEC("lsm/inode_set_acl")
int BPF_PROG(trace_inode_set_acl, struct mnt_idmap* idmap, struct dentry* dentry,
const char* acl_name, struct posix_acl* kacl) {
struct metrics_t* m = get_metrics();
if (m == NULL) {
return 0;
}
struct submit_event_args_t args = {.metrics = &m->inode_set_acl};

args.metrics->total++;

struct bound_path_t* bound_path = dentry_read(dentry);
if (bound_path == NULL) {
bpf_printk("Failed to read path from dentry");
args.metrics->error++;
return 0;
}
args.filename = bound_path->path;

struct inode* inode_ptr = BPF_CORE_READ(dentry, d_inode);
args.inode = inode_to_key(inode_ptr);
args.monitored = is_monitored(&args.inode, bound_path, NULL);

if (args.monitored == NOT_MONITORED) {
args.metrics->ignored++;
return 0;
}

submit_acl_event(&args, acl_name, kacl);
return 0;
}

SEC("lsm/path_rmdir")
int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) {
struct metrics_t* m = get_metrics();
Expand Down
19 changes: 19 additions & 0 deletions fact-ebpf/src/bpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ typedef enum monitored_t {
// For the time being we just keep a char.
typedef char inode_value_t;

#define FACT_MAX_ACL_ENTRIES 32

// ACL type constants matching the xattr names
#define FACT_ACL_TYPE_ACCESS 0
#define FACT_ACL_TYPE_DEFAULT 1

struct acl_entry_t {
short e_tag;
unsigned short e_perm;
unsigned int e_id;
};

typedef enum file_activity_type_t {
FILE_ACTIVITY_INIT = -1,
FILE_ACTIVITY_OPEN = 0,
Expand All @@ -64,6 +76,7 @@ typedef enum file_activity_type_t {
FILE_ACTIVITY_RENAME,
DIR_ACTIVITY_CREATION,
DIR_ACTIVITY_UNLINK,
FILE_ACTIVITY_ACL_SET,
} file_activity_type_t;

struct event_t {
Expand All @@ -90,6 +103,11 @@ struct event_t {
inode_key_t inode;
monitored_t monitored;
} rename;
struct {
unsigned int count;
unsigned int acl_type;
struct acl_entry_t entries[FACT_MAX_ACL_ENTRIES];
} acl;
};
};

Expand Down Expand Up @@ -132,4 +150,5 @@ struct metrics_t {
struct metrics_by_hook_t path_mkdir;
struct metrics_by_hook_t d_instantiate;
struct metrics_by_hook_t path_rmdir;
struct metrics_by_hook_t inode_set_acl;
};
1 change: 1 addition & 0 deletions fact-ebpf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ impl metrics_t {
self.path_mkdir = self.path_mkdir.accumulate(&other.path_mkdir);
self.path_rmdir = self.path_rmdir.accumulate(&other.path_rmdir);
self.d_instantiate = self.d_instantiate.accumulate(&other.d_instantiate);
self.inode_set_acl = self.inode_set_acl.accumulate(&other.inode_set_acl);
self
}
}
Expand Down
36 changes: 25 additions & 11 deletions fact/src/bpf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ mod checks;

const RINGBUFFER_NAME: &str = "rb";

/// Hooks that may not be available on all supported kernels. If these
/// fail to load, FACT will log a warning and continue without them.
const OPTIONAL_HOOKS: &[&str] = &["inode_set_acl"];

pub struct Bpf {
obj: Ebpf,

Expand Down Expand Up @@ -178,28 +182,38 @@ impl Bpf {
let Some(hook) = name.strip_prefix("trace_") else {
bail!("Invalid hook name: {name}");
};
match prog {
Program::Lsm(prog) => prog.load(hook, btf)?,
let result = match prog {
Program::Lsm(prog) => prog.load(hook, btf),
u => unimplemented!("{u:?}"),
};
if let Err(e) = result {
if OPTIONAL_HOOKS.contains(&hook) {
warn!("Optional hook {hook} not available on this kernel, skipping: {e}");
continue;
}
return Err(e.into());
}
}
Ok(())
}

/// Attaches all BPF programs. If any attach fails, all previously
/// attached programs are automatically detached via drop.
/// Attaches all loaded BPF programs. Programs that were not loaded
/// (e.g. optional hooks on unsupported kernels) are skipped.
/// If any attach fails, all previously attached programs are
/// automatically detached via drop.
fn attach_progs(&mut self) -> anyhow::Result<()> {
self.links = self
.obj
.programs_mut()
.map(|(_, prog)| match prog {
for (_, prog) in self.obj.programs_mut() {
match prog {
Program::Lsm(prog) => {
if prog.fd().is_err() {
continue;
}
let link_id = prog.attach()?;
prog.take_link(link_id)
self.links.push(prog.take_link(link_id)?);
}
u => unimplemented!("{u:?}"),
})
.collect::<Result<_, _>>()?;
}
}
Ok(())
}

Expand Down
Loading
Loading