Skip to content

Commit a6ddb60

Browse files
ROX-33034: Track xattr changes (#790)
1 parent 8807e7e commit a6ddb60

9 files changed

Lines changed: 596 additions & 5 deletions

File tree

fact-ebpf/src/bpf/events.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ __always_inline static void __submit_event(struct submit_event_args_t* args,
3737
event->monitored = args->monitored;
3838
inode_copy(&event->inode, &args->inode);
3939
inode_copy(&event->parent_inode, &args->parent_inode);
40-
bpf_probe_read_str(event->filename, PATH_MAX, args->filename);
40+
if (args->filename != NULL) {
41+
bpf_probe_read_str(event->filename, PATH_MAX, args->filename);
42+
} else {
43+
event->filename[0] = '\0';
44+
}
4145

4246
struct helper_t* helper = get_helper();
4347
if (helper == NULL) {
@@ -144,3 +148,15 @@ __always_inline static void submit_rmdir_event(struct submit_event_args_t* args)
144148

145149
__submit_event(args, path_hooks_support_bpf_d_path);
146150
}
151+
152+
__always_inline static void submit_xattr_event(struct submit_event_args_t* args,
153+
file_activity_type_t event_type,
154+
const char* xattr_name) {
155+
if (!reserve_event(args)) {
156+
return;
157+
}
158+
args->event->type = event_type;
159+
bpf_probe_read_str(args->event->xattr.name, XATTR_NAME_MAX_LEN, xattr_name);
160+
161+
__submit_event(args, false);
162+
}

fact-ebpf/src/bpf/main.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,48 @@ int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) {
389389
return 0;
390390
}
391391

392+
__always_inline static int handle_xattr(struct metrics_by_hook_t* hook_metrics,
393+
struct dentry* dentry,
394+
const char* xattr_name,
395+
file_activity_type_t event_type) {
396+
struct submit_event_args_t args = {.metrics = hook_metrics};
397+
398+
args.metrics->total++;
399+
400+
args.inode = inode_to_key(dentry->d_inode);
401+
args.parent_inode = inode_to_key(BPF_CORE_READ(dentry, d_parent, d_inode));
402+
403+
args.monitored = inode_is_monitored(inode_get(&args.inode), inode_get(&args.parent_inode));
404+
405+
if (args.monitored == NOT_MONITORED) {
406+
args.metrics->ignored++;
407+
return 0;
408+
}
409+
410+
submit_xattr_event(&args, event_type, xattr_name);
411+
return 0;
412+
}
413+
414+
SEC("lsm/inode_setxattr")
415+
int BPF_PROG(trace_inode_setxattr, struct mnt_idmap* idmap, struct dentry* dentry,
416+
const char* name, const void* value, size_t size, int flags) {
417+
struct metrics_t* m = get_metrics();
418+
if (m == NULL) {
419+
return 0;
420+
}
421+
return handle_xattr(&m->inode_setxattr, dentry, name, FILE_ACTIVITY_SETXATTR);
422+
}
423+
424+
SEC("lsm/inode_removexattr")
425+
int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* dentry,
426+
const char* name) {
427+
struct metrics_t* m = get_metrics();
428+
if (m == NULL) {
429+
return 0;
430+
}
431+
return handle_xattr(&m->inode_removexattr, dentry, name, FILE_ACTIVITY_REMOVEXATTR);
432+
}
433+
392434
SEC("lsm/path_rmdir")
393435
int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) {
394436
struct metrics_t* m = get_metrics();

fact-ebpf/src/bpf/types.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
#define LINEAGE_MAX 2
1717

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

2024
typedef struct lineage_t {
@@ -64,6 +68,8 @@ typedef enum file_activity_type_t {
6468
FILE_ACTIVITY_RENAME,
6569
DIR_ACTIVITY_CREATION,
6670
DIR_ACTIVITY_UNLINK,
71+
FILE_ACTIVITY_SETXATTR,
72+
FILE_ACTIVITY_REMOVEXATTR,
6773
} file_activity_type_t;
6874

6975
struct event_t {
@@ -90,6 +96,9 @@ struct event_t {
9096
inode_key_t inode;
9197
monitored_t monitored;
9298
} rename;
99+
struct {
100+
char name[XATTR_NAME_MAX_LEN];
101+
} xattr;
93102
};
94103
};
95104

@@ -132,4 +141,6 @@ struct metrics_t {
132141
struct metrics_by_hook_t path_mkdir;
133142
struct metrics_by_hook_t d_instantiate;
134143
struct metrics_by_hook_t path_rmdir;
144+
struct metrics_by_hook_t inode_setxattr;
145+
struct metrics_by_hook_t inode_removexattr;
135146
};

fact/src/event/mod.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ use std::{
99
use globset::GlobSet;
1010
use serde::Serialize;
1111

12-
use fact_ebpf::{PATH_MAX, event_t, file_activity_type_t, inode_key_t, monitored_t};
12+
use fact_ebpf::{
13+
PATH_MAX, XATTR_NAME_MAX_LEN, event_t, file_activity_type_t, inode_key_t, monitored_t,
14+
};
1315

1416
use crate::host_info;
1517
use process::Process;
@@ -131,6 +133,10 @@ impl Event {
131133
matches!(self.file, FileData::Creation(_) | FileData::MkDir(_))
132134
}
133135

136+
pub fn is_xattr(&self) -> bool {
137+
matches!(self.file, FileData::SetXattr(_) | FileData::RemoveXattr(_))
138+
}
139+
134140
pub fn is_mkdir(&self) -> bool {
135141
matches!(self.file, FileData::MkDir(_))
136142
}
@@ -162,6 +168,8 @@ impl Event {
162168
FileData::Chmod(data) => &data.inner.inode,
163169
FileData::Chown(data) => &data.inner.inode,
164170
FileData::Rename(data) => &data.new.inode,
171+
FileData::SetXattr(data) => &data.inner.inode,
172+
FileData::RemoveXattr(data) => &data.inner.inode,
165173
}
166174
}
167175

@@ -176,6 +184,8 @@ impl Event {
176184
FileData::Chmod(data) => &data.inner.parent_inode,
177185
FileData::Chown(data) => &data.inner.parent_inode,
178186
FileData::Rename(data) => &data.new.parent_inode,
187+
FileData::SetXattr(data) => &data.inner.parent_inode,
188+
FileData::RemoveXattr(data) => &data.inner.parent_inode,
179189
}
180190
}
181191

