From 75fa3dc475f01b0ac2b8a6f3a25e6ed338c040af Mon Sep 17 00:00:00 2001 From: Giles Hutton Date: Mon, 13 Apr 2026 14:51:43 +0100 Subject: [PATCH 1/2] ROX-34104: deduplicate overlayfs events Introduces a new map to track overlayfs open event duplication in the BPF driver, keeping the overlayfs event which tends to have the richer information (e.g. create) The underlying file system event is dropped. --- fact-ebpf/src/bpf/inode.h | 16 ++++++++++++++++ fact-ebpf/src/bpf/main.c | 16 ++++++++++++++++ fact-ebpf/src/bpf/maps.h | 9 +++++++++ tests/test_editors/test_nvim.py | 8 -------- tests/test_editors/test_sed.py | 4 ---- tests/test_editors/test_vi.py | 20 -------------------- tests/test_editors/test_vim.py | 20 -------------------- tests/test_file_open.py | 2 -- tests/test_path_chmod.py | 2 -- tests/test_path_chown.py | 8 -------- tests/test_path_rename.py | 2 -- tests/test_path_unlink.py | 2 -- 12 files changed, 41 insertions(+), 68 deletions(-) diff --git a/fact-ebpf/src/bpf/inode.h b/fact-ebpf/src/bpf/inode.h index 481313e3..71c3115c 100644 --- a/fact-ebpf/src/bpf/inode.h +++ b/fact-ebpf/src/bpf/inode.h @@ -12,6 +12,7 @@ // clang-format on #define BTRFS_SUPER_MAGIC 0x9123683E +#define OVERLAYFS_SUPER_MAGIC 0x794c7630 /** * Retrieve the inode and device numbers and return them as a new key. @@ -58,6 +59,21 @@ __always_inline static inode_key_t inode_to_key(struct inode* inode) { return key; } +/** + * Check if the given inode belongs to an overlayfs filesystem. + * + * Overlayfs triggers LSM hooks for both the merged view and the + * underlying filesystem. The underlying event carries the same + * path and process data, so we can safely skip the overlayfs one. + */ +__always_inline static bool inode_is_overlayfs(struct inode* inode) { + if (inode == NULL) { + return false; + } + unsigned long magic = BPF_CORE_READ(inode, i_sb, s_magic); + return magic == OVERLAYFS_SUPER_MAGIC; +} + __always_inline static inode_value_t* inode_get(const struct inode_key_t* inode) { if (inode == NULL) { return NULL; diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index d262f2d8..8d9b1041 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -37,6 +37,22 @@ int BPF_PROG(trace_file_open, struct file* file) { goto ignored; } + // Overlayfs deduplication: overlayfs triggers file_open twice — once + // on the overlay inode (with richer semantics like FMODE_CREATED) and + // once on the underlying filesystem inode. We keep the overlayfs + // event and skip the underlying duplicate. + __u64 pid_tgid = bpf_get_current_pid_tgid(); + if (inode_is_overlayfs(file->f_inode)) { + char flag = 1; + bpf_map_update_elem(&overlayfs_dedup, &pid_tgid, &flag, BPF_ANY); + } else { + char* flag = bpf_map_lookup_elem(&overlayfs_dedup, &pid_tgid); + if (flag != NULL) { + bpf_map_delete_elem(&overlayfs_dedup, &pid_tgid); + goto ignored; + } + } + struct bound_path_t* path = path_read_unchecked(&file->f_path); if (path == NULL) { bpf_printk("Failed to read path"); diff --git a/fact-ebpf/src/bpf/maps.h b/fact-ebpf/src/bpf/maps.h index c8595829..0d75bd58 100644 --- a/fact-ebpf/src/bpf/maps.h +++ b/fact-ebpf/src/bpf/maps.h @@ -97,6 +97,15 @@ struct { __uint(max_entries, 1); } metrics SEC(".maps"); +// Track pid_tgid of overlayfs file_open events so we can skip the +// duplicate underlying filesystem event that follows immediately. +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __type(key, __u64); + __type(value, char); + __uint(max_entries, 1024); +} overlayfs_dedup SEC(".maps"); + __always_inline static struct metrics_t* get_metrics() { unsigned int zero = 0; return bpf_map_lookup_elem(&metrics, &zero); diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index 9f3d095d..51ffc26b 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -85,8 +85,6 @@ def test_new_file_ovfs(editor_container, server): events = [ Event(process=process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=fut, host_path=''), ] server.wait_events(events, strict=True) @@ -120,12 +118,8 @@ def test_open_file_ovfs(editor_container, server): events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=nvim, event_type=EventType.CREATION, file=vi_test_file, host_path=''), - Event(process=nvim, event_type=EventType.OPEN, - file=vi_test_file, host_path=''), Event(process=nvim, event_type=EventType.OWNERSHIP, file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), Event(process=nvim, event_type=EventType.UNLINK, @@ -134,8 +128,6 @@ def test_open_file_ovfs(editor_container, server): file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=nvim, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=nvim, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=nvim, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=nvim, event_type=EventType.UNLINK, diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index c3f6a2f8..122d1527 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -69,12 +69,8 @@ def test_sed_ovfs(vi_container, server): events = [ Event(process=shell, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=shell, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=sed, event_type=EventType.CREATION, file=sed_tmp_file, host_path=''), - Event(process=sed, event_type=EventType.OPEN, - file=sed_tmp_file, host_path=''), Event(process=sed, event_type=EventType.OWNERSHIP, file=sed_tmp_file, host_path='', owner_uid=0, owner_gid=0), Event(process=sed, event_type=EventType.RENAME, diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index 4301890a..62be23a9 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -59,24 +59,16 @@ def test_new_file_ovfs(vi_container, server): events = [ Event(process=process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=process, event_type=EventType.CREATION, file=swx_file, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=swx_file, host_path=''), Event(process=process, event_type=EventType.UNLINK, file=swx_file, host_path=''), Event(process=process, event_type=EventType.UNLINK, file=swap_file, host_path=''), Event(process=process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=process, event_type=EventType.UNLINK, file=swap_file, host_path=''), ] @@ -179,30 +171,20 @@ def test_open_file_ovfs(vi_container, server): events = [ Event(process=touch_process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch_process, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=swx_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, file=swap_file, host_path='', mode=0o644), Event(process=vi_process, event_type=EventType.CREATION, file=vi_test_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=vi_test_file, host_path=''), Event(process=vi_process, event_type=EventType.OWNERSHIP, file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), Event(process=vi_process, event_type=EventType.UNLINK, @@ -211,8 +193,6 @@ def test_open_file_ovfs(vi_container, server): file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index 56cbb667..a7b9f83d 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -55,24 +55,16 @@ def test_new_file_ovfs(editor_container, server): events = [ Event(process=process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=process, event_type=EventType.CREATION, file=swx_file, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=swx_file, host_path=''), Event(process=process, event_type=EventType.UNLINK, file=swx_file, host_path=''), Event(process=process, event_type=EventType.UNLINK, file=swap_file, host_path=''), Event(process=process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=process, event_type=EventType.UNLINK, file=swap_file, host_path=''), ] @@ -173,30 +165,20 @@ def test_open_file_ovfs(editor_container, server): events = [ Event(process=touch_process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch_process, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=swx_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swx_file, host_path=''), Event(process=vi_process, event_type=EventType.UNLINK, file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=swap_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=swap_file, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, file=swap_file, host_path='', mode=0o644), Event(process=vi_process, event_type=EventType.CREATION, file=vi_test_file, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=vi_test_file, host_path=''), Event(process=vi_process, event_type=EventType.OWNERSHIP, file=vi_test_file, host_path='', owner_uid=0, owner_gid=0), Event(process=vi_process, event_type=EventType.UNLINK, @@ -205,8 +187,6 @@ def test_open_file_ovfs(editor_container, server): file=fut_backup, host_path='', old_file=fut, old_host_path=''), Event(process=vi_process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=vi_process, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=vi_process, event_type=EventType.PERMISSION, file=fut, host_path='', mode=0o100644), Event(process=vi_process, event_type=EventType.UNLINK, diff --git a/tests/test_file_open.py b/tests/test_file_open.py index c43b8a15..0bc5ab2b 100644 --- a/tests/test_file_open.py +++ b/tests/test_file_open.py @@ -166,8 +166,6 @@ def test_overlay(test_container, server): events = [ Event(process=process, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=process, event_type=EventType.OPEN, - file=fut, host_path='') ] server.wait_events(events) diff --git a/tests/test_path_chmod.py b/tests/test_path_chmod.py index 08cb483d..811a4e3e 100644 --- a/tests/test_path_chmod.py +++ b/tests/test_path_chmod.py @@ -175,8 +175,6 @@ def test_overlay(test_container, server): events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=chmod, event_type=EventType.PERMISSION, file=fut, host_path='', mode=int(mode, 8)), ] diff --git a/tests/test_path_chown.py b/tests/test_path_chown.py index a8c75c0b..021b5b55 100644 --- a/tests/test_path_chown.py +++ b/tests/test_path_chown.py @@ -60,8 +60,6 @@ def test_chown(test_container, server, filename): events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch, event_type=EventType.OPEN, file=fut, - host_path=''), Event(process=chown, event_type=EventType.OWNERSHIP, file=fut, host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), ] @@ -104,8 +102,6 @@ def test_multiple(test_container, server): events.extend([ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch, event_type=EventType.OPEN, file=fut, - host_path=''), Event(process=chown, event_type=EventType.OWNERSHIP, file=fut, host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), ]) @@ -150,8 +146,6 @@ def test_ignored(test_container, server): events = [ Event(process=reported_touch, event_type=EventType.CREATION, file=monitored_file, host_path=''), - Event(process=reported_touch, event_type=EventType.OPEN, - file=monitored_file, host_path=''), Event(process=reported_chown, event_type=EventType.OWNERSHIP, file=monitored_file, host_path='', owner_uid=TEST_UID, owner_gid=TEST_GID), ] @@ -201,8 +195,6 @@ def test_no_change(test_container, server): events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch, event_type=EventType.OPEN, file=fut, - host_path=''), chown_event, chown_event, ] diff --git a/tests/test_path_rename.py b/tests/test_path_rename.py index a3600eff..12e417e1 100644 --- a/tests/test_path_rename.py +++ b/tests/test_path_rename.py @@ -174,8 +174,6 @@ def test_overlay(test_container, server): events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=mv, event_type=EventType.RENAME, file=new_fut, host_path='', old_file=fut, old_host_path=''), ] diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 66a533e6..e7675c53 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -170,8 +170,6 @@ def test_overlay(test_container, server): events = [ Event(process=touch, event_type=EventType.CREATION, file=fut, host_path=''), - Event(process=touch, event_type=EventType.OPEN, - file=fut, host_path=''), Event(process=rm, event_type=EventType.UNLINK, file=fut, host_path=''), ] From 6c382859783669a2b95f5b591f3095846049db79 Mon Sep 17 00:00:00 2001 From: Giles Hutton Date: Mon, 13 Apr 2026 18:13:39 +0100 Subject: [PATCH 2/2] PR review fixes; use per cpu map, doc changes --- fact-ebpf/src/bpf/inode.h | 5 +++-- fact-ebpf/src/bpf/maps.h | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fact-ebpf/src/bpf/inode.h b/fact-ebpf/src/bpf/inode.h index 71c3115c..fa841536 100644 --- a/fact-ebpf/src/bpf/inode.h +++ b/fact-ebpf/src/bpf/inode.h @@ -11,6 +11,7 @@ #include // clang-format on +// From: man(2) statfs #define BTRFS_SUPER_MAGIC 0x9123683E #define OVERLAYFS_SUPER_MAGIC 0x794c7630 @@ -63,8 +64,8 @@ __always_inline static inode_key_t inode_to_key(struct inode* inode) { * Check if the given inode belongs to an overlayfs filesystem. * * Overlayfs triggers LSM hooks for both the merged view and the - * underlying filesystem. The underlying event carries the same - * path and process data, so we can safely skip the overlayfs one. + * underlying filesystem. This can be used to distinguish between + * them. */ __always_inline static bool inode_is_overlayfs(struct inode* inode) { if (inode == NULL) { diff --git a/fact-ebpf/src/bpf/maps.h b/fact-ebpf/src/bpf/maps.h index 0d75bd58..46d3e3f0 100644 --- a/fact-ebpf/src/bpf/maps.h +++ b/fact-ebpf/src/bpf/maps.h @@ -100,10 +100,10 @@ struct { // Track pid_tgid of overlayfs file_open events so we can skip the // duplicate underlying filesystem event that follows immediately. struct { - __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(type, BPF_MAP_TYPE_LRU_PERCPU_HASH); __type(key, __u64); __type(value, char); - __uint(max_entries, 1024); + __uint(max_entries, 1); } overlayfs_dedup SEC(".maps"); __always_inline static struct metrics_t* get_metrics() {