Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 30 additions & 35 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ aya = { version = "0.13.1", default-features = false }
anyhow = { version = "1", default-features = false, features = ["std", "backtrace"] }
clap = { version = "4.5.41", features = ["derive", "env"] }
env_logger = { version = "0.11.5", default-features = false, features = ["humantime"] }
glob = "0.3.3"
globset = "0.4.18"
http-body-util = "0.1.3"
hyper = { version = "1.6.0", default-features = false }
hyper-tls = "0.6.0"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ In order to run these tests as part of the unit test suite y use the
following command:

```shell
cargo test --config 'target."cfg(all())".runner="sudo -E" --features=bpf-test
cargo test --config 'target."cfg(all())".runner="sudo -E"' --features=bpf-test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this one!

```

## Create compile_commands.json
Expand Down
24 changes: 20 additions & 4 deletions fact-ebpf/src/bpf/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,21 @@ int BPF_PROG(trace_file_open, struct file* file) {

inode_key_t inode_key = inode_to_key(file->f_inode);
const inode_value_t* inode = inode_get(&inode_key);
inode_key_t* inode_to_submit = &inode_key;
switch (inode_is_monitored(inode)) {
case NOT_MONITORED:
if (!is_monitored(path)) {
goto ignored;
}
// Matched by path prefix only, not by inode.
// Set inode to NULL so userspace knows to do glob matching.
inode_to_submit = NULL;
break;
case MONITORED:
break;
}

submit_event(&m->file_open, event_type, path->path, &inode_key, true);
submit_event(&m->file_open, event_type, path->path, inode_to_submit, true);
Comment on lines 47 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to use a map because the inode_key_t type is small, but we might want to have a per cpu array map and use a pointer to that to simplify the code even further. It would also reduce the amount of stack used for every program, which might be helpful in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyways, I don't want to block the PR on this, we can do it in a follow up.


return 0;

Expand Down Expand Up @@ -100,13 +104,17 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) {

inode_key_t inode_key = inode_to_key(dentry->d_inode);
const inode_value_t* inode = inode_get(&inode_key);
inode_key_t* inode_to_submit = &inode_key;

switch (inode_is_monitored(inode)) {
case NOT_MONITORED:
if (!is_monitored(path)) {
m->path_unlink.ignored++;
return 0;
}
// Matched by path prefix only, not by inode.
// Set inode to NULL so userspace knows to do glob matching.
inode_to_submit = NULL;
break;

case MONITORED:
Expand All @@ -117,7 +125,7 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) {
submit_event(&m->path_unlink,
FILE_ACTIVITY_UNLINK,
path->path,
&inode_key,
inode_to_submit,
path_hooks_support_bpf_d_path);
return 0;

Expand Down Expand Up @@ -150,13 +158,17 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) {

inode_key_t inode_key = inode_to_key(path->dentry->d_inode);
const inode_value_t* inode = inode_get(&inode_key);
inode_key_t* inode_to_submit = &inode_key;

switch (inode_is_monitored(inode)) {
case NOT_MONITORED:
if (!is_monitored(bound_path)) {
m->path_chmod.ignored++;
return 0;
}
// Matched by path prefix only, not by inode.
// Set inode to NULL so userspace knows to do glob matching.
inode_to_submit = NULL;
break;

case MONITORED:
Expand All @@ -166,7 +178,7 @@ int BPF_PROG(trace_path_chmod, struct path* path, umode_t mode) {
umode_t old_mode = BPF_CORE_READ(path, dentry, d_inode, i_mode);
submit_mode_event(&m->path_chmod,
bound_path->path,
&inode_key,
inode_to_submit,
mode,
old_mode,
path_hooks_support_bpf_d_path);
Expand Down Expand Up @@ -201,13 +213,17 @@ int BPF_PROG(trace_path_chown, struct path* path, unsigned long long uid, unsign

inode_key_t inode_key = inode_to_key(path->dentry->d_inode);
const inode_value_t* inode = inode_get(&inode_key);
inode_key_t* inode_to_submit = &inode_key;

switch (inode_is_monitored(inode)) {
case NOT_MONITORED:
if (!is_monitored(bound_path)) {
m->path_chown.ignored++;
return 0;
}
// Matched by path prefix only, not by inode.
// Set inode to NULL so userspace knows to do glob matching.
inode_to_submit = NULL;
break;

case MONITORED:
Expand All @@ -220,7 +236,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_key,
inode_to_submit,
uid,
gid,
old_uid,
Expand Down
23 changes: 17 additions & 6 deletions fact-ebpf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,22 @@ impl TryFrom<&PathBuf> for path_prefix_t {
prefix: value.display().to_string(),
});
};
let len = if filename.len() > LPM_SIZE_MAX as usize {
LPM_SIZE_MAX as usize
} else {
filename.len()
};

// Take the start of the path until the first occurence of a wildcard
// character. This is used as a filter in the kernel in cases where
// the inode has failed to match. The full wildcard string is used
// for further processing in userspace.
//
// unwrap is safe here - if there are no matches, the full string is the
// only item in the iterator
let filename_prefix = filename.split(['*', '?', '[', '{']).next().unwrap();
let len = filename_prefix.len().min(LPM_SIZE_MAX as usize);

unsafe {
let mut cfg: path_prefix_t = std::mem::zeroed();
memcpy(
cfg.path.as_mut_ptr() as *mut _,
filename.as_ptr() as *const _,
filename_prefix.as_ptr() as *const _,
len,
);
cfg.bit_len = (len * 8) as u32;
Expand All @@ -63,6 +68,12 @@ impl PartialEq for path_prefix_t {

unsafe impl Pod for path_prefix_t {}

impl inode_key_t {
pub fn empty(&self) -> bool {
self.inode == 0 && self.dev == 0
}
}

impl PartialEq for inode_key_t {
fn eq(&self, other: &Self) -> bool {
self.inode == other.inode && self.dev == other.dev
Expand Down
2 changes: 2 additions & 0 deletions fact/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ anyhow = { workspace = true }
aya = { workspace = true }
clap = { workspace = true }
env_logger = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }
http-body-util = { workspace = true }
hyper = { workspace = true }
hyper-tls = { workspace = true }
Expand Down
34 changes: 32 additions & 2 deletions fact/src/bpf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use aya::{
Btf, Ebpf,
};
use checks::Checks;
use globset::{Glob, GlobSet, GlobSetBuilder};
use libc::c_char;
use log::{error, info};
use tokio::{
Expand All @@ -30,6 +31,8 @@ pub struct Bpf {

paths: Vec<path_prefix_t>,
paths_config: watch::Receiver<Vec<PathBuf>>,

paths_globset: GlobSet,
}

impl Bpf {
Expand Down Expand Up @@ -61,6 +64,7 @@ impl Bpf {
tx,
paths,
paths_config,
paths_globset: GlobSet::empty(),
};

bpf.load_paths()?;
Expand Down Expand Up @@ -127,11 +131,23 @@ impl Bpf {

// Add the new prefixes
let mut new_paths = Vec::with_capacity(paths_config.len());
let mut builder = GlobSetBuilder::new();
for p in paths_config.iter() {
let Some(glob_str) = p.to_str() else {
bail!("failed to convert path {} to string", p.display());
};

builder.add(
Glob::new(glob_str)
.with_context(|| format!("invalid glob {}", glob_str))
.unwrap(),
);

let prefix = path_prefix_t::try_from(p)?;
path_prefix.insert(&prefix.into(), 0, 0)?;
new_paths.push(prefix);
}
self.paths_globset = builder.build()?;

// Remove old prefixes
for p in self.paths.iter().filter(|p| !new_paths.contains(p)) {
Expand Down Expand Up @@ -193,7 +209,21 @@ impl Bpf {
while let Some(event) = ringbuf.next() {
let event: &event_t = unsafe { &*(event.as_ptr() as *const _) };
let event = match Event::try_from(event) {
Ok(event) => event,
Ok(event) => {
// With wildcards, the kernel can only match on the inode and
// then the longest non-wildcard prefix (e.g. for /etc/**/*.conf,
// the kernel matches up to /etc/)
//
// The kernel sets inode to 0 when it matched via path prefix only.
// so we only need to perform a glob match against the filename
if !event.get_inode().empty() ||
self.paths_globset.is_match(event.get_filename()) {
event
} else {
event_counter.dropped();
continue;
}
},
Err(e) => {
error!("Failed to parse event: '{e}'");
event_counter.dropped();
Expand Down Expand Up @@ -253,7 +283,7 @@ mod bpf_tests {

let monitored_path = env!("CARGO_MANIFEST_DIR");
let monitored_path = PathBuf::from(monitored_path);
let paths = vec![monitored_path.clone()];
let paths = vec![PathBuf::from(format!("{}/**/*", monitored_path.display()))];
let mut config = FactConfig::default();
config.set_paths(paths);
let reloader = Reloader::from(config);
Expand Down
Loading