From f27ef4d9abb3e7d86e00476dde85bc18bfa5f7aa Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Fri, 12 Jun 2026 15:45:40 -0700 Subject: [PATCH 01/11] X-Smart-Branch-Parent: main From 0db0d9dceb6117c8c0c014404a2028915c02900f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 13 Jun 2026 10:14:39 -0700 Subject: [PATCH 02/11] Added tests --- tests/test_inode_xattr.py | 199 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 tests/test_inode_xattr.py diff --git a/tests/test_inode_xattr.py b/tests/test_inode_xattr.py new file mode 100644 index 00000000..366d2125 --- /dev/null +++ b/tests/test_inode_xattr.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import os + +from event import Event, EventType, Process +from server import FileActivityService +from utils import get_metric_value + + +def get_kernel_setxattr_added(fact_config: tuple[dict, str]): + """ + Query Prometheus metrics to get the count of setxattr events + added to the ring buffer. + + Args: + fact_config: The fact configuration tuple + (config dict, config file path). + + Returns: + The current value of + kernel_inode_setxattr_events{label="Added"} metric. + """ + value = get_metric_value( + fact_config, + 'kernel_inode_setxattr_events', + {'label': 'Added'}, + ) + return int(value) if value is not None else 0 + + +def get_kernel_removexattr_added(fact_config: tuple[dict, str]): + """ + Query Prometheus metrics to get the count of removexattr events + added to the ring buffer. + + Args: + fact_config: The fact configuration tuple + (config dict, config file path). + + Returns: + The current value of + kernel_inode_removexattr_events{label="Added"} metric. + """ + value = get_metric_value( + fact_config, + 'kernel_inode_removexattr_events', + {'label': 'Added'}, + ) + return int(value) if value is not None else 0 + + +def test_setxattr( + test_file: str, + fact_config: tuple[dict, str], +): + """ + Tests that setting a user xattr on a monitored file is tracked + via kernel metrics. + + The test_file fixture creates a file before fact starts, so it is + picked up by the initial scan and its inode is already tracked. + + Args: + test_file: File monitored on the host. + fact_config: The fact configuration. + """ + initial = get_kernel_setxattr_added(fact_config) + + os.setxattr(test_file, 'user.fact_test', b'test_value') + + final = get_kernel_setxattr_added(fact_config) + delta = final - initial + assert delta == 1, f'Expected exactly 1 setxattr event added, got {delta}' + + +def test_removexattr( + test_file: str, + fact_config: tuple[dict, str], +): + """ + Tests that removing a user xattr from a monitored file is tracked + via kernel metrics. + + Args: + test_file: File monitored on the host. + fact_config: The fact configuration. + """ + os.setxattr(test_file, 'user.fact_remove', b'to_remove') + + initial = get_kernel_removexattr_added(fact_config) + + os.removexattr(test_file, 'user.fact_remove') + + final = get_kernel_removexattr_added(fact_config) + delta = final - initial + assert delta == 1, ( + f'Expected exactly 1 removexattr event added, got {delta}' + ) + + +def test_setxattr_multiple( + test_file: str, + fact_config: tuple[dict, str], +): + """ + Tests that setting multiple xattrs on a monitored file tracks + all of them. + + Args: + test_file: File monitored on the host. + fact_config: The fact configuration. + """ + initial = get_kernel_setxattr_added(fact_config) + + os.setxattr(test_file, 'user.attr1', b'value1') + os.setxattr(test_file, 'user.attr2', b'value2') + os.setxattr(test_file, 'user.attr3', b'value3') + + final = get_kernel_setxattr_added(fact_config) + delta = final - initial + assert delta == 3, f'Expected exactly 3 setxattr events added, got {delta}' + + +def test_setxattr_ignored( + test_file: str, + ignored_dir: str, + fact_config: tuple[dict, str], +): + """ + Tests that xattr changes on unmonitored files are not tracked, + while xattr changes on monitored files are. + + Args: + test_file: File monitored on the host. + ignored_dir: Temporary directory that is not monitored by fact. + fact_config: The fact configuration. + """ + ignored_file = os.path.join(ignored_dir, 'ignored.txt') + with open(ignored_file, 'w') as f: + f.write('ignored') + + initial = get_kernel_setxattr_added(fact_config) + + os.setxattr(ignored_file, 'user.ignored', b'value') + + after_ignored = get_kernel_setxattr_added(fact_config) + assert after_ignored == initial, ( + 'Setting xattr on ignored file should not increment Added metric' + ) + + os.setxattr(test_file, 'user.monitored', b'value') + + final = get_kernel_setxattr_added(fact_config) + delta = final - initial + assert delta == 1, ( + f'Expected exactly 1 setxattr event (monitored file only), got {delta}' + ) + + +def test_setxattr_new_file( + monitored_dir: str, + server: FileActivityService, + fact_config: tuple[dict, str], +): + """ + Tests that xattr tracking works for files created while fact is + running, not just files from the initial scan. + + A new file is created in the monitored directory and its creation + event is awaited to ensure the inode is tracked before setting + an xattr. + + Args: + monitored_dir: Temporary directory path that is monitored. + server: The server instance to communicate with. + fact_config: The fact configuration. + """ + process = Process.from_proc() + + test_file = os.path.join(monitored_dir, 'xattr_new.txt') + with open(test_file, 'w') as f: + f.write('new file') + + server.wait_events([ + Event( + process=process, + event_type=EventType.CREATION, + file=test_file, + host_path=test_file, + ), + ]) + + initial = get_kernel_setxattr_added(fact_config) + + os.setxattr(test_file, 'user.new_file', b'value') + + final = get_kernel_setxattr_added(fact_config) + delta = final - initial + assert delta == 1, f'Expected exactly 1 setxattr event added, got {delta}' From 1aa7ed05962170511be1be60dea76e9aeef1c617 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 13 Jun 2026 10:18:50 -0700 Subject: [PATCH 03/11] Added tracking for xattr changes --- fact-ebpf/src/bpf/events.h | 22 +++++++++ fact-ebpf/src/bpf/main.c | 74 ++++++++++++++++++++++++++++++ fact-ebpf/src/bpf/types.h | 10 ++++ fact-ebpf/src/lib.rs | 2 + fact/src/event/mod.rs | 55 +++++++++++++++++++++- fact/src/host_scanner.rs | 4 +- fact/src/metrics/kernel_metrics.rs | 18 ++++++++ 7 files changed, 182 insertions(+), 3 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index ac1d1bd8..c0ccdd3a 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -144,3 +144,25 @@ __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_setxattr_event(struct submit_event_args_t* args, + const char* xattr_name) { + if (!reserve_event(args)) { + return; + } + args->event->type = FILE_ACTIVITY_SETXATTR; + bpf_probe_read_str(args->event->xattr.name, XATTR_NAME_MAX_LEN, xattr_name); + + __submit_event(args, false); +} + +__always_inline static void submit_removexattr_event(struct submit_event_args_t* args, + const char* xattr_name) { + if (!reserve_event(args)) { + return; + } + args->event->type = FILE_ACTIVITY_REMOVEXATTR; + bpf_probe_read_str(args->event->xattr.name, XATTR_NAME_MAX_LEN, xattr_name); + + __submit_event(args, false); +} diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index eb2033e8..e4d3c66a 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -389,6 +389,80 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { return 0; } +SEC("lsm/inode_setxattr") +int BPF_PROG(trace_inode_setxattr, struct mnt_idmap* idmap, struct dentry* dentry, + const char* name, const void* value, size_t size, int flags) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + struct submit_event_args_t args = {.metrics = &m->inode_setxattr}; + + args.metrics->total++; + + args.inode = inode_to_key(dentry->d_inode); + + struct dentry* parent_dentry = BPF_CORE_READ(dentry, d_parent); + 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 = inode_is_monitored(inode_get(&args.inode), inode_get(&args.parent_inode)); + + if (args.monitored == NOT_MONITORED) { + args.metrics->ignored++; + return 0; + } + + // Path is resolved in userspace from the inode map + struct bound_path_t* path = get_bound_path(BOUND_PATH_MAIN); + if (path == NULL) { + args.metrics->error++; + return 0; + } + path->path[0] = '\0'; + args.filename = path->path; + + submit_setxattr_event(&args, name); + return 0; +} + +SEC("lsm/inode_removexattr") +int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* dentry, + const char* name) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + struct submit_event_args_t args = {.metrics = &m->inode_removexattr}; + + args.metrics->total++; + + args.inode = inode_to_key(dentry->d_inode); + + struct dentry* parent_dentry = BPF_CORE_READ(dentry, d_parent); + 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 = inode_is_monitored(inode_get(&args.inode), inode_get(&args.parent_inode)); + + if (args.monitored == NOT_MONITORED) { + args.metrics->ignored++; + return 0; + } + + // Path is resolved in userspace from the inode map + struct bound_path_t* path = get_bound_path(BOUND_PATH_MAIN); + if (path == NULL) { + args.metrics->error++; + return 0; + } + path->path[0] = '\0'; + args.filename = path->path; + + submit_removexattr_event(&args, name); + return 0; +} + SEC("lsm/path_rmdir") int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { struct metrics_t* m = get_metrics(); diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 2f11c0db..d7f8a21f 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -15,6 +15,9 @@ #define LINEAGE_MAX 2 +// Matches Linux kernel XATTR_NAME_MAX (255) + null terminator +#define XATTR_NAME_MAX_LEN 256 + #define LPM_SIZE_MAX 256 typedef struct lineage_t { @@ -64,6 +67,8 @@ typedef enum file_activity_type_t { FILE_ACTIVITY_RENAME, DIR_ACTIVITY_CREATION, DIR_ACTIVITY_UNLINK, + FILE_ACTIVITY_SETXATTR, + FILE_ACTIVITY_REMOVEXATTR, } file_activity_type_t; struct event_t { @@ -90,6 +95,9 @@ struct event_t { inode_key_t inode; monitored_t monitored; } rename; + struct { + char name[XATTR_NAME_MAX_LEN]; + } xattr; }; }; @@ -132,4 +140,6 @@ 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_setxattr; + struct metrics_by_hook_t inode_removexattr; }; diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index f38fe801..a1dd046a 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -145,6 +145,8 @@ 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_setxattr = self.inode_setxattr.accumulate(&other.inode_setxattr); + self.inode_removexattr = self.inode_removexattr.accumulate(&other.inode_removexattr); self } } diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 7944ada3..3a51d07e 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -9,7 +9,9 @@ use std::{ use globset::GlobSet; use serde::Serialize; -use fact_ebpf::{PATH_MAX, event_t, file_activity_type_t, inode_key_t, monitored_t}; +use fact_ebpf::{ + PATH_MAX, XATTR_NAME_MAX_LEN, event_t, file_activity_type_t, inode_key_t, monitored_t, +}; use crate::host_info; use process::Process; @@ -131,6 +133,10 @@ impl Event { matches!(self.file, FileData::Creation(_) | FileData::MkDir(_)) } + pub fn is_xattr(&self) -> bool { + matches!(self.file, FileData::SetXattr(_) | FileData::RemoveXattr(_)) + } + pub fn is_mkdir(&self) -> bool { matches!(self.file, FileData::MkDir(_)) } @@ -162,6 +168,8 @@ impl Event { FileData::Chmod(data) => &data.inner.inode, FileData::Chown(data) => &data.inner.inode, FileData::Rename(data) => &data.new.inode, + FileData::SetXattr(data) => &data.inner.inode, + FileData::RemoveXattr(data) => &data.inner.inode, } } @@ -176,6 +184,8 @@ impl Event { FileData::Chmod(data) => &data.inner.parent_inode, FileData::Chown(data) => &data.inner.parent_inode, FileData::Rename(data) => &data.new.parent_inode, + FileData::SetXattr(data) => &data.inner.parent_inode, + FileData::RemoveXattr(data) => &data.inner.parent_inode, } } @@ -199,6 +209,8 @@ impl Event { FileData::Chmod(data) => &data.inner.filename, FileData::Chown(data) => &data.inner.filename, FileData::Rename(data) => &data.new.filename, + FileData::SetXattr(data) => &data.inner.filename, + FileData::RemoveXattr(data) => &data.inner.filename, } } @@ -219,6 +231,8 @@ impl Event { FileData::Chmod(data) => &data.inner.host_file, FileData::Chown(data) => &data.inner.host_file, FileData::Rename(data) => &data.new.host_file, + FileData::SetXattr(data) => &data.inner.host_file, + FileData::RemoveXattr(data) => &data.inner.host_file, } } @@ -243,6 +257,8 @@ impl Event { FileData::Chmod(data) => data.inner.host_file = host_path, FileData::Chown(data) => data.inner.host_file = host_path, FileData::Rename(data) => data.new.host_file = host_path, + FileData::SetXattr(data) => data.inner.host_file = host_path, + FileData::RemoveXattr(data) => data.inner.host_file = host_path, } } @@ -264,6 +280,8 @@ impl Event { FileData::Chmod(data) => data.inner.monitored, FileData::Chown(data) => data.inner.monitored, FileData::Rename(data) => data.new.monitored, + FileData::SetXattr(data) => data.inner.monitored, + FileData::RemoveXattr(data) => data.inner.monitored, } } @@ -356,6 +374,8 @@ pub enum FileData { Chmod(ChmodFileData), Chown(ChownFileData), Rename(RenameFileData), + SetXattr(XattrFileData), + RemoveXattr(XattrFileData), } impl FileData { @@ -407,6 +427,18 @@ impl FileData { }; FileData::Rename(data) } + file_activity_type_t::FILE_ACTIVITY_SETXATTR => { + let xattr_name = slice_to_string( + &unsafe { extra_data.xattr }.name[..XATTR_NAME_MAX_LEN as usize], + )?; + FileData::SetXattr(XattrFileData { inner, xattr_name }) + } + file_activity_type_t::FILE_ACTIVITY_REMOVEXATTR => { + let xattr_name = slice_to_string( + &unsafe { extra_data.xattr }.name[..XATTR_NAME_MAX_LEN as usize], + )?; + FileData::RemoveXattr(XattrFileData { inner, xattr_name }) + } invalid => unreachable!("Invalid event type: {invalid:?}"), }; @@ -433,6 +465,12 @@ impl From for fact_api::file_activity::File { FileData::RmDir(_) => { unreachable!("RmDir event reached protobuf conversion"); } + FileData::SetXattr(_) => { + unreachable!("SetXattr event reached protobuf conversion"); + } + FileData::RemoveXattr(_) => { + unreachable!("RemoveXattr event reached protobuf conversion"); + } FileData::Unlink(event) => { let activity = Some(fact_api::FileActivityBase::from(event)); let f_act = fact_api::FileUnlink { activity }; @@ -465,6 +503,8 @@ impl PartialEq for FileData { (FileData::Unlink(this), FileData::Unlink(other)) => this == other, (FileData::Chmod(this), FileData::Chmod(other)) => this == other, (FileData::Rename(this), FileData::Rename(other)) => this == other, + (FileData::SetXattr(this), FileData::SetXattr(other)) => this == other, + (FileData::RemoveXattr(this), FileData::RemoveXattr(other)) => this == other, _ => false, } } @@ -595,6 +635,19 @@ impl PartialEq for RenameFileData { } } +#[derive(Debug, Clone, Serialize)] +pub struct XattrFileData { + inner: BaseFileData, + xattr_name: String, +} + +#[cfg(test)] +impl PartialEq for XattrFileData { + fn eq(&self, other: &Self) -> bool { + self.xattr_name == other.xattr_name && self.inner == other.inner + } +} + #[cfg(test)] mod test_utils { use std::os::raw::c_char; diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 273bd5df..2b4b8585 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -452,8 +452,8 @@ You can increase this limit with: self.handle_unlink_event(&event); } - // Skip directory creation and deletion events - we track them internally but don't send to sensor - if event.is_mkdir() || event.is_rmdir() { + // Skip events that are tracked internally but not yet sent to sensor + if event.is_mkdir() || event.is_rmdir() || event.is_xattr() { continue; } diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index 15da3993..df51f432 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -16,6 +16,8 @@ pub struct KernelMetrics { path_mkdir: EventCounter, path_rmdir: EventCounter, d_instantiate: EventCounter, + inode_setxattr: EventCounter, + inode_removexattr: EventCounter, map: PerCpuArray, } @@ -61,6 +63,16 @@ impl KernelMetrics { "Events processed by the d_instantiate LSM hook", &[], // Labels are not needed since `collect` will add them all ); + let inode_setxattr = EventCounter::new( + "kernel_inode_setxattr_events", + "Events processed by the inode_setxattr LSM hook", + &[], // Labels are not needed since `collect` will add them all + ); + let inode_removexattr = EventCounter::new( + "kernel_inode_removexattr_events", + "Events processed by the inode_removexattr LSM hook", + &[], // Labels are not needed since `collect` will add them all + ); file_open.register(reg); path_unlink.register(reg); @@ -70,6 +82,8 @@ impl KernelMetrics { path_mkdir.register(reg); path_rmdir.register(reg); d_instantiate.register(reg); + inode_setxattr.register(reg); + inode_removexattr.register(reg); KernelMetrics { file_open, @@ -80,6 +94,8 @@ impl KernelMetrics { path_mkdir, path_rmdir, d_instantiate, + inode_setxattr, + inode_removexattr, map: kernel_metrics, } } @@ -132,6 +148,8 @@ impl KernelMetrics { KernelMetrics::refresh_labels(&self.path_mkdir, &metrics.path_mkdir); KernelMetrics::refresh_labels(&self.path_rmdir, &metrics.path_rmdir); KernelMetrics::refresh_labels(&self.d_instantiate, &metrics.d_instantiate); + KernelMetrics::refresh_labels(&self.inode_setxattr, &metrics.inode_setxattr); + KernelMetrics::refresh_labels(&self.inode_removexattr, &metrics.inode_removexattr); Ok(()) } From 1a2bbdd5c105aa94e76b3d38ff6cb8fb96e93909 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 13 Jun 2026 10:56:23 -0700 Subject: [PATCH 04/11] Combined calls to BPF_CORE_READ --- fact-ebpf/src/bpf/main.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index e4d3c66a..adb1d2b6 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -401,10 +401,7 @@ int BPF_PROG(trace_inode_setxattr, struct mnt_idmap* idmap, struct dentry* dentr args.metrics->total++; args.inode = inode_to_key(dentry->d_inode); - - struct dentry* parent_dentry = BPF_CORE_READ(dentry, d_parent); - 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.parent_inode = inode_to_key(BPF_CORE_READ(dentry, d_parent, d_inode)); args.monitored = inode_is_monitored(inode_get(&args.inode), inode_get(&args.parent_inode)); @@ -438,10 +435,7 @@ int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* de args.metrics->total++; args.inode = inode_to_key(dentry->d_inode); - - struct dentry* parent_dentry = BPF_CORE_READ(dentry, d_parent); - 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.parent_inode = inode_to_key(BPF_CORE_READ(dentry, d_parent, d_inode)); args.monitored = inode_is_monitored(inode_get(&args.inode), inode_get(&args.parent_inode)); From ceb572752a22aa865fa7b8a3d3304282a8dd4e5d Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sat, 13 Jun 2026 11:36:51 -0700 Subject: [PATCH 05/11] Clarified comment that the path is not available. Also clarified relevant code --- fact-ebpf/src/bpf/main.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index adb1d2b6..9de3a74b 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -410,14 +410,15 @@ int BPF_PROG(trace_inode_setxattr, struct mnt_idmap* idmap, struct dentry* dentr return 0; } - // Path is resolved in userspace from the inode map - struct bound_path_t* path = get_bound_path(BOUND_PATH_MAIN); - if (path == NULL) { + // inode hooks don't provide a struct path, so filename is left empty. + // __submit_event requires a valid pointer for bpf_probe_read_str. + struct bound_path_t* bound_path = get_bound_path(BOUND_PATH_MAIN); + if (bound_path == NULL) { args.metrics->error++; return 0; } - path->path[0] = '\0'; - args.filename = path->path; + bound_path->path[0] = '\0'; + args.filename = bound_path->path; submit_setxattr_event(&args, name); return 0; @@ -444,14 +445,15 @@ int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* de return 0; } - // Path is resolved in userspace from the inode map - struct bound_path_t* path = get_bound_path(BOUND_PATH_MAIN); - if (path == NULL) { + // inode hooks don't provide a struct path, so filename is left empty. + // __submit_event requires a valid pointer for bpf_probe_read_str. + struct bound_path_t* bound_path = get_bound_path(BOUND_PATH_MAIN); + if (bound_path == NULL) { args.metrics->error++; return 0; } - path->path[0] = '\0'; - args.filename = path->path; + bound_path->path[0] = '\0'; + args.filename = bound_path->path; submit_removexattr_event(&args, name); return 0; From 0d13bc10b972017354923c2187d6b0e7124aa51e Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 14 Jun 2026 10:53:53 -0700 Subject: [PATCH 06/11] Added helper to reduce dry --- fact-ebpf/src/bpf/main.c | 51 ++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 9de3a74b..0bd1950c 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -389,14 +389,11 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) { return 0; } -SEC("lsm/inode_setxattr") -int BPF_PROG(trace_inode_setxattr, struct mnt_idmap* idmap, struct dentry* dentry, - const char* name, const void* value, size_t size, int flags) { - struct metrics_t* m = get_metrics(); - if (m == NULL) { - return 0; - } - struct submit_event_args_t args = {.metrics = &m->inode_setxattr}; +__always_inline static int handle_xattr(struct metrics_by_hook_t* hook_metrics, + struct dentry* dentry, + const char* xattr_name, + file_activity_type_t event_type) { + struct submit_event_args_t args = {.metrics = hook_metrics}; args.metrics->total++; @@ -420,42 +417,28 @@ int BPF_PROG(trace_inode_setxattr, struct mnt_idmap* idmap, struct dentry* dentr bound_path->path[0] = '\0'; args.filename = bound_path->path; - submit_setxattr_event(&args, name); + submit_xattr_event(&args, event_type, xattr_name); return 0; } -SEC("lsm/inode_removexattr") -int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* dentry, - const char* name) { +SEC("lsm/inode_setxattr") +int BPF_PROG(trace_inode_setxattr, struct mnt_idmap* idmap, struct dentry* dentry, + const char* name, const void* value, size_t size, int flags) { struct metrics_t* m = get_metrics(); if (m == NULL) { return 0; } - struct submit_event_args_t args = {.metrics = &m->inode_removexattr}; - - args.metrics->total++; - - args.inode = inode_to_key(dentry->d_inode); - args.parent_inode = inode_to_key(BPF_CORE_READ(dentry, d_parent, d_inode)); - - args.monitored = inode_is_monitored(inode_get(&args.inode), inode_get(&args.parent_inode)); - - if (args.monitored == NOT_MONITORED) { - args.metrics->ignored++; - return 0; - } + return handle_xattr(&m->inode_setxattr, dentry, name, FILE_ACTIVITY_SETXATTR); +} - // inode hooks don't provide a struct path, so filename is left empty. - // __submit_event requires a valid pointer for bpf_probe_read_str. - struct bound_path_t* bound_path = get_bound_path(BOUND_PATH_MAIN); - if (bound_path == NULL) { - args.metrics->error++; +SEC("lsm/inode_removexattr") +int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* dentry, + const char* name) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { return 0; } - bound_path->path[0] = '\0'; - args.filename = bound_path->path; - - submit_removexattr_event(&args, name); + return handle_xattr(&m->inode_removexattr, dentry, name, FILE_ACTIVITY_REMOVEXATTR); return 0; } From 9a0b65ed76425d54b7bf6723409d7fd315c9755f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 14 Jun 2026 10:55:25 -0700 Subject: [PATCH 07/11] Added events.h which had been forgotten --- fact-ebpf/src/bpf/events.h | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index c0ccdd3a..396cf0d4 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -145,23 +145,13 @@ __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_setxattr_event(struct submit_event_args_t* args, - const char* xattr_name) { +__always_inline static void submit_xattr_event(struct submit_event_args_t* args, + file_activity_type_t event_type, + const char* xattr_name) { if (!reserve_event(args)) { return; } - args->event->type = FILE_ACTIVITY_SETXATTR; - bpf_probe_read_str(args->event->xattr.name, XATTR_NAME_MAX_LEN, xattr_name); - - __submit_event(args, false); -} - -__always_inline static void submit_removexattr_event(struct submit_event_args_t* args, - const char* xattr_name) { - if (!reserve_event(args)) { - return; - } - args->event->type = FILE_ACTIVITY_REMOVEXATTR; + args->event->type = event_type; bpf_probe_read_str(args->event->xattr.name, XATTR_NAME_MAX_LEN, xattr_name); __submit_event(args, false); From b2863462fc0bd898a614f37b7598fbdc89d8b32a Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 14 Jun 2026 11:06:29 -0700 Subject: [PATCH 08/11] Fix format --- fact-ebpf/src/bpf/events.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 396cf0d4..1d2e158e 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -146,8 +146,8 @@ __always_inline static void submit_rmdir_event(struct submit_event_args_t* args) } __always_inline static void submit_xattr_event(struct submit_event_args_t* args, - file_activity_type_t event_type, - const char* xattr_name) { + file_activity_type_t event_type, + const char* xattr_name) { if (!reserve_event(args)) { return; } From bd38c3d7d2c7bd7e2da3a4741b5cd58aacfe29a6 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 14 Jun 2026 11:08:13 -0700 Subject: [PATCH 09/11] Removed unreachable return --- fact-ebpf/src/bpf/main.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 0bd1950c..2b8e2ed8 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -439,7 +439,6 @@ int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* de return 0; } return handle_xattr(&m->inode_removexattr, dentry, name, FILE_ACTIVITY_REMOVEXATTR); - return 0; } SEC("lsm/path_rmdir") From 7743c69c806307dab6d737886990c530f6bdb79f Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 14 Jun 2026 11:16:21 -0700 Subject: [PATCH 10/11] Fixed format again --- tests/test_inode_xattr.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_inode_xattr.py b/tests/test_inode_xattr.py index 366d2125..4f37ebdd 100644 --- a/tests/test_inode_xattr.py +++ b/tests/test_inode_xattr.py @@ -181,14 +181,16 @@ def test_setxattr_new_file( with open(test_file, 'w') as f: f.write('new file') - server.wait_events([ - Event( - process=process, - event_type=EventType.CREATION, - file=test_file, - host_path=test_file, - ), - ]) + server.wait_events( + [ + Event( + process=process, + event_type=EventType.CREATION, + file=test_file, + host_path=test_file, + ), + ] + ) initial = get_kernel_setxattr_added(fact_config) From 7b389c611cdd25fa3704c78128edb8b228aec12d Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Sun, 14 Jun 2026 12:00:03 -0700 Subject: [PATCH 11/11] Renamed test_inode_xattr.py to test_xattr.py --- tests/{test_inode_xattr.py => test_xattr.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_inode_xattr.py => test_xattr.py} (100%) diff --git a/tests/test_inode_xattr.py b/tests/test_xattr.py similarity index 100% rename from tests/test_inode_xattr.py rename to tests/test_xattr.py