@@ -199,6 +209,8 @@ impl Event {
199209
FileData::Chmod(data) => &data.inner.filename,
200210
FileData::Chown(data) => &data.inner.filename,
201211
FileData::Rename(data) => &data.new.filename,
212+
FileData::SetXattr(data) => &data.inner.filename,
213+
FileData::RemoveXattr(data) => &data.inner.filename,
202214
}
203215
}
204216

@@ -219,6 +231,8 @@ impl Event {
219231
FileData::Chmod(data) => &data.inner.host_file,
220232
FileData::Chown(data) => &data.inner.host_file,
221233
FileData::Rename(data) => &data.new.host_file,
234+
FileData::SetXattr(data) => &data.inner.host_file,
235+
FileData::RemoveXattr(data) => &data.inner.host_file,
222236
}
223237
}
224238

@@ -243,6 +257,8 @@ impl Event {
243257
FileData::Chmod(data) => data.inner.host_file = host_path,
244258
FileData::Chown(data) => data.inner.host_file = host_path,
245259
FileData::Rename(data) => data.new.host_file = host_path,
260+
FileData::SetXattr(data) => data.inner.host_file = host_path,
261+
FileData::RemoveXattr(data) => data.inner.host_file = host_path,
246262
}
247263
}
248264

@@ -264,6 +280,8 @@ impl Event {
264280
FileData::Chmod(data) => data.inner.monitored,
265281
FileData::Chown(data) => data.inner.monitored,
266282
FileData::Rename(data) => data.new.monitored,
283+
FileData::SetXattr(data) => data.inner.monitored,
284+
FileData::RemoveXattr(data) => data.inner.monitored,
267285
}
268286
}
269287

