Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fd0fd1c
X-Smart-Branch-Parent: main
JoukoVirtanen Apr 9, 2026
5b2167e
Added integration tests
JoukoVirtanen Mar 31, 2026
12f602e
Instrument inode tracking on directory being created
JoukoVirtanen Apr 1, 2026
8406a5a
Only tracking directories if the parent is monitored
JoukoVirtanen Apr 1, 2026
03bd26e
Removed some comments
JoukoVirtanen Apr 1, 2026
b1fc6f5
Combined two uses of BPF_CORE_READ
JoukoVirtanen Apr 2, 2026
dbea1a7
Added DIR_ACTIVITY_CREATION
JoukoVirtanen Apr 2, 2026
d1b4136
Added permalink to linux/stat.h
JoukoVirtanen Apr 2, 2026
a120084
Removing map entry in case of early return in lsm/d_instantiate
JoukoVirtanen Apr 2, 2026
fb7ce80
Using os.makedirs instead of os.mkdir three times
JoukoVirtanen Apr 2, 2026
60f96eb
Accumulate m.path_mkdir and m.d_instantiate
JoukoVirtanen Apr 3, 2026
651edd4
Checking pid_tgid earlier so if inode is null we still cleanup
JoukoVirtanen Apr 3, 2026
a57baf7
Not using BPF_F_NO_PREALLOC
JoukoVirtanen Apr 7, 2026
1fa3c77
Switched from BPF_MAP_TYPE_HASH to BPF_MAP_TYPE_LRU_HASH
JoukoVirtanen Apr 7, 2026
0d1929d
Apply suggestion from @Molter73
JoukoVirtanen Apr 7, 2026
e9634e5
Parameterized test
JoukoVirtanen Apr 7, 2026
11eba2e
Fixed verifier issue. Not sending directory creation events
JoukoVirtanen Apr 7, 2026
008b580
make format
JoukoVirtanen Apr 7, 2026
e86fa4e
Improved test Event constructor
JoukoVirtanen Apr 8, 2026
fd6bcbb
Remove is_dir_creation
JoukoVirtanen Apr 8, 2026
5f83efa
Removed unneeded S_ISDIR
JoukoVirtanen Apr 8, 2026
b97f03e
make format
JoukoVirtanen Apr 8, 2026
0874869
Update tests/test_path_mkdir.py
JoukoVirtanen Apr 9, 2026
ba14b12
Fixes after rebase
JoukoVirtanen Apr 9, 2026
e4cd882
Moved the check for null inode up
JoukoVirtanen Apr 9, 2026
34f8ff4
Not adding a new event type field to Event struct
JoukoVirtanen Apr 9, 2026
db4645b
make format
JoukoVirtanen Apr 9, 2026
c4ef0ac
Empty commit
JoukoVirtanen Apr 10, 2026
616720d
Apply suggestions from code review
JoukoVirtanen Apr 10, 2026
c7ec074
Fixed syntax error
JoukoVirtanen Apr 10, 2026
7ee6881
make format
JoukoVirtanen Apr 10, 2026
4b77658
Fixed clippy errors
JoukoVirtanen Apr 10, 2026
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
14 changes: 14 additions & 0 deletions fact-ebpf/src/bpf/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,17 @@ __always_inline static void submit_rename_event(struct metrics_by_hook_t* m,

__submit_event(event, m, FILE_ACTIVITY_RENAME, new_filename, new_inode, new_parent_inode, path_hooks_support_bpf_d_path);
}

__always_inline static void submit_mkdir_event(struct metrics_by_hook_t* m,
const char filename[PATH_MAX],
inode_key_t* inode,
inode_key_t* parent_inode) {
struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0);
if (event == NULL) {
m->ringbuffer_full++;
return;
}

// d_instantiate doesn't support bpf_d_path, so we use false and rely on the stashed path from path_mkdir
__submit_event(event, m, DIR_ACTIVITY_CREATION, filename, inode, parent_inode, false);
}
16 changes: 16 additions & 0 deletions fact-ebpf/src/bpf/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,19 @@ __always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct

