-
Notifications
You must be signed in to change notification settings - Fork 4
ROX-33198: Instrument inode tracking on file open lsm hook #391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9cba674
7773d9d
e1bb8f0
ded9eb6
3779d62
a067654
7b21586
4f4809a
53fcce5
e98ddbe
4f1686a
6ad6d1d
f0049e4
97b60c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,21 +33,21 @@ __always_inline static inode_key_t inode_to_key(struct inode* inode) { | |
| return key; | ||
| } | ||
|
|
||
| unsigned long magic = inode->i_sb->s_magic; | ||
| unsigned long magic = BPF_CORE_READ(inode, i_sb, s_magic); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| switch (magic) { | ||
| case BTRFS_SUPER_MAGIC: | ||
| if (bpf_core_type_exists(struct btrfs_inode)) { | ||
| struct btrfs_inode* btrfs_inode = container_of(inode, struct btrfs_inode, vfs_inode); | ||
| key.inode = inode->i_ino; | ||
| key.inode = BPF_CORE_READ(inode, i_ino); | ||
| key.dev = BPF_CORE_READ(btrfs_inode, root, anon_dev); | ||
| break; | ||
| } | ||
| // If the btrfs_inode does not exist, most likely it is not | ||
| // supported on the system. Fallback to the generic implementation | ||
| // just in case. | ||
| default: | ||
| key.inode = inode->i_ino; | ||
| key.dev = inode->i_sb->s_dev; | ||
| key.inode = BPF_CORE_READ(inode, i_ino); | ||
| key.dev = BPF_CORE_READ(inode, i_sb, s_dev); | ||
| break; | ||
| } | ||
|
|
||
|
|
@@ -65,6 +65,14 @@ __always_inline static inode_value_t* inode_get(struct inode_key_t* inode) { | |
| return bpf_map_lookup_elem(&inode_map, inode); | ||
| } | ||
|
|
||
| __always_inline static long inode_add(struct inode_key_t* inode) { | ||
| if (inode == NULL) { | ||
| return -1; | ||
| } | ||
| inode_value_t value = 0; | ||
| return bpf_map_update_elem(&inode_map, inode, &value, BPF_ANY); | ||
| } | ||
|
|
||
| __always_inline static long inode_remove(struct inode_key_t* inode) { | ||
| if (inode == NULL) { | ||
| return 0; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,11 +47,24 @@ int BPF_PROG(trace_file_open, struct file* file) { | |
| inode_key_t inode_key = inode_to_key(file->f_inode); | ||
| inode_key_t* inode_to_submit = &inode_key; | ||
|
|
||
| // Extract parent inode | ||
| struct dentry* parent_dentry = BPF_CORE_READ(file, f_path.dentry, d_parent); | ||
| struct inode* parent_inode_ptr = parent_dentry ? BPF_CORE_READ(parent_dentry, d_inode) : NULL; | ||
| inode_key_t parent_key = inode_to_key(parent_inode_ptr); | ||
|
|
||
| // For file creation events, check if the parent directory is being | ||
| // monitored. If so, add the new file's inode to the tracked set. | ||
| if (event_type == FILE_ACTIVITY_CREATION) { | ||
| if (inode_is_monitored(inode_get(&parent_key)) == MONITORED) { | ||
| inode_add(&inode_key); | ||
| } | ||
| } | ||
|
Comment on lines
+55
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move some of this logic to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe. I tried this locally. It seemed to involve a lot of changes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My initial attempt didn't work. I might try again. |
||
|
|
||
| if (!is_monitored(inode_key, path, &inode_to_submit)) { | ||
| goto ignored; | ||
| } | ||
|
|
||
| submit_open_event(&m->file_open, event_type, path->path, inode_to_submit); | ||
| submit_open_event(&m->file_open, event_type, path->path, inode_to_submit, &parent_key); | ||
|
|
||
| return 0; | ||
|
|
||
|
|
@@ -86,7 +99,8 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { | |
|
|
||
| submit_unlink_event(&m->path_unlink, | ||
| path->path, | ||
| inode_to_submit); | ||
| inode_to_submit, | ||
| NULL); | ||
| return 0; | ||
| } | ||
|
|
||
|
|
@@ -118,6 +132,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) { | |
| submit_mode_event(&m->path_chmod, | ||
| bound_path->path, | ||
| inode_to_submit, | ||
| NULL, | ||
| mode, | ||
| old_mode); | ||
|
|
||
|
|
@@ -158,6 +173,7 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign | |
| submit_ownership_event(&m->path_chown, | ||
| bound_path->path, | ||
| inode_to_submit, | ||
| NULL, | ||
| uid, | ||
| gid, | ||
| old_uid, | ||
|
|
@@ -207,7 +223,8 @@ int BPF_PROG(trace_path_rename, struct path* old_dir, | |
| new_path->path, | ||
| old_path->path, | ||
| old_inode_submit, | ||
| new_inode_submit); | ||
| new_inode_submit, | ||
| NULL); | ||
| return 0; | ||
|
|
||
| error: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,6 +131,11 @@ impl HostScanner { | |
| self.update_entry(path.as_path()).with_context(|| { | ||
| format!("Failed to update entry for {}", path.display()) | ||
| })?; | ||
| } else if path.is_dir() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is slightly related to a conversation I've been having with @Stringy. IMO, in the case of finding a directory that matches a glob pattern, we should recursively scan into it, the reason being that tracking the directory itself isn't of much value if the pattern doesn't hit any files inside the directory. For now we can leave it like this, but I think we need to take a close look at what users might expect when configuring different types of glob patterns (i.e do they want that strict set of patterns to be monitored, or are they expressing a set of paths that work as the base to be monitored?) |
||
| self.metrics.scan_inc(ScanLabels::DirectoryScanned); | ||
| self.update_entry(path.as_path()).with_context(|| { | ||
| format!("Failed to update entry for {}", path.display()) | ||
| })?; | ||
| } else { | ||
| self.metrics.scan_inc(ScanLabels::FsItemIgnored); | ||
| } | ||
|
|
@@ -154,17 +159,26 @@ impl HostScanner { | |
| dev: metadata.st_dev(), | ||
| }; | ||
|
|
||
| let host_path = host_info::remove_host_mount(path); | ||
| self.update_entry_with_inode(&inode, host_path)?; | ||
|
|
||
| debug!("Added entry for {}: {inode:?}", path.display()); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Similar to update_entry except we are are directly using the inode instead of the path. | ||
| fn update_entry_with_inode(&self, inode: &inode_key_t, path: PathBuf) -> anyhow::Result<()> { | ||
| self.kernel_inode_map | ||
| .borrow_mut() | ||
| .insert(inode, 0, 0) | ||
| .insert(*inode, 0, 0) | ||
| .with_context(|| format!("Failed to insert kernel entry for {}", path.display()))?; | ||
|
|
||
| let mut inode_map = self.inode_map.borrow_mut(); | ||
| let entry = inode_map.entry(inode).or_default(); | ||
| *entry = host_info::remove_host_mount(path); | ||
| let entry = inode_map.entry(*inode).or_default(); | ||
| *entry = path; | ||
|
|
||
| self.metrics.scan_inc(ScanLabels::FileUpdated); | ||
|
|
||
| debug!("Added entry for {}: {inode:?}", path.display()); | ||
| Ok(()) | ||
| } | ||
|
|
||
|
|
@@ -178,6 +192,34 @@ impl HostScanner { | |
| self.inode_map.borrow().get(inode?).cloned() | ||
| } | ||
|
|
||
| /// Handle file creation events by adding new inodes to the map. | ||
| /// | ||
| /// We use the parent inode provided by the eBPF code | ||
| /// to look up the parent directory's host path, then construct the full | ||
| /// path by appending the new file's name. | ||
| fn handle_creation_event(&self, event: &Event) -> anyhow::Result<()> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm hoping this is still WIP and the debug statements are for testing, we really don't need a debug statement on each return condition, particularly not in the hot path where monitoring sets of files mildly noisy will just flood the logs and make it hard to see what is actually going on. Please cut down on the debug statements, if something is expected to happen, don't put a statement there.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried removing all the debug statements in this method and managed to boil it down from 50+ lines to just this: let inode = event.get_inode();
let parent_inode = event.get_parent_inode();
if self.get_host_path(Some(inode)).is_some() || parent_inode.empty() {
return Ok(());
}
if let Some(filename) = event.get_filename().file_name()
&& let Some(parent_host_path) = self.get_host_path(Some(parent_inode))
{
let host_path = parent_host_path.join(filename);
self.update_entry_with_inode(inode, host_path)
.with_context(|| {
format!(
"Failed to add creation event entry for {}",
filename.display()
)
})?;
}
Ok(())Given this is a lot more compact and easy to ready and the additional verbosity in the logs the implementation in the PR doesn't bring much of a benefit IMO, I would propose we try to move the implementation closer to this. |
||
| let inode = event.get_inode(); | ||
| let parent_inode = event.get_parent_inode(); | ||
| if self.get_host_path(Some(inode)).is_some() || parent_inode.empty() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| if let Some(filename) = event.get_filename().file_name() { | ||
| if let Some(parent_host_path) = self.get_host_path(Some(parent_inode)) { | ||
| let host_path = parent_host_path.join(filename); | ||
| self.update_entry_with_inode(inode, host_path) | ||
| .with_context(|| { | ||
| format!( | ||
| "Failed to add creation event entry for {}", | ||
| filename.display() | ||
| ) | ||
| })?; | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Periodically notify the host scanner main task that a scan needs | ||
| /// to happen. | ||
| /// | ||
|
|
@@ -219,6 +261,13 @@ impl HostScanner { | |
| }; | ||
| self.metrics.events.added(); | ||
|
|
||
| // Handle file creation events by adding new inodes to the map | ||
| if event.is_creation() { | ||
| if let Err(e) = self.handle_creation_event(&event) { | ||
| warn!("Failed to handle creation event: {e}"); | ||
| } | ||
| } | ||
|
|
||
| if let Some(host_path) = self.get_host_path(Some(event.get_inode())) { | ||
| self.metrics.scan_inc(ScanLabels::InodeHit); | ||
| event.set_host_path(host_path); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parent inode was added to the event so that it could be used to find the parent path. It is added for all event types, not just when a file is opened. Perhaps NULL could be passed instead of the actual parent inode for the other type of events for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, passing
NULLshould be enough for nowThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done