Skip to content

Commit 6b5f77c

Browse files
ROX-33217: Instrument inode tracking on directory being created path mkdir (#465)
1 parent ecf34f5 commit 6b5f77c

File tree

10 files changed

+256
-2
lines changed

10 files changed

+256
-2
lines changed

fact-ebpf/src/bpf/events.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,17 @@ __always_inline static void submit_rename_event(struct metrics_by_hook_t* m,
129129

130130
__submit_event(event, m, FILE_ACTIVITY_RENAME, new_filename, new_inode, new_parent_inode, path_hooks_support_bpf_d_path);
131131
}
132+
133+
__always_inline static void submit_mkdir_event(struct metrics_by_hook_t* m,
134+
const char filename[PATH_MAX],
135+
inode_key_t* inode,
136+
inode_key_t* parent_inode) {
137+
struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0);
138+
if (event == NULL) {
139+
m->ringbuffer_full++;
140+
return;
141+
}
142+
143+
// d_instantiate doesn't support bpf_d_path, so we use false and rely on the stashed path from path_mkdir
144+
__submit_event(event, m, DIR_ACTIVITY_CREATION, filename, inode, parent_inode, false);
145+
}

fact-ebpf/src/bpf/file.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,19 @@ __always_inline static inode_monitored_t is_monitored(inode_key_t inode, struct
4343

4444
return NOT_MONITORED;
4545
}
46+
47+
// Check if a new directory should be tracked based on its parent and path.
48+
// This is used during mkdir operations where the child inode doesn't exist yet.
49+
__always_inline static inode_monitored_t should_track_mkdir(inode_key_t parent_inode, struct bound_path_t* child_path) {
50+
const inode_value_t* volatile parent_value = inode_get(&parent_inode);
51+
52+
if (parent_value != NULL) {
53+
return PARENT_MONITORED;
54+
}
55+
56+
if (path_is_monitored(child_path)) {
57+
return MONITORED;
58+
}
59+
60+
return NOT_MONITORED;
61+
}

fact-ebpf/src/bpf/main.c

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,98 @@ int BPF_PROG(trace_path_rename, struct path* old_dir,
231231
m->path_rename.error++;
232232
return 0;
233233
}
234+
235+
SEC("lsm/path_mkdir")
236+
int BPF_PROG(trace_path_mkdir, struct path* dir, struct dentry* dentry, umode_t mode) {
237+
struct metrics_t* m = get_metrics();
238+
if (m == NULL) {
239+
return 0;
240+
}
241+
242+
m->path_mkdir.total++;
243+
244+
struct bound_path_t* path = path_read_append_d_entry(dir, dentry);
245+
if (path == NULL) {
246+
bpf_printk("Failed to read path");
247+
m->path_mkdir.error++;
248+
return 0;
249+
}
250+
251+
struct inode* parent_inode_ptr = BPF_CORE_READ(dir, dentry, d_inode);
252+
inode_key_t parent_inode = inode_to_key(parent_inode_ptr);
253+
254+
if (should_track_mkdir(parent_inode, path) != PARENT_MONITORED) {
255+
m->path_mkdir.ignored++;
256+
return 0;
257+
}
258+
259+
// Stash mkdir context for security_d_instantiate
260+
__u64 pid_tgid = bpf_get_current_pid_tgid();
261+
struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid);
262+
if (mkdir_ctx == NULL) {
263+
static const struct mkdir_context_t empty_ctx = {0};
264+
if (bpf_map_update_elem(&mkdir_context, &pid_tgid, &empty_ctx, BPF_NOEXIST) != 0) {
265+
bpf_printk("Failed to create mkdir context entry");
266+
m->path_mkdir.error++;
267+
return 0;
268+
}
269+
mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid);
270+
if (mkdir_ctx == NULL) {
271+
bpf_printk("Failed to lookup mkdir context after creation");
272+
m->path_mkdir.error++;
273+
return 0;
274+
}
275+
}
276+
277+
long path_copy_len = bpf_probe_read_str(mkdir_ctx->path, PATH_MAX, path->path);
278+
if (path_copy_len < 0) {
279+
bpf_printk("Failed to copy path string");
280+
m->path_mkdir.error++;
281+
bpf_map_delete_elem(&mkdir_context, &pid_tgid);
282+
return 0;
283+
}
284+
mkdir_ctx->parent_inode = parent_inode;
285+
286+
return 0;
287+
}
288+
289+
SEC("lsm/d_instantiate")
290+
int BPF_PROG(trace_d_instantiate, struct dentry* dentry, struct inode* inode) {
291+
struct metrics_t* m = get_metrics();
292+
if (m == NULL) {
293+
return 0;
294+
}
295+
296+
m->d_instantiate.total++;
297+
298+
__u64 pid_tgid = bpf_get_current_pid_tgid();
299+
300+
if (inode == NULL) {
301+
m->d_instantiate.ignored++;
302+
goto cleanup;
303+
}
304+
305+
struct mkdir_context_t* mkdir_ctx = bpf_map_lookup_elem(&mkdir_context, &pid_tgid);
306+
307+
if (mkdir_ctx == NULL) {
308+
m->d_instantiate.ignored++;
309+
return 0;
310+
}
311+
312+
inode_key_t inode_key = inode_to_key(inode);
313+
314+
if (inode_add(&inode_key) == 0) {
315+
m->d_instantiate.added++;
316+
} else {
317+
m->d_instantiate.error++;
318+
}
319+
320+
submit_mkdir_event(&m->d_instantiate,
321+
mkdir_ctx->path,
322+
&inode_key,
323+
&mkdir_ctx->parent_inode);
324+
325+
cleanup:
326+
bpf_map_delete_elem(&mkdir_context, &pid_tgid);
327+
return 0;
328+
}

