Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2a13742
X-Smart-Branch-Parent: main
JoukoVirtanen Jun 17, 2026
ecb4b18
Added tests
JoukoVirtanen Jun 13, 2026
c8a6272
Added tracking for xattr changes
JoukoVirtanen Jun 13, 2026
f89ab5f
Combined calls to BPF_CORE_READ
JoukoVirtanen Jun 13, 2026
49eaa7d
Clarified comment that the path is not available. Also clarified rele…
JoukoVirtanen Jun 13, 2026
02220c0
Added helper to reduce dry
JoukoVirtanen Jun 14, 2026
0512fae
Added events.h which had been forgotten
JoukoVirtanen Jun 14, 2026
9decf24
Fix format
JoukoVirtanen Jun 14, 2026
f91f5ce
Removed unreachable return
JoukoVirtanen Jun 14, 2026
5706020
Fixed format again
JoukoVirtanen Jun 14, 2026
6a0e437
Renamed test_inode_xattr.py to test_xattr.py
JoukoVirtanen Jun 14, 2026
a7e7566
Tests now check gRPC messages instead of metrics
JoukoVirtanen Jun 17, 2026
5dc6136
Fact now sends xattr events as gRPC messages
JoukoVirtanen Jun 17, 2026
c3ea63b
Added parameterized UTF-8 tests
JoukoVirtanen Jun 17, 2026
333df8c
Test are more forgiving of extra node events
JoukoVirtanen Jun 17, 2026
5fc913d
Validating the path in __submit_event instead of setting it in xattr …
JoukoVirtanen Jun 17, 2026
b7af95d
Added link to XATTR_NAME_MAX
JoukoVirtanen Jun 17, 2026
dd9da9b
Tests ignore events of unexpected type
JoukoVirtanen Jun 17, 2026
ba56735
Updated protobuf definitions
JoukoVirtanen Jun 18, 2026
9b3b13c
Reverted changes to strict event comparison mode
JoukoVirtanen Jun 20, 2026
0462c53
Added xattr changes
JoukoVirtanen Jun 21, 2026
dba763c
Undid changes to tests/test_path_rename.py
JoukoVirtanen Jun 21, 2026
12e54f3
Optionally skipping xattr events
JoukoVirtanen Jun 21, 2026
6309a8d
make format
JoukoVirtanen Jun 21, 2026
71934ae
Add os.removexattr to test_setxattr_multiple
JoukoVirtanen Jun 21, 2026
b9cb83b
Extended test_setxattr_ignored with removexattr and renamed it
JoukoVirtanen Jun 21, 2026
4f517be
Added os.removexattr to test_setxattr_new_file test_xattr_utf8_filena…
JoukoVirtanen Jun 21, 2026
4770b44
Added a test that paramertizes the xattr name
JoukoVirtanen Jun 21, 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
18 changes: 17 additions & 1 deletion fact-ebpf/src/bpf/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ __always_inline static void __submit_event(struct submit_event_args_t* args,
event->monitored = args->monitored;
inode_copy(&event->inode, &args->inode);
inode_copy(&event->parent_inode, &args->parent_inode);
bpf_probe_read_str(event->filename, PATH_MAX, args->filename);
if (args->filename != NULL) {
bpf_probe_read_str(event->filename, PATH_MAX, args->filename);
} else {
event->filename[0] = '\0';
}

struct helper_t* helper = get_helper();
if (helper == NULL) {
Expand Down Expand Up @@ -144,3 +148,15 @@ __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_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 = event_type;
bpf_probe_read_str(args->event->xattr.name, XATTR_NAME_MAX_LEN, xattr_name);

__submit_event(args, false);
}
42 changes: 42 additions & 0 deletions fact-ebpf/src/bpf/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,48 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) {
return 0;
}

__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++;

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;
}

submit_xattr_event(&args, event_type, xattr_name);
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;
}
return handle_xattr(&m->inode_setxattr, dentry, name, FILE_ACTIVITY_SETXATTR);
}

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;
}
return handle_xattr(&m->inode_removexattr, dentry, name, FILE_ACTIVITY_REMOVEXATTR);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

SEC("lsm/path_rmdir")
int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) {
struct metrics_t* m = get_metrics();
Expand Down
11 changes: 11 additions & 0 deletions fact-ebpf/src/bpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

#define LINEAGE_MAX 2

// Matches Linux kernel XATTR_NAME_MAX (255) + null terminator.
// https://github.com/torvalds/linux/blob/66affa37cfac0aec061cc4bcf4a065b0c52f7e19/include/uapi/linux/limits.h#L15
#define XATTR_NAME_MAX_LEN 256

#define LPM_SIZE_MAX 256

typedef struct lineage_t {
Expand Down Expand Up @@ -64,6 +68,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 {
Expand All @@ -90,6 +96,9 @@ struct event_t {
inode_key_t inode;
monitored_t monitored;
} rename;
struct {
char name[XATTR_NAME_MAX_LEN];
} xattr;
};
};

Expand Down Expand Up @@ -132,4 +141,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;
};
2 changes: 2 additions & 0 deletions fact-ebpf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
67 changes: 66 additions & 1 deletion fact/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(_))
}
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -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,
}
}

Expand All @@ -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,
}
}

Expand All @@ -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,
}
}

Expand All @@ -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,
}
}

Expand All @@ -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,
}
}

Expand Down Expand Up @@ -356,6 +374,8 @@ pub enum FileData {
Chmod(ChmodFileData),
Chown(ChownFileData),
Rename(RenameFileData),
SetXattr(XattrFileData),
RemoveXattr(XattrFileData),
}

impl FileData {
Expand Down Expand Up @@ -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:?}"),
};

Expand All @@ -433,6 +465,14 @@ impl From<FileData> for fact_api::file_activity::File {
FileData::RmDir(_) => {
unreachable!("RmDir event reached protobuf conversion");
}
FileData::SetXattr(event) => {
let f_act = fact_api::FileXattrChange::from(event);
fact_api::file_activity::File::XattrSet(f_act)
}
FileData::RemoveXattr(event) => {
let f_act = fact_api::FileXattrChange::from(event);
fact_api::file_activity::File::XattrRemove(f_act)
}
FileData::Unlink(event) => {
let activity = Some(fact_api::FileActivityBase::from(event));
let f_act = fact_api::FileUnlink { activity };
Expand Down Expand Up @@ -465,6 +505,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,
}
}
Expand Down Expand Up @@ -595,6 +637,29 @@ impl PartialEq for RenameFileData {
}
}

#[derive(Debug, Clone, Serialize)]
pub struct XattrFileData {
inner: BaseFileData,
xattr_name: String,
}

impl From<XattrFileData> for fact_api::FileXattrChange {
fn from(value: XattrFileData) -> Self {
let activity = fact_api::FileActivityBase::from(value.inner);
fact_api::FileXattrChange {
activity: Some(activity),
xattr_name: value.xattr_name,
}
}
}

#[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;
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 @@ -16,6 +16,8 @@ pub struct KernelMetrics {
path_mkdir: EventCounter,
path_rmdir: EventCounter,
d_instantiate: EventCounter,
inode_setxattr: EventCounter,
inode_removexattr: EventCounter,
map: PerCpuArray<MapData, metrics_t>,
}

Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -80,6 +94,8 @@ impl KernelMetrics {
path_mkdir,
path_rmdir,
d_instantiate,
inode_setxattr,
inode_removexattr,
map: kernel_metrics,
}
}
Expand Down Expand Up @@ -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(())
}
Expand Down
Loading
Loading