Skip to content

Commit 087a210

Browse files
authored
ROX-30437: basic inode tracking for host path resolution (#166)
* ROX-30437: basic inode tracking for host path resolution This is a very basic implementation of inode tracking meant to be used in the upcoming release and improved upon in the near future. The current implementation will perform a scan of the paths that are configured to be monitored, using the inode and device numbers as a key to two maps: - A BPF hash map for kernelspace to know when an inode triggers an event. - A HashMap in userspace that maps the inode to a path that we found the inode at. With these two maps we are able to confidently emit events for files that are mounted into containers with paths that don't necessarily match the prefixes configured for monitoring and, at a later stage in userspace, add the path on the host to the event itself. The implemented approach is far from complete, it will only work as long as the files found during the initial scan are not moved, deleted or replaced by a different file. Future patches will extend the functionality of both kernel and userspace to be able to catch more corner cases. * ROX-30437: add integration tests Added tests will validate events generated on an overlayfs file properly shows the event on the upper layer and the access to the underlying FS. They also validate a mounted path on a container resolves to the correct host path. While developing these tests, it became painfully obvious getting the information of the process running inside the container is not straightforward. Because containers tend to be fairly static, we should be able to manually create the information statically in the test and still have everything work correctly. In order to minimize the amount of changes on existing tests, the default Process constructor now takes fields directly and there is a from_proc class method that builds a new Process object from /proc. Additionally, getting the pid of a process in a container is virtually impossible, so we make the pid check optional. * ROX-30437: fix k8s manifest for inode tracking * chore: general cleanups * Added a missing null check. * Added missing doc strings. * Downgrade inode and dev numbers to 32 bits. * Added some logging statements. * Small comment on char vs bool
1 parent 18bdcaa commit 087a210

File tree

23 files changed

+847
-213
lines changed

23 files changed

+847
-213
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fact-ebpf/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ license.workspace = true
1010
[dependencies]
1111
aya = { workspace = true }
1212
libc = { workspace = true }
13+
serde = { workspace = true }
1314

1415
[build-dependencies]
1516
anyhow = { workspace = true }

fact-ebpf/src/bpf/builtins.h

Lines changed: 0 additions & 7 deletions
This file was deleted.

fact-ebpf/src/bpf/events.h

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
#pragma once
22

3-
#include <bpf/bpf_helpers.h>
3+
// clang-format off
4+
#include "vmlinux.h"
45

6+
#include "inode.h"
57
#include "maps.h"
68
#include "process.h"
79
#include "types.h"
8-
#include "vmlinux.h"
910

10-
__always_inline static void submit_event(struct metrics_by_hook_t* m, file_activity_type_t event_type, const char filename[PATH_MAX], struct dentry* dentry, bool use_bpf_d_path) {
11+
#include <bpf/bpf_helpers.h>
12+
// clang-format on
13+
14+
__always_inline static void submit_event(struct metrics_by_hook_t* m,
15+
file_activity_type_t event_type,
16+
const char filename[PATH_MAX],
17+
inode_key_t* inode,
18+
bool use_bpf_d_path) {
1119
struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0);
1220
if (event == NULL) {
1321
m->ringbuffer_full++;
@@ -16,18 +24,14 @@ __always_inline static void submit_event(struct metrics_by_hook_t* m, file_activ
1624

1725
event->type = event_type;
1826
event->timestamp = bpf_ktime_get_boot_ns();
27+
inode_copy_or_reset(&event->inode, inode);
1928
bpf_probe_read_str(event->filename, PATH_MAX, filename);
2029

2130
struct helper_t* helper = get_helper();
2231
if (helper == NULL) {
2332
goto error;
2433
}
2534

26-
const char* p = get_host_path(helper->buf, dentry);
27-
if (p != NULL) {
28-
bpf_probe_read_str(event->host_file, PATH_MAX, p);
29-
}
30-
3135
int64_t err = process_fill(&event->process, use_bpf_d_path);
3236
if (err) {
3337
bpf_printk("Failed to fill process information: %d", err);

fact-ebpf/src/bpf/file.h

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,14 @@
33
// clang-format off
44
#include "vmlinux.h"
55

6-
#include "bound_path.h"
76
#include "builtins.h"
8-
#include "d_path.h"
97
#include "types.h"
108
#include "maps.h"
119

1210
#include <bpf/bpf_helpers.h>
1311
#include <bpf/bpf_core_read.h>
1412
// clang-format on
1513

16-
__always_inline static char* get_host_path(char buf[PATH_MAX * 2], struct dentry* d) {
17-
int offset = PATH_MAX - 1;
18-
buf[PATH_MAX - 1] = '\0';
19-
20-
for (int i = 0; i < 16 && offset > 0; i++) {
21-
struct qstr d_name;
22-
BPF_CORE_READ_INTO(&d_name, d, d_name);
23-
if (d_name.name == NULL) {
24-
break;
25-
}
26-
27-
int len = d_name.len;
28-
if (len <= 0 || len >= PATH_MAX) {
29-
return NULL;
30-
}
31-
32-
offset -= len;
33-
if (offset <= 0) {
34-
return NULL;
35-
}
36-
37-
if (bpf_probe_read_kernel(&buf[offset], len, d_name.name) != 0) {
38-
return NULL;
39-
}
40-
41-
if (len == 1 && buf[offset] == '/') {
42-
// Reached the root
43-
offset++;
44-
break;
45-
}
46-
47-
offset--;
48-
buf[offset] = '/';
49-
50-
struct dentry* parent = BPF_CORE_READ(d, d_parent);
51-
// if we reached the root
52-
if (parent == NULL || d == parent) {
53-
break;
54-
}
55-
d = parent;
56-
}
57-
58-
return &buf[offset];
59-
}
60-
6114
__always_inline static bool is_monitored(struct bound_path_t* path) {
6215
if (!filter_by_prefix()) {
6316
// no path configured, allow all

fact-ebpf/src/bpf/inode.h

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#pragma once
2+
3+
// clang-format off
4+
#include "vmlinux.h"
5+
6+
#include "kdev.h"
7+
#include "types.h"
8+
#include "maps.h"
9+
10+
#include <bpf/bpf_core_read.h>
11+
#include <bpf/bpf_helpers.h>
12+
// clang-format on
13+
14+
#define BTRFS_SUPER_MAGIC 0x9123683E
15+
16+
/**
17+
* Retrieve the inode and device numbers and return them as a new key.
18+
*
19+
* Different filesystems may `stat` files in different ways, if support
20+
* for a new filesystem is needed, add it here.
21+
*
22+
* Most Linux filesystems use the following generic function to fill
23+
* these fields when running `stat`:
24+
* https://github.com/torvalds/linux/blob/7d0a66e4bb9081d75c82ec4957c50034cb0ea449/fs/stat.c#L82
25+
*
26+
* The method used to retrieve the device is different in btrfs and can
27+
* be found here:
28+
* https://github.com/torvalds/linux/blob/7d0a66e4bb9081d75c82ec4957c50034cb0ea449/fs/btrfs/inode.c#L8038
29+
*/
30+
__always_inline static inode_key_t inode_to_key(struct inode* inode) {
31+
inode_key_t key = {0};
32+
if (inode == NULL) {
33+
return key;
34+
}
35+
36+
unsigned long magic = inode->i_sb->s_magic;
37+
switch (magic) {
38+
case BTRFS_SUPER_MAGIC:
39+
if (bpf_core_type_exists(struct btrfs_inode)) {
40+
struct btrfs_inode* btrfs_inode = container_of(inode, struct btrfs_inode, vfs_inode);
41+
key.inode = inode->i_ino;
42+
key.dev = BPF_CORE_READ(btrfs_inode, root, anon_dev);
43+
break;
44+
}
45+
// If the btrfs_inode does not exist, most likely it is not
46+
// supported on the system. Fallback to the generic implementation
47+
// just in case.
48+
default:
49+
key.inode = inode->i_ino;
50+
key.dev = inode->i_sb->s_dev;
51+
break;
52+
}
53+
54+
// Encode the device so it matches with the result of `stat` in
55+
// userspace
56+
key.dev = new_encode_dev(key.dev);
57+
58+
return key;
59+
}
60+
61+
__always_inline static inode_value_t* inode_get(struct inode_key_t* inode) {
62+
if (inode == NULL) {
63+
return NULL;
64+
}
65+
return bpf_map_lookup_elem(&inode_map, inode);
66+
}
67+
68+
__always_inline static long inode_remove(struct inode_key_t* inode) {
69+
if (inode == NULL) {
70+
return 0;
71+
}
72+
return bpf_map_delete_elem(&inode_map, inode);
73+
}
74+
75+
typedef enum inode_monitored_t {
76+
NOT_MONITORED = 0,
77+
MONITORED,
78+
} inode_monitored_t;
79+
80+
/**
81+
* Check if the provided inode is being monitored.
82+
*
83+
* The current implementation is very basic and might seem like
84+
* overkill, but in the near future this function will be extended to
85+
* check if the parent of the provided inode is monitored and provide
86+
* different results for handling more complicated scenarios.
87+
*/
88+
__always_inline static inode_monitored_t inode_is_monitored(const inode_value_t* inode) {
89+
if (inode != NULL) {
90+
return MONITORED;
91+
}
92+
93+
return NOT_MONITORED;
94+
}
95+
96+
__always_inline static void inode_copy_or_reset(inode_key_t* dst, const inode_key_t* src) {
97+
if (dst == NULL) {
98+
return;
99+
}
100+
101+
if (src != NULL) {
102+
dst->inode = src->inode;
103+
dst->dev = src->dev;
104+
} else {
105+
dst->inode = 0;
106+
dst->dev = 0;
107+
}
108+
}

fact-ebpf/src/bpf/kdev.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
// clang-format off
4+
#include "vmlinux.h"
5+
6+
#include <bpf/bpf_helpers.h>
7+
// clang-format on
8+
9+
// Most of the code in this file is taken from:
10+
// https://github.com/torvalds/linux/blob/559e608c46553c107dbba19dae0854af7b219400/include/linux/kdev_t.h
11+
12+
#define MINORBITS 20
13+
#define MINORMASK ((1U << MINORBITS) - 1)
14+
15+
#define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS))
16+
#define MINOR(dev) ((unsigned int)((dev) & MINORMASK))
17+
18+
__always_inline static u32 new_encode_dev(dev_t dev) {
19+
unsigned major = MAJOR(dev);
20+
unsigned minor = MINOR(dev);
21+
return (minor & 0xff) | (major << 8) | ((minor & ~0xff) << 12);
22+
}

fact-ebpf/src/bpf/main.c

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
#include "file.h"
55
#include "types.h"
6-
#include "process.h"
6+
#include "inode.h"
77
#include "maps.h"
88
#include "events.h"
99
#include "bound_path.h"
@@ -44,12 +44,19 @@ int BPF_PROG(trace_file_open, struct file* file) {
4444
return 0;
4545
}
4646

47-
if (!is_monitored(path)) {
48-
goto ignored;
47+
inode_key_t inode_key = inode_to_key(file->f_inode);
48+
const inode_value_t* inode = inode_get(&inode_key);
49+
switch (inode_is_monitored(inode)) {
50+
case NOT_MONITORED:
51+
if (!is_monitored(path)) {
52+
goto ignored;
53+
}
54+
break;
55+
case MONITORED:
56+
break;
4957
}
5058

51-
struct dentry* d = BPF_CORE_READ(file, f_path.dentry);
52-
submit_event(&m->file_open, event_type, path->path, d, true);
59+
submit_event(&m->file_open, event_type, path->path, &inode_key, true);
5360

5461
return 0;
5562

@@ -91,12 +98,27 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) {
9198
goto error;
9299
}
93100

94-
if (!is_monitored(path)) {
95-
m->path_unlink.ignored++;
96-
return 0;
101+
inode_key_t inode_key = inode_to_key(dentry->d_inode);
102+
const inode_value_t* inode = inode_get(&inode_key);
103+
104+
switch (inode_is_monitored(inode)) {
105+
case NOT_MONITORED:
106+
if (!is_monitored(path)) {
107+
m->path_unlink.ignored++;
108+
return 0;
109+
}
110+
break;
111+
112+
case MONITORED:
113+
inode_remove(&inode_key);
114+
break;
97115
}
98116

99-
submit_event(&m->path_unlink, FILE_ACTIVITY_UNLINK, path->path, dentry, path_unlink_supports_bpf_d_path);
117+
submit_event(&m->path_unlink,
118+
FILE_ACTIVITY_UNLINK,
119+
path->path,
120+
&inode_key,
121+
path_unlink_supports_bpf_d_path);
100122
return 0;
101123

102124
error:

fact-ebpf/src/bpf/maps.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ struct {
9292
__uint(max_entries, 8 * 1024 * 1024);
9393
} rb SEC(".maps");
9494

95+
struct {
96+
__uint(type, BPF_MAP_TYPE_HASH);
97+
__type(key, inode_key_t);
98+
__type(value, inode_value_t);
99+
__uint(max_entries, 1024);
100+
} inode_map SEC(".maps");
101+
95102
struct {
96103
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
97104
__type(key, __u32);

fact-ebpf/src/bpf/process.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#pragma once
22

3-
#include "file.h"
4-
#include "maps.h"
5-
#include "types.h"
6-
73
// clang-format off
84
#include "vmlinux.h"
95

6+
#include "d_path.h"
7+
#include "maps.h"
8+
#include "types.h"
9+
1010
#include <bpf/bpf_helpers.h>
1111
#include <bpf/bpf_core_read.h>
1212
// clang-format on

0 commit comments

Comments
 (0)