return NOT_MONITORED;
}

// Check if a new directory should be tracked based on its parent and path.
// This is used during mkdir operations where the child inode doesn't exist yet.
__always_inline static inode_monitored_t should_track_mkdir(inode_key_t parent_inode, struct bound_path_t* child_path) {
const inode_value_t* volatile parent_value = inode_get(&parent_inode);

if (parent_value != NULL) {
return PARENT_MONITORED;
}

if (path_is_monitored(child_path)) {
return MONITORED;
}

return NOT_MONITORED;
}
95 changes: 95 additions & 0 deletions fact-ebpf/src/bpf/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,98 @@ int BPF_PROG(trace_path_rename, struct path* old_dir,
m->path_rename.error++;
return 0;
}

SEC("lsm/path_mkdir")
int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t mode) {
struct metrics_t* m = get_metrics();
if (m == NULL) {
return 0;
}

m->path_mkdir.total++;

struct bound_path_t* path = path_read_append_d_entry(dir, dentry);
if (path == NULL) {
bpf_printk("Failed to read path");
m->path_mkdir.error++;
return 0;
}

struct inode* parent_inode_ptr = BPF_CORE_READ(dir, dentry, d_inode);
inode_key_t parent_inode = inode_to_key(parent_inode_ptr);

if (should_track_mkdir(parent_inode, path) != PARENT_MONITORED) {
m->path_mkdir.ignored++;
return 0;
}

// Stash mkdir context for security_d_instantiate
__u64 pid_tgid = bpf_get_current_pid_tgid();
struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid);
if (mkdir_ctx == NULL) {
static const struct mkdir_context_t empty_ctx = {0};
if (bpf_map_update_elem(&mkdir_context, &pid_tgid, &empty_ctx, BPF_NOEXIST) != 0) {
bpf_printk("Failed to create mkdir context entry");
m->path_mkdir.error++;
return 0;
}
mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid);
if (mkdir_ctx == NULL) {
bpf_printk("Failed to lookup mkdir context after creation");
m->path_mkdir.error++;
return 0;
}
}

long path_copy_len = bpf_probe_read_str(mkdir_ctx->path, PATH_MAX, path->path);
if (path_copy_len < 0) {
bpf_printk("Failed to copy path string");
m->path_mkdir.error++;
bpf_map_delete_elem(&mkdir_context, &pid_tgid);
return 0;
}
mkdir_ctx->parent_inode = parent_inode;

return 0;
}

SEC("lsm/d_instantiate")
int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) {
struct metrics_t* m = get_metrics();
if (m == NULL) {
return 0;
}

m->d_instantiate.total++;

__u64 pid_tgid = bpf_get_current_pid_tgid();

if (inode == NULL) {
m->d_instantiate.ignored++;
goto cleanup;
}

struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid);

if (mkdir_ctx == NULL) {
m->d_instantiate.ignored++;
return 0;
}

inode_key_t inode_key = inode_to_key(inode);

if (inode_add(&inode_key) == 0) {
m->d_instantiate.added++;
} else {
m->d_instantiate.error++;
}

submit_mkdir_event(&m->d_instantiate,
mkdir_ctx->path,
&inode_key,
&mkdir_ctx->parent_inode);

cleanup:
bpf_map_delete_elem(&mkdir_context, &pid_tgid);
return 0;
}
7 changes: 7 additions & 0 deletions fact-ebpf/src/bpf/maps.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ struct {
__uint(map_flags, BPF_F_NO_PREALLOC);
} inode_map SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, __u64);
__type(value, struct mkdir_context_t);
__uint(max_entries, 16384);
} mkdir_context SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, __u32);
Expand Down
9 changes: 9 additions & 0 deletions fact-ebpf/src/bpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ typedef enum file_activity_type_t {
FILE_ACTIVITY_CHMOD,
FILE_ACTIVITY_CHOWN,
FILE_ACTIVITY_RENAME,
DIR_ACTIVITY_CREATION,
} file_activity_type_t;