fact-ebpf/src/bpf/maps.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ struct {
8383
__uint(map_flags, BPF_F_NO_PREALLOC);
8484
} inode_map SEC(".maps");
8585

86+
struct {
87+
__uint(type, BPF_MAP_TYPE_LRU_HASH);
88+
__type(key, __u64);
89+
__type(value, struct mkdir_context_t);
90+
__uint(max_entries, 16384);
91+
} mkdir_context SEC(".maps");
92+
8693
struct {
8794
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
8895
__type(key, __u32);

fact-ebpf/src/bpf/types.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ typedef enum file_activity_type_t {
5555
FILE_ACTIVITY_CHMOD,
5656
FILE_ACTIVITY_CHOWN,
5757
FILE_ACTIVITY_RENAME,
58+
DIR_ACTIVITY_CREATION,
5859
} file_activity_type_t;
5960

6061
struct event_t {
@@ -96,6 +97,12 @@ struct path_prefix_t {
9697
const char path[LPM_SIZE_MAX];
9798
};
9899

100+
// Context for correlating mkdir operations
101+
struct mkdir_context_t {
102+
char path[PATH_MAX];
103+
inode_key_t parent_inode;
104+
};
105+
99106
// Metrics types
100107
struct metrics_by_hook_t {
101108
unsigned long long total;
@@ -111,4 +118,6 @@ struct metrics_t {
111118
struct metrics_by_hook_t path_chmod;
112119
struct metrics_by_hook_t path_chown;
113120
struct metrics_by_hook_t path_rename;
121+
struct metrics_by_hook_t path_mkdir;
122+
struct metrics_by_hook_t d_instantiate;
114123
};

fact-ebpf/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ impl metrics_t {
125125
m.path_chmod = m.path_chmod.accumulate(&other.path_chmod);
126126
m.path_chown = m.path_chown.accumulate(&other.path_chown);
127127
m.path_rename = m.path_rename.accumulate(&other.path_rename);
128+
m.path_mkdir = m.path_mkdir.accumulate(&other.path_mkdir);
129+
m.d_instantiate = m.d_instantiate.accumulate(&other.d_instantiate);
128130
m
129131
}
130132
}

fact/src/event/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ impl Event {
127127
}
128128

129129
pub fn is_creation(&self) -> bool {
130-
matches!(self.file, FileData::Creation(_))
130+
matches!(self.file, FileData::Creation(_) | FileData::MkDir(_))
131+
}
132+
133+
pub fn is_mkdir(&self) -> bool {
134+
matches!(self.file, FileData::MkDir(_))
131135
}
132136

133137
pub fn is_unlink(&self) -> bool {
@@ -143,6 +147,7 @@ impl Event {
143147
match &self.file {
144148
FileData::Open(data) => &data.inode,
145149
FileData::Creation(data) => &data.inode,
150+
FileData::MkDir(data) => &data.inode,
146151
FileData::Unlink(data) => &data.inode,
147152
FileData::Chmod(data) => &data.inner.inode,
148153
FileData::Chown(data) => &data.inner.inode,
@@ -155,6 +160,7 @@ impl Event {
155160
match &self.file {
156161
FileData::Open(data) => &data.parent_inode,
157162
FileData::Creation(data) => &data.parent_inode,
163+
FileData::MkDir(data) => &data.parent_inode,
158164
FileData::Unlink(data) => &data.parent_inode,
159165
FileData::Chmod(data) => &data.inner.parent_inode,
160166
FileData::Chown(data) => &data.inner.parent_inode,
@@ -176,6 +182,7 @@ impl Event {
176182
match &self.file {
177183
FileData::Open(data) => &data.filename,
178184
FileData::Creation(data) => &data.filename,
185+
FileData::MkDir(data) => &data.filename,
179186
FileData::Unlink(data) => &data.filename,
180187
FileData::Chmod(data) => &data.inner.filename,
181188
FileData::Chown(data) => &data.inner.filename,
@@ -194,6 +201,7 @@ impl Event {
194201
match &self.file {
195202
FileData::Open(data) => &data.host_file,
196203
FileData::Creation(data) => &data.host_file,
204+
FileData::MkDir(data) => &data.host_file,
197205
FileData::Unlink(data) => &data.host_file,
198206
FileData::Chmod(data) => &data.inner.host_file,
199207
FileData::Chown(data) => &data.inner.host_file,
@@ -209,6 +217,7 @@ impl Event {
209217
match &mut self.file {
210218
FileData::Open(data) => data.host_file = host_path,
211219
FileData::Creation(data) => data.host_file = host_path,
220+
FileData::MkDir(data) => data.host_file = host_path,
212221
FileData::Unlink(data) => data.host_file = host_path,
213222
FileData::Chmod(data) => data.inner.host_file = host_path,
214223
FileData::Chown(data) => data.inner.host_file = host_path,
@@ -293,6 +302,7 @@ impl PartialEq for Event {
293302
pub enum FileData {
294303
Open(BaseFileData),
295304
Creation(BaseFileData),
305+
MkDir(BaseFileData),
296306
Unlink(BaseFileData),
297307
Chmod(ChmodFileData),
298308
Chown(ChownFileData),
@@ -311,6 +321,7 @@ impl FileData {
311321
let file = match event_type {
312322
file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner),
313323
file_activity_type_t::FILE_ACTIVITY_CREATION => FileData::Creation(inner),
324+
file_activity_type_t::DIR_ACTIVITY_CREATION => FileData::MkDir(inner),
314325
file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner),
315326
file_activity_type_t::FILE_ACTIVITY_CHMOD => {
316327
let data = ChmodFileData {
@@ -359,6 +370,9 @@ impl From<FileData> for fact_api::file_activity::File {
359370
let f_act = fact_api::FileCreation { activity };
360371
fact_api::file_activity::File::Creation(f_act)
361372
}
373+
FileData::MkDir(_) => {
374+
unreachable!("MkDir event reached protobuf conversion");
375+
}
362376
FileData::Unlink(event) => {
363377
let activity = Some(fact_api::FileActivityBase::from(event));
364378
let f_act = fact_api::FileUnlink { activity };
@@ -386,6 +400,7 @@ impl PartialEq for FileData {
386400
match (self, other) {
387401
(FileData::Open(this), FileData::Open(other)) => this == other,
388402
(FileData::Creation(this), FileData::Creation(other)) => this == other,
403+
(FileData::MkDir(this), FileData::MkDir(other)) => this == other,
389404
(FileData::Unlink(this), FileData::Unlink(other)) => this == other,
390405
(FileData::Chmod(this), FileData::Chmod(other)) => this == other,
391406
(FileData::Rename(this), FileData::Rename(other)) => this == other,

fact/src/host_scanner.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ impl HostScanner {
280280
};
281281
self.metrics.events.added();
282282

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

304+
// Skip directory creation events - we track them internally but don't send to sensor
305+
if event.is_mkdir() {
306+
continue;
307+
}
308+
304309
let event = Arc::new(event);
305310
if let Err(e) = self.tx.send(event) {
306311
self.metrics.events.dropped();

fact/src/metrics/kernel_metrics.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub struct KernelMetrics {
1313
path_chmod: EventCounter,
1414
path_chown: EventCounter,
1515
path_rename: EventCounter,
16+
path_mkdir: EventCounter,
17+
d_instantiate: EventCounter,
1618
map: PerCpuArray<MapData, metrics_t>,
1719
}
1820

@@ -43,19 +45,33 @@ impl KernelMetrics {
4345
"Events processed by the path_rename LSM hook",
4446
&[], // Labels are not needed since `collect` will add them all
4547
);
48+
let path_mkdir = EventCounter::new(
49+
"kernel_path_mkdir_events",
50+
"Events processed by the path_mkdir LSM hook",
51+
&[], // Labels are not needed since `collect` will add them all
52+
);
53+
let d_instantiate = EventCounter::new(
54+
"kernel_d_instantiate_events",
55+
"Events processed by the d_instantiate LSM hook",
56+
&[], // Labels are not needed since `collect` will add them all
57+
);
4658

4759
file_open.register(reg);
4860
path_unlink.register(reg);
4961
path_chmod.register(reg);
5062
path_chown.register(reg);
5163
path_rename.register(reg);
64+
path_mkdir.register(reg);
65+
d_instantiate.register(reg);
5266

5367
KernelMetrics {
5468
file_open,
5569
path_unlink,
5670
path_chmod,
5771
path_chown,
5872
path_rename,
73+
path_mkdir,
74+
d_instantiate,
5975
map: kernel_metrics,
6076
}
6177
}
@@ -105,6 +121,8 @@ impl KernelMetrics {
105121
KernelMetrics::refresh_labels(&self.path_chmod, &metrics.path_chmod);
106122
KernelMetrics::refresh_labels(&self.path_chown, &metrics.path_chown);
107123
KernelMetrics::refresh_labels(&self.path_rename, &metrics.path_rename);
124+
KernelMetrics::refresh_labels(&self.path_mkdir, &metrics.path_mkdir);
125+
KernelMetrics::refresh_labels(&self.d_instantiate, &metrics.d_instantiate);
108126

109127
Ok(())
110128
}

0 commit comments

Comments
 (0)