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
5 changes: 4 additions & 1 deletion fact-ebpf/src/bpf/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct submit_event_args_t {
inode_key_t inode;
inode_key_t parent_inode;
monitored_t monitored;
unsigned int nlink;
};

__always_inline static bool reserve_event(struct submit_event_args_t* args) {
Expand Down Expand Up @@ -117,7 +118,8 @@ __always_inline static void submit_ownership_event(struct submit_event_args_t* a
__always_inline static void submit_rename_event(struct submit_event_args_t* args,
const char old_filename[PATH_MAX],
inode_key_t* old_inode,
monitored_t old_monitored) {
monitored_t old_monitored,
unsigned int old_nlink) {
if (!reserve_event(args)) {
return;
}
Expand All @@ -126,6 +128,7 @@ __always_inline static void submit_rename_event(struct submit_event_args_t* args
bpf_probe_read_str(args->event->from.filename, PATH_MAX, old_filename);
inode_copy(&args->event->from.inode, old_inode);
args->event->from.monitored = old_monitored;
args->event->from.nlink = old_nlink;

__submit_event(args, path_hooks_support_bpf_d_path);
}
Expand Down
95 changes: 84 additions & 11 deletions fact-ebpf/src/bpf/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ int BPF_PROG(trace_file_open, struct file* file) {
inode_add(&args.inode);
}

args.nlink = BPF_CORE_READ(file, f_inode, i_nlink);
submit_open_event(&args, event_type);

return 0;
Expand All @@ -86,6 +87,51 @@ int BPF_PROG(trace_file_open, struct file* file) {
return 0;
}

SEC("lsm/path_link")
int BPF_PROG(trace_path_link, struct dentry* old_dentry, const struct path* new_dir, struct dentry* new_dentry) {
struct metrics_t* m = get_metrics();
if (m == NULL) {
return 0;
}
struct submit_event_args_t args = {.metrics = &m->file_open};

args.metrics->total++;

struct bound_path_t* new_path = path_read_append_d_entry((struct path*)new_dir, new_dentry);
if (new_path == NULL) {
bpf_printk("Failed to read new path");
m->file_open.error++;
return 0;
}
args.filename = new_path->path;

// The inode is from the old file (being linked to)
args.inode = inode_to_key(old_dentry->d_inode);

struct dentry* parent_dentry = BPF_CORE_READ(new_dir, dentry);
struct inode* parent_inode_ptr = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL;
args.parent_inode = inode_to_key(parent_inode_ptr);

args.monitored = is_monitored(&args.inode, new_path, &args.parent_inode);
if (args.monitored == NOT_MONITORED) {
goto ignored;
}

// Add the inode to tracking if monitored by parent
if (args.monitored == MONITORED_BY_PARENT) {
inode_add(&args.inode);
}

args.nlink = BPF_CORE_READ(old_dentry, d_inode, i_nlink);
submit_open_event(&args, FILE_ACTIVITY_CREATION);

return 0;

ignored:
m->file_open.ignored++;
return 0;
}

SEC("lsm/path_unlink")
int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) {
struct metrics_t* m = get_metrics();
Expand All @@ -112,8 +158,15 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) {
return 0;
}

// We only support files with one link for now
inode_remove(&args.inode);
// Read nlink BEFORE the unlink completes (this is a pre-hook)
// After unlink, nlink will be decremented by 1
unsigned int nlink_before = BPF_CORE_READ(dentry, d_inode, i_nlink);
args.nlink = nlink_before > 0 ? nlink_before - 1 : 0;

// Only remove from kernel map if this is the last link
if (args.nlink == 0) {
inode_remove(&args.inode);
}

submit_unlink_event(&args);
return 0;
Expand Down Expand Up @@ -146,6 +199,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) {
}

umode_t old_mode = BPF_CORE_READ(path, dentry, d_inode, i_mode);
args.nlink = BPF_CORE_READ(path, dentry, d_inode, i_nlink);
submit_mode_event(&args, mode, old_mode);

return 0;
Expand Down Expand Up @@ -183,6 +237,7 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign
struct dentry* d = BPF_CORE_READ(path, dentry);
unsigned long long old_uid = BPF_CORE_READ(d, d_inode, i_uid.val);
unsigned long long old_gid = BPF_CORE_READ(d, d_inode, i_gid.val);
args.nlink = BPF_CORE_READ(d, d_inode, i_nlink);

submit_ownership_event(&args, uid, gid, old_uid, old_gid);

Expand Down Expand Up @@ -221,6 +276,16 @@ int BPF_PROG(trace_path_rename, struct path* old_dir,
inode_key_t old_inode = inode_to_key(old_dentry->d_inode);
monitored_t old_monitored = is_monitored(&old_inode, old_path, NULL);

// Get nlink for both old and new dentries
// For rename, the old path's inode nlink doesn't change (same inode, just different path)
unsigned int old_nlink = BPF_CORE_READ(old_dentry, d_inode, i_nlink);
// new_dentry->d_inode is the inode being overwritten (if any), read before it's removed
unsigned int new_nlink = 0;
if (new_dentry->d_inode != NULL) {
new_nlink = BPF_CORE_READ(new_dentry, d_inode, i_nlink);
}
args.nlink = old_nlink;

// From this point on we need to handle inode tracking.
//
// The result will be a combination of whether we are already tracking
Expand All @@ -238,7 +303,8 @@ int BPF_PROG(trace_path_rename, struct path* old_dir,
// Old inode is monitored, new path is not.
// If the old path is a directory userspace will remove any
// subdirectories and files too.
inode_remove(&old_inode);
// Note: We don't remove from kernel map here - userspace will decide
// based on whether other monitored hardlinks exist
}
break;

Expand All @@ -250,7 +316,7 @@ int BPF_PROG(trace_path_rename, struct path* old_dir,
// which should never happen. When the inode crosses into a new
// mount, a new inode is created altogether. Still, we can cover
// our bases.
inode_remove(&old_inode);
// Note: We don't remove from kernel map here - userspace will decide
}
break;

Expand All @@ -264,17 +330,19 @@ int BPF_PROG(trace_path_rename, struct path* old_dir,
}
} else if (!inode_is_empty(&args.inode)) {
// Old inode is monitored and will land on a path that has a
// monitored parent but the path itself is not monitored, we
// stop tracking the inode
inode_remove(&old_inode);
// monitored parent but the path itself is not monitored
// Note: We don't remove from kernel map here - userspace will decide
// based on whether other monitored hardlinks exist
}
break;

case MONITORED_BY_INODE:
// If we landed here, the new path already has an inode that is
// being tracked and is about to be overwritten, we need to remove
// it from the map
inode_remove(&args.inode);
// being tracked and is about to be overwritten
// Only remove if this is the last link to the overwritten inode
if (new_nlink <= 1) {
inode_remove(&args.inode);
}
if (old_monitored != MONITORED_BY_INODE) {
// Old inode is not monitored, but is landing in a monitored
// path that uses inode tracking.
Expand All @@ -283,7 +351,7 @@ int BPF_PROG(trace_path_rename, struct path* old_dir,
break;
}

submit_rename_event(&args, old_path->path, &old_inode, old_monitored);
submit_rename_event(&args, old_path->path, &old_inode, old_monitored, old_nlink);
return 0;

error:
Expand Down Expand Up @@ -375,6 +443,7 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) {
args.monitored = mkdir_ctx->monitored;

args.inode = inode_to_key(inode);
args.nlink = BPF_CORE_READ(inode, i_nlink);

if (inode_add(&args.inode) == 0) {
args.metrics->added++;
Expand Down Expand Up @@ -481,6 +550,10 @@ int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) {
return 0;
}

// Directories should have nlink = 2 when empty (. and ..)
// After rmdir, it will be removed, so we always remove from map
args.nlink = BPF_CORE_READ(dentry, d_inode, i_nlink);

submit_rmdir_event(&args);
return 0;
}
Expand Down
1 change: 1 addition & 0 deletions fact-ebpf/src/bpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ struct event_t {
char filename[PATH_MAX];
inode_key_t inode;
monitored_t monitored;
unsigned int nlink;
} from; // Used by events that have two paths (like rename or move_mount).
struct {
char name[XATTR_NAME_MAX_LEN];
Expand Down
20 changes: 18 additions & 2 deletions fact/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl Event {
inode: Default::default(),
parent_inode: Default::default(),
monitored: Default::default(),
nlink: Default::default(),
};
let file = match data {
EventTestData::Creation => FileData::Creation(inner),
Expand Down Expand Up @@ -373,12 +374,22 @@ impl TryFrom<&event_t> for Event {
fn try_from(value: &event_t) -> Result<Self, Self::Error> {
let process = Process::try_from(value.process)?;
let timestamp = host_info::get_boot_time() + value.timestamp;
// nlink is only available in the union for rename/move_mount events (from.nlink)
// For other events, we use the default value
let nlink = match value.type_ {
file_activity_type_t::FILE_ACTIVITY_RENAME
| file_activity_type_t::FILE_ACTIVITY_MOVE_MOUNT => unsafe {
value.__bindgen_anon_1.from.nlink
},
_ => Default::default(),
};
let file = FileData::new(
value.type_,
value.filename,
value.inode,
value.parent_inode,
value.monitored,
nlink,
value.__bindgen_anon_1,
)?;

Expand Down Expand Up @@ -456,17 +467,19 @@ impl FileData {
inode: inode_key_t,
parent_inode: inode_key_t,
monitored: monitored_t,
nlink: u32,
extra_data: fact_ebpf::event_t__bindgen_ty_1,
) -> anyhow::Result<Self> {
fn read_from_data(extra_data: fact_ebpf::event_t__bindgen_ty_1) -> BaseFileData {
let filename = unsafe { extra_data.from.filename };
let inode = unsafe { extra_data.from.inode };
let monitored = unsafe { extra_data.from.monitored };
let nlink = unsafe { extra_data.from.nlink };

BaseFileData::new(filename, inode, Default::default(), monitored)
BaseFileData::new(filename, inode, Default::default(), monitored, nlink)
}

let inner = BaseFileData::new(filename, inode, parent_inode, monitored);
let inner = BaseFileData::new(filename, inode, parent_inode, monitored, nlink);
let file = match event_type {
file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner),
file_activity_type_t::FILE_ACTIVITY_CREATION => FileData::Creation(inner),
Expand Down Expand Up @@ -693,6 +706,7 @@ pub struct BaseFileData {
inode: inode_key_t,
parent_inode: inode_key_t,
monitored: monitored_t,
pub nlink: u32,
}

impl BaseFileData {
Expand All @@ -701,13 +715,15 @@ impl BaseFileData {
inode: inode_key_t,
parent_inode: inode_key_t,
monitored: monitored_t,
nlink: u32,
) -> Self {
BaseFileData {
filename: sanitize_d_path(&filename),
host_file: PathBuf::new(), // this field is set by HostScanner
inode,
parent_inode,
monitored,
nlink,
}
}
}
Expand Down
Loading
Loading