struct event_t {
Expand Down Expand Up @@ -96,6 +97,12 @@ struct path_prefix_t {
const char path[LPM_SIZE_MAX];
};

// Context for correlating mkdir operations
struct mkdir_context_t {
char path[PATH_MAX];
inode_key_t parent_inode;
};

// Metrics types
struct metrics_by_hook_t {
unsigned long long total;
Expand All @@ -111,4 +118,6 @@ struct metrics_t {
struct metrics_by_hook_t path_chmod;
struct metrics_by_hook_t path_chown;
struct metrics_by_hook_t path_rename;
struct metrics_by_hook_t path_mkdir;
struct metrics_by_hook_t d_instantiate;
};
2 changes: 2 additions & 0 deletions fact-ebpf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ impl metrics_t {
m.path_chmod = m.path_chmod.accumulate(&other.path_chmod);
m.path_chown = m.path_chown.accumulate(&other.path_chown);
m.path_rename = m.path_rename.accumulate(&other.path_rename);
m.path_mkdir = m.path_mkdir.accumulate(&other.path_mkdir);
m.d_instantiate = m.d_instantiate.accumulate(&other.d_instantiate);
m
}
}
Expand Down
17 changes: 16 additions & 1 deletion fact/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ impl Event {
}

pub fn is_creation(&self) -> bool {
matches!(self.file, FileData::Creation(_))
matches!(self.file, FileData::Creation(_) | FileData::MkDir(_))
}

pub fn is_mkdir(&self) -> bool {
matches!(self.file, FileData::MkDir(_))
}

pub fn is_unlink(&self) -> bool {
Expand All @@ -143,6 +147,7 @@ impl Event {
match &self.file {
FileData::Open(data) => &data.inode,
FileData::Creation(data) => &data.inode,
FileData::MkDir(data) => &data.inode,
FileData::Unlink(data) => &data.inode,
FileData::Chmod(data) => &data.inner.inode,
FileData::Chown(data) => &data.inner.inode,
Expand All @@ -155,6 +160,7 @@ impl Event {
match &self.file {
FileData::Open(data) => &data.parent_inode,
FileData::Creation(data) => &data.parent_inode,
FileData::MkDir(data) => &data.parent_inode,
FileData::Unlink(data) => &data.parent_inode,
FileData::Chmod(data) => &data.inner.parent_inode,
FileData::Chown(data) => &data.inner.parent_inode,
Expand All @@ -176,6 +182,7 @@ impl Event {
match &self.file {
FileData::Open(data) => &data.filename,
FileData::Creation(data) => &data.filename,
FileData::MkDir(data) => &data.filename,
FileData::Unlink(data) => &data.filename,
FileData::Chmod(data) => &data.inner.filename,
FileData::Chown(data) => &data.inner.filename,
Expand All @@ -194,6 +201,7 @@ impl Event {
match &self.file {
FileData::Open(data) => &data.host_file,
FileData::Creation(data) => &data.host_file,
FileData::MkDir(data) => &data.host_file,
FileData::Unlink(data) => &data.host_file,
FileData::Chmod(data) => &data.inner.host_file,
FileData::Chown(data) => &data.inner.host_file,
Expand All @@ -209,6 +217,7 @@ impl Event {
match &mut self.file {
FileData::Open(data) => data.host_file = host_path,
FileData::Creation(data) => data.host_file = host_path,
FileData::MkDir(data) => data.host_file = host_path,
FileData::Unlink(data) => data.host_file = host_path,
FileData::Chmod(data) => data.inner.host_file = host_path,
FileData::Chown(data) => data.inner.host_file = host_path,
Expand Down Expand Up @@ -293,6 +302,7 @@ impl PartialEq for Event {
pub enum FileData {
Open(BaseFileData),
Creation(BaseFileData),
MkDir(BaseFileData),
Unlink(BaseFileData),
Chmod(ChmodFileData),
Chown(ChownFileData),
Expand All @@ -311,6 +321,7 @@ impl FileData {
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),
file_activity_type_t::DIR_ACTIVITY_CREATION => FileData::MkDir(inner),
file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner),
file_activity_type_t::FILE_ACTIVITY_CHMOD => {
let data = ChmodFileData {
Expand Down Expand Up @@ -359,6 +370,9 @@ impl From<FileData> for fact_api::file_activity::File {
let f_act = fact_api::FileCreation { activity };
fact_api::file_activity::File::Creation(f_act)
}
FileData::MkDir(_) => {
unreachable!("MkDir event reached protobuf conversion");
}
FileData::Unlink(event) => {
let activity = Some(fact_api::FileActivityBase::from(event));
let f_act = fact_api::FileUnlink { activity };
Expand Down Expand Up @@ -386,6 +400,7 @@ impl PartialEq for FileData {
match (self, other) {
(FileData::Open(this), FileData::Open(other)) => this == other,
(FileData::Creation(this), FileData::Creation(other)) => this == other,
(FileData::MkDir(this), FileData::MkDir(other)) => this == other,
(FileData::Unlink(this), FileData::Unlink(other)) => this == other,
(FileData::Chmod(this), FileData::Chmod(other)) => this == other,
(FileData::Rename(this), FileData::Rename(other)) => this == other,
Expand Down
7 changes: 6 additions & 1 deletion fact/src/host_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ impl HostScanner {
};
self.metrics.events.added();

// Handle file creation events by adding new inodes to the map
// Handle file and directory creation events by adding new inodes to the map
if event.is_creation() &&
let Err(e) = self.handle_creation_event(&event) {
warn!("Failed to handle creation event: {e}");
Expand All @@ -301,6 +301,11 @@ impl HostScanner {
self.handle_unlink_event(&event);
}

// Skip directory creation events - we track them internally but don't send to sensor
if event.is_mkdir() {
continue;
}

let event = Arc::new(event);
if let Err(e) = self.tx.send(event) {
self.metrics.events.dropped();
Expand Down
18 changes: 18 additions & 0 deletions fact/src/metrics/kernel_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct KernelMetrics {
path_chmod: EventCounter,
path_chown: EventCounter,
path_rename: EventCounter,
path_mkdir: EventCounter,
d_instantiate: EventCounter,
map: PerCpuArray<MapData, metrics_t>,
}

Expand Down Expand Up @@ -43,19 +45,33 @@ impl KernelMetrics {
"Events processed by the path_rename LSM hook",
&[], // Labels are not needed since `collect` will add them all
);
let path_mkdir = EventCounter::new(
"kernel_path_mkdir_events",
"Events processed by the path_mkdir LSM hook",
&[], // Labels are not needed since `collect` will add them all
);
let d_instantiate = EventCounter::new(
"kernel_d_instantiate_events",
"Events processed by the d_instantiate LSM hook",
&[], // Labels are not needed since `collect` will add them all
);

file_open.register(reg);
path_unlink.register(reg);
path_chmod.register(reg);
path_chown.register(reg);
path_rename.register(reg);
path_mkdir.register(reg);
d_instantiate.register(reg);

KernelMetrics {
file_open,
path_unlink,
path_chmod,
path_chown,
path_rename,
path_mkdir,
d_instantiate,
map: kernel_metrics,
}
}
Expand Down Expand Up @@ -105,6 +121,8 @@ impl KernelMetrics {
KernelMetrics::refresh_labels(&self.path_chmod, &metrics.path_chmod);
KernelMetrics::refresh_labels(&self.path_chown, &metrics.path_chown);
KernelMetrics::refresh_labels(&self.path_rename, &metrics.path_rename);
KernelMetrics::refresh_labels(&self.path_mkdir, &metrics.path_mkdir);
KernelMetrics::refresh_labels(&self.d_instantiate, &metrics.d_instantiate);

Ok(())
}
Expand Down
Loading
Loading