@@ -356,6 +374,8 @@ pub enum FileData {
356374
Chmod(ChmodFileData),
357375
Chown(ChownFileData),
358376
Rename(RenameFileData),
377+
SetXattr(XattrFileData),
378+
RemoveXattr(XattrFileData),
359379
}
360380

361381
impl FileData {
@@ -407,6 +427,18 @@ impl FileData {
407427
};
408428
FileData::Rename(data)
409429
}
430+
file_activity_type_t::FILE_ACTIVITY_SETXATTR => {
431+
let xattr_name = slice_to_string(
432+
&unsafe { extra_data.xattr }.name[..XATTR_NAME_MAX_LEN as usize],
433+
)?;
434+
FileData::SetXattr(XattrFileData { inner, xattr_name })
435+
}
436+
file_activity_type_t::FILE_ACTIVITY_REMOVEXATTR => {
437+
let xattr_name = slice_to_string(
438+
&unsafe { extra_data.xattr }.name[..XATTR_NAME_MAX_LEN as usize],
439+
)?;
440+
FileData::RemoveXattr(XattrFileData { inner, xattr_name })
441+
}
410442
invalid => unreachable!("Invalid event type: {invalid:?}"),
411443
};
412444

@@ -433,6 +465,14 @@ impl From<FileData> for fact_api::file_activity::File {
433465
FileData::RmDir(_) => {
434466
unreachable!("RmDir event reached protobuf conversion");
435467
}
468+
FileData::SetXattr(event) => {
469+
let f_act = fact_api::FileXattrChange::from(event);
470+
fact_api::file_activity::File::XattrSet(f_act)
471+
}
472+
FileData::RemoveXattr(event) => {
473+
let f_act = fact_api::FileXattrChange::from(event);
474+
fact_api::file_activity::File::XattrRemove(f_act)
475+
}
436476
FileData::Unlink(event) => {
437477
let activity = Some(fact_api::FileActivityBase::from(event));
438478
let f_act = fact_api::FileUnlink { activity };
@@ -465,6 +505,8 @@ impl PartialEq for FileData {
465505
(FileData::Unlink(this), FileData::Unlink(other)) => this == other,
466506
(FileData::Chmod(this), FileData::Chmod(other)) => this == other,
467507
(FileData::Rename(this), FileData::Rename(other)) => this == other,
508+
(FileData::SetXattr(this), FileData::SetXattr(other)) => this == other,
509+
(FileData::RemoveXattr(this), FileData::RemoveXattr(other)) => this == other,
468510
_ => false,
469511
}
470512
}
@@ -595,6 +637,29 @@ impl PartialEq for RenameFileData {
595637
}
596638
}
597639

