diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index b7c044f1..5ac35159 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -94,6 +94,9 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { return 0; } + // We only support files with one link for now + inode_remove(&inode_key); + submit_unlink_event(&m->path_unlink, path->path, inode_to_submit, diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 40bd317a..bc9d7897 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -130,6 +130,10 @@ impl Event { matches!(self.file, FileData::Creation(_)) } + pub fn is_unlink(&self) -> bool { + matches!(self.file, FileData::Unlink(_)) + } + /// Unwrap the inner FileData and return the inode that triggered /// the event. /// diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index 36cacdef..03754912 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -220,6 +220,18 @@ impl HostScanner { Ok(()) } + /// Special handling for unlink events. + /// + /// This method removes the inode from the userland inode->path map. + /// The probe already cleared the kernel inode map. + fn handle_unlink_event(&self, event: &Event) { + let inode = event.get_inode(); + + if self.inode_map.borrow_mut().remove(inode).is_some() { + self.metrics.scan_inc(ScanLabels::InodeRemoved); + } + } + /// Periodically notify the host scanner main task that a scan needs /// to happen. /// @@ -277,6 +289,11 @@ impl HostScanner { event.set_old_host_path(host_path); } + // Special handling for unlink events + if event.is_unlink() { + self.handle_unlink_event(&event); + } + let event = Arc::new(event); if let Err(e) = self.tx.send(event) { self.metrics.events.dropped(); diff --git a/tests/test_path_unlink.py b/tests/test_path_unlink.py index 66a533e6..3c94386a 100644 --- a/tests/test_path_unlink.py +++ b/tests/test_path_unlink.py @@ -227,3 +227,55 @@ def test_unmonitored_mounted_dir(test_container, test_file, server): file=fut, host_path=test_file) server.wait_events([event]) + + +def test_probe_inode_map(monitored_dir, ignored_dir, server): + """ + TODO[ROX-33222]: This test won't work when hardlinks are handled properly. + + This test demonstrates that the current implementation removes the inode + from the kernel map correctly, as a second unmonitored hardlink deletion + is not noticed. + + Args: + monitored_dir: Temporary directory path that is monitored by fact. + ignored_dir: Temporary directory path that is NOT monitored by fact. + server: The server instance to communicate with. + """ + process = Process.from_proc() + + # File Under Test - original file in monitored directory + original_file = os.path.join(monitored_dir, 'original.txt') + + # Create the original file + with open(original_file, 'w') as f: + f.write('This is a test') + + # Create two hardlinks in the unmonitored directory + hardlink_file1 = os.path.join(ignored_dir, 'hardlink1.txt') + os.link(original_file, hardlink_file1) + + hardlink_file2 = os.path.join(ignored_dir, 'hardlink2.txt') + os.link(original_file, hardlink_file2) + + e = Event(process=process, event_type=EventType.CREATION, + file=original_file, host_path=original_file) + + server.wait_events([e]) + + os.remove(hardlink_file1) + os.remove(hardlink_file2) + + # Create a guard file to ensure all events have been processed + guard_file = os.path.join(monitored_dir, 'guard.txt') + with open(guard_file, 'w') as f: + f.write('guard') + + events = [ + Event(process=process, event_type=EventType.UNLINK, + file=hardlink_file1, host_path=original_file), + Event(process=process, event_type=EventType.CREATION, + file=guard_file, host_path=guard_file), + ] + + server.wait_events(events)