Skip to content

Commit b359ef6

Browse files
authored
ROX-30256: track files and directories being renamed (#308)
Implement the path_rename LSM hook in order to capture and generate rename events. The implementation is fairly basic and does nothing with regards to inode tracking other than checking if either of the inodes involved are tracked. With this change we get better visibility into commands that use renaming as a means for editing a file, like vi and sed.
1 parent 05d281b commit b359ef6

File tree

18 files changed

+523
-55
lines changed

18 files changed

+523
-55
lines changed

fact-ebpf/src/bpf/bound_path.h

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ __always_inline static void path_write_char(char* p, unsigned int offset, char c
1919
*path_safe_access(p, offset) = c;
2020
}
2121

22-
__always_inline static struct bound_path_t* _path_read(struct path* path, bool use_bpf_d_path) {
23-
struct bound_path_t* bound_path = get_bound_path();
22+
__always_inline static struct bound_path_t* _path_read(struct path* path, bound_path_buffer_t key, bool use_bpf_d_path) {
23+
struct bound_path_t* bound_path = get_bound_path(key);
2424
if (bound_path == NULL) {
2525
return NULL;
2626
}
@@ -37,11 +37,19 @@ __always_inline static struct bound_path_t* _path_read(struct path* path, bool u
3737
}
3838

3939
__always_inline static struct bound_path_t* path_read(struct path* path) {
40-
return _path_read(path, true);
40+
return _path_read(path, BOUND_PATH_MAIN, true);
4141
}
4242

4343
__always_inline static struct bound_path_t* path_read_no_d_path(struct path* path) {
44-
return _path_read(path, false);
44+
return _path_read(path, BOUND_PATH_MAIN, false);
45+
}
46+
47+
__always_inline static struct bound_path_t* path_read_alt(struct path* path) {
48+
return _path_read(path, BOUND_PATH_ALTERNATE, true);
49+
}
50+
51+
__always_inline static struct bound_path_t* path_read_alt_no_d_path(struct path* path) {
52+
return _path_read(path, BOUND_PATH_ALTERNATE, false);
4553
}
4654

4755
enum path_append_status_t {
@@ -69,3 +77,47 @@ __always_inline static enum path_append_status_t path_append_dentry(struct bound
6977

7078
return 0;
7179
}
80+
81+
__always_inline static struct bound_path_t* _path_read_append_d_entry(struct path* dir, struct dentry* dentry, bound_path_buffer_t key) {
82+
struct bound_path_t* path = _path_read(dir, key, path_hooks_support_bpf_d_path);
83+
84+
if (path == NULL) {
85+
bpf_printk("Failed to read path");
86+
return NULL;
87+
}
88+
path_write_char(path->path, path->len - 1, '/');
89+
90+
switch (path_append_dentry(path, dentry)) {
91+
case PATH_APPEND_SUCCESS:
92+
break;
93+
case PATH_APPEND_INVALID_LENGTH:
94+
bpf_printk("Invalid path length: %u", path->len);
95+
return NULL;
96+
case PATH_APPEND_READ_ERROR:
97+
bpf_printk("Failed to read final path component");
98+
return NULL;
99+
}
100+
return path;
101+
}
102+
103+
/**
104+
* Read the path and append the supplied dentry.
105+
*
106+
* A very common pattern in the kernel is to provide a struct path to a
107+
* directory and a dentry to an element in said directory, this helper
108+
* provides a short way of resolving the full path in one call.
109+
*/
110+
__always_inline static struct bound_path_t* path_read_append_d_entry(struct path* dir, struct dentry* dentry) {
111+
return _path_read_append_d_entry(dir, dentry, BOUND_PATH_MAIN);
112+
}
113+
114+
/**
115+
* Read the path and append the supplied dentry.
116+
*
117+
* This works essentially the same as path_read_append_d_entry, but does
118+
* so in an alternate buffer. Useful for operations that take more than
119+
* one path, like path_rename.
120+
*/
121+
__always_inline static struct bound_path_t* path_read_alt_append_d_entry(struct path* dir, struct dentry* dentry) {
122+
return _path_read_append_d_entry(dir, dentry, BOUND_PATH_ALTERNATE);
123+
}

fact-ebpf/src/bpf/events.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,21 @@ __always_inline static void submit_ownership_event(struct metrics_by_hook_t* m,
9595

9696
__submit_event(event, m, FILE_ACTIVITY_CHOWN, filename, inode, use_bpf_d_path);
9797
}
98+
99+
__always_inline static void submit_rename_event(struct metrics_by_hook_t* m,
100+
const char new_filename[PATH_MAX],
101+
const char old_filename[PATH_MAX],
102+
inode_key_t* new_inode,
103+
inode_key_t* old_inode,
104+
bool use_bpf_d_path) {
105+
struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0);
106+
if (event == NULL) {
107+
m->ringbuffer_full++;
108+
return;
109+
}
110+
111+
bpf_probe_read_str(event->rename.old_filename, PATH_MAX, old_filename);
112+
inode_copy_or_reset(&event->rename.old_inode, old_inode);
113+
114+
__submit_event(event, m, FILE_ACTIVITY_RENAME, new_filename, new_inode, use_bpf_d_path);
115+
}

fact-ebpf/src/bpf/inode.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ typedef enum inode_monitored_t {
8585
* check if the parent of the provided inode is monitored and provide
8686
* different results for handling more complicated scenarios.
8787
*/
88-
__always_inline static inode_monitored_t inode_is_monitored(const inode_value_t* inode) {
88+
__always_inline static inode_monitored_t inode_is_monitored(const inode_value_t* volatile inode) {
8989
if (inode != NULL) {
9090
return MONITORED;
9191
}

fact-ebpf/src/bpf/main.c

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,28 +74,11 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) {
7474

7575
m->path_unlink.total++;
7676

77-
struct bound_path_t* path = NULL;
78-
if (path_hooks_support_bpf_d_path) {
79-
path = path_read(dir);
80-
} else {
81-
path = path_read_no_d_path(dir);
82-
}
83-
77+
struct bound_path_t* path = path_read_append_d_entry(dir, dentry);
8478
if (path == NULL) {
8579
bpf_printk("Failed to read path");
86-
goto error;
87-
}
88-
path_write_char(path->path, path->len - 1, '/');
89-
90-
switch (path_append_dentry(path, dentry)) {
91-
case PATH_APPEND_SUCCESS:
92-
break;
93-
case PATH_APPEND_INVALID_LENGTH:
94-
bpf_printk("Invalid path length: %u", path->len);
95-
goto error;
96-
case PATH_APPEND_READ_ERROR:
97-
bpf_printk("Failed to read final path component");
98-
goto error;
80+
m->path_unlink.error++;
81+
return 0;
9982
}
10083

10184
inode_key_t inode_key = inode_to_key(dentry->d_inode);
@@ -120,10 +103,6 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) {
120103
&inode_key,
121104
path_hooks_support_bpf_d_path);
122105
return 0;
123-
124-
error:
125-
m->path_unlink.error++;
126-
return 0;
127106
}
128107

129108
SEC("lsm/path_chmod")
@@ -229,3 +208,52 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign
229208

230209
return 0;
231210
}
211+
212+
SEC("lsm/path_rename")
213+
int BPF_PROG(trace_path_rename, struct path* old_dir,
214+
struct dentry* old_dentry, struct path* new_dir,
215+
struct dentry* new_dentry, unsigned int flags) {
216+
struct metrics_t* m = get_metrics();
217+
if (m == NULL) {
218+
return 0;
219+
}
220+
221+
m->path_rename.total++;
222+
223+
struct bound_path_t* new_path = path_read_append_d_entry(new_dir, new_dentry);
224+
if (new_path == NULL) {
225+
bpf_printk("Failed to read path");
226+
goto error;
227+
}
228+
229+
struct bound_path_t* old_path = path_read_alt_append_d_entry(old_dir, old_dentry);
230+
if (old_path == NULL) {
231+
bpf_printk("Failed to read path");
232+
goto error;
233+
}
234+
235+
inode_key_t old_inode = inode_to_key(old_dentry->d_inode);
236+
const inode_value_t* volatile old_inode_value = inode_get(&old_inode);
237+
inode_key_t new_inode = inode_to_key(new_dentry->d_inode);
238+
const inode_value_t* volatile new_inode_value = inode_get(&new_inode);
239+
240+
if (inode_is_monitored(old_inode_value) == NOT_MONITORED &&
241+
inode_is_monitored(new_inode_value) == NOT_MONITORED &&
242+
!is_monitored(old_path) &&
243+
!is_monitored(new_path)) {
244+
m->path_rename.ignored++;
245+
return 0;
246+
}
247+
248+
submit_rename_event(&m->path_rename,
249+
new_path->path,
250+
old_path->path,
251+
&old_inode,
252+
&new_inode,
253+
path_hooks_support_bpf_d_path);
254+
return 0;
255+
256+
error:
257+
m->path_rename.error++;
258+
return 0;
259+
}

fact-ebpf/src/bpf/maps.h

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,16 @@ struct {
7979
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
8080
__type(key, __u32);
8181
__type(value, struct bound_path_t);
82-
__uint(max_entries, 1);
82+
__uint(max_entries, 2);
8383
} bound_path_heap SEC(".maps");
8484

85-
__always_inline static struct bound_path_t* get_bound_path() {
86-
unsigned int zero = 0;
87-
return bpf_map_lookup_elem(&bound_path_heap, &zero);
85+
typedef enum {
86+
BOUND_PATH_MAIN = 0,
87+
BOUND_PATH_ALTERNATE = 1,
88+
} bound_path_buffer_t;
89+
90+
__always_inline static struct bound_path_t* get_bound_path(bound_path_buffer_t key) {
91+
return bpf_map_lookup_elem(&bound_path_heap, &key);
8892
}
8993

9094
struct {

fact-ebpf/src/bpf/types.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ typedef enum file_activity_type_t {
5454
FILE_ACTIVITY_UNLINK,
5555
FILE_ACTIVITY_CHMOD,
5656
FILE_ACTIVITY_CHOWN,
57+
FILE_ACTIVITY_RENAME,
5758
} file_activity_type_t;
5859

5960
struct event_t {
@@ -73,6 +74,10 @@ struct event_t {
7374
unsigned int gid;
7475
} old, new;
7576
} chown;
77+
struct {
78+
char old_filename[PATH_MAX];
79+
inode_key_t old_inode;
80+
} rename;
7681
};
7782
};
7883

@@ -104,4 +109,5 @@ struct metrics_t {
104109
struct metrics_by_hook_t path_unlink;
105110
struct metrics_by_hook_t path_chmod;
106111
struct metrics_by_hook_t path_chown;
112+
struct metrics_by_hook_t path_rename;
107113
};

fact-ebpf/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ impl metrics_t {
113113
m.path_unlink = m.path_unlink.accumulate(&other.path_unlink);
114114
m.path_chmod = m.path_chmod.accumulate(&other.path_chmod);
115115
m.path_chown = m.path_chown.accumulate(&other.path_chown);
116+
m.path_rename = m.path_rename.accumulate(&other.path_rename);
116117
m
117118
}
118119
}

fact/src/bpf/mod.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ mod bpf_tests {
269269
tokio::time::sleep(Duration::from_millis(500)).await;
270270

271271
// Create a file
272-
let file = NamedTempFile::new_in(monitored_path).expect("Failed to create temporary file");
272+
let file = NamedTempFile::new_in(&monitored_path).expect("Failed to create temporary file");
273273
println!("Created {file:?}");
274274

275275
// Trigger permission changes
@@ -286,6 +286,12 @@ mod bpf_tests {
286286
let current = Process::current();
287287
let file_path = file.path().to_path_buf();
288288

289+
// Trigger a file rename
290+
let renamed_path = monitored_path.join("target");
291+
std::fs::rename(&file, &renamed_path).expect("Failed to rename file");
292+
// Move the file back so it can be properly closed
293+
std::fs::rename(&renamed_path, &file).expect("Failed to rename file");
294+
289295
let expected_events = [
290296
Event::new(
291297
EventTestData::Creation,
@@ -303,6 +309,22 @@ mod bpf_tests {
303309
current.clone(),
304310
)
305311
.unwrap(),
312+
Event::new(
313+
EventTestData::Rename(file_path.clone()),
314+
host_info::get_hostname(),
315+
renamed_path.clone(),
316+
PathBuf::new(),
317+
current.clone(),
318+
)
319+
.unwrap(),
320+
Event::new(
321+
EventTestData::Rename(renamed_path),
322+
host_info::get_hostname(),
323+
file_path.clone(),
324+
PathBuf::new(),
325+
current.clone(),
326+
)
327+
.unwrap(),
306328
Event::new(
307329
EventTestData::Unlink,
308330
host_info::get_hostname(),
@@ -312,7 +334,6 @@ mod bpf_tests {
312334
)
313335
.unwrap(),
314336
];
315-
316337
// Close the file, removing it
317338
file.close().expect("Failed to close temp file");
318339

0 commit comments

Comments
 (0)