640+
#[derive(Debug, Clone, Serialize)]
641+
pub struct XattrFileData {
642+
inner: BaseFileData,
643+
xattr_name: String,
644+
}
645+
646+
impl From<XattrFileData> for fact_api::FileXattrChange {
647+
fn from(value: XattrFileData) -> Self {
648+
let activity = fact_api::FileActivityBase::from(value.inner);
649+
fact_api::FileXattrChange {
650+
activity: Some(activity),
651+
xattr_name: value.xattr_name,
652+
}
653+
}
654+
}
655+
656+
#[cfg(test)]
657+
impl PartialEq for XattrFileData {
658+
fn eq(&self, other: &Self) -> bool {
659+
self.xattr_name == other.xattr_name && self.inner == other.inner
660+
}
661+
}
662+
598663
#[cfg(test)]
599664
mod test_utils {
600665
use std::os::raw::c_char;

fact/src/metrics/kernel_metrics.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,6 @@ define_kernel_metrics!(
7070
path_mkdir,
7171
path_rmdir,
7272
d_instantiate,
73+
inode_setxattr,
74+
inode_removexattr,
7375
);

tests/event.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ class EventType(Enum):
4646
PERMISSION = 4
4747
OWNERSHIP = 5
4848
RENAME = 6
49+
XATTR_SET = 7
50+
XATTR_REMOVE = 8
4951

5052

5153
class Process:
@@ -233,6 +235,7 @@ def __init__(
233235
owner_gid: int | None = None,
234236
old_file: str | Pattern[str] | None = None,
235237
old_host_path: str | Pattern[str] | None = None,
238+
xattr_name: str | None = None,
236239
):
237240
self._type: EventType = event_type
238241
self._process: Process = process
@@ -243,6 +246,7 @@ def __init__(
243246
self._owner_gid: int | None = owner_gid
244247
self._old_file: str | Pattern[str] | None = old_file
245248
self._old_host_path: str | Pattern[str] | None = old_host_path
249+
self._xattr_name: str | None = xattr_name
246250

247251
@property
248252
def event_type(self) -> EventType:
@@ -280,6 +284,10 @@ def old_file(self) -> str | Pattern[str] | None:
280284
def old_host_path(self) -> str | Pattern[str] | None:
281285
return self._old_host_path
282286

287+
@property
288+
def xattr_name(self) -> str | None:
289+
return self._xattr_name
290+
283291
@classmethod
284292
def _diff_field(cls, diff: dict, name: str, expected: Any, actual: Any):
285293
if expected != actual:
@@ -388,6 +396,13 @@ def diff(self, other: FileActivity) -> dict | None:
388396
self.owner_gid,
389397
event_field.gid,
390398
)
399+
elif self.event_type in (EventType.XATTR_SET, EventType.XATTR_REMOVE):
400+
Event._diff_field(
401+
diff,
402+
'xattr_name',
403+
self.xattr_name,
404+
event_field.xattr_name,
405+
)
391406

392407
return diff if diff else None
393408

@@ -411,6 +426,19 @@ def __str__(self) -> str:
411426
f', old_host_path="{self.old_host_path}"'
412427
)
413428

429+
if self.event_type in (EventType.XATTR_SET, EventType.XATTR_REMOVE):
430+
s += f', xattr_name="{self.xattr_name}"'
431+
414432
s += ')'
415433

416434
return s
435+
436+
437+
def selinux_xattr(process: Process, host_path: str = '') -> Event:
438+
return Event(
439+
process=process,
440+
event_type=EventType.XATTR_SET,
441+
file='',
442+
host_path=host_path,
443+
xattr_name='security.selinux',
444+
)

tests/server.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def _wait_events(
9292
self,
9393
events: list[Event],
9494
strict: bool,
95+
skip_xattr: bool,
9596
cancel: ThreadingEvent,
9697
):
9798
while self.is_running() and not cancel.is_set():
@@ -102,6 +103,12 @@ def _wait_events(
102103

103104
print(f'Got event: {msg}')
104105

106+
if skip_xattr and msg.WhichOneof('file') in (
107+
'xattr_set',
108+
'xattr_remove',
109+
):
110+
continue
111+
105112
# Check if msg matches the next expected event
106113
diff = events[0].diff(msg)
107114
if diff is None:
@@ -111,7 +118,12 @@ def _wait_events(
111118
elif strict:
112119
raise ValueError(json.dumps(diff, indent=4))
113120

114-
def wait_events(self, events: list[Event], strict: bool = True):
121+
def wait_events(
122+
self,
123+
events: list[Event],
124+
strict: bool = True,
125+
skip_xattr: bool = True,
126+
):
115127
"""
116128
Continuously checks the server for incoming events until the
117129
specified events are found.
@@ -125,7 +137,13 @@ def wait_events(self, events: list[Event], strict: bool = True):
125137
"""
126138
print('Waiting for events:', *events, sep='\n')
127139
cancel = ThreadingEvent()
128-
fs = self.executor.submit(self._wait_events, events, strict, cancel)
140+
fs = self.executor.submit(
141+
self._wait_events,
142+
events,
143+
strict,
144+
skip_xattr,
145+
cancel,
146+
)
129147
try:
130148
fs.result(timeout=5)
131149
except TimeoutError:

0 commit comments

Comments
 (0)