Skip to content

Commit bdbefbc

Browse files
authored
feat(hot-reload): reload paths on the fly (#128)
This patch makes it so detaching and reattaching programs is no longer needed for updating the list of monitored paths. This is achieved by first loading all the new paths into the BPF trie map and then removing the entries that are not part of the new configuration. An integration test was added to ensure adding new paths does not remove existing paths accidentally.
1 parent 556f3c8 commit bdbefbc

File tree

3 files changed

+60
-22
lines changed

3 files changed

+60
-22
lines changed

fact-ebpf/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ impl From<path_prefix_t> for lpm_trie::Key<[c_char; LPM_SIZE_MAX as usize]> {
5454
}
5555
}
5656

57+
impl PartialEq for path_prefix_t {
58+
fn eq(&self, other: &Self) -> bool {
59+
self.bit_len == other.bit_len && self.path == other.path
60+
}
61+
}
62+
5763
unsafe impl Pod for path_prefix_t {}
5864

5965
impl metrics_by_hook_t {

fact/src/bpf.rs

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{io, path::PathBuf, sync::Arc};
33
use anyhow::{bail, Context};
44
use aya::{
55
maps::{Array, LpmTrie, MapData, PerCpuArray, RingBuf},
6-
programs::{lsm::LsmLink, Lsm},
6+
programs::Lsm,
77
Btf, Ebpf,
88
};
99
use libc::c_char;
@@ -27,7 +27,6 @@ pub struct Bpf {
2727

2828
paths: Vec<path_prefix_t>,
2929
paths_config: watch::Receiver<Vec<PathBuf>>,
30-
links: Vec<LsmLink>,
3130
}
3231

3332
impl Bpf {
@@ -44,15 +43,13 @@ impl Bpf {
4443
.set_max_entries(RINGBUFFER_NAME, ringbuf_size * 1024)
4544
.load(fact_ebpf::EBPF_OBJ)?;
4645

47-
let links = Vec::new();
4846
let paths = Vec::new();
4947
let (tx, _) = broadcast::channel(100);
5048
let mut bpf = Bpf {
5149
obj,
5250
tx,
5351
paths,
5452
paths_config,
55-
links,
5653
};
5754

5855
bpf.load_paths()?;
@@ -113,19 +110,21 @@ impl Bpf {
113110
let mut path_prefix: LpmTrie<&mut MapData, [c_char; LPM_SIZE_MAX as usize], c_char> =
114111
LpmTrie::try_from(path_prefix)?;
115112

116-
// Remove old prefixes
117-
for p in &self.paths {
118-
path_prefix.remove(&(*p).into())?;
119-
}
120-
self.paths.clear();
121-
122-
// Add the new ones
113+
// Add the new prefixes
114+
let mut new_paths = Vec::with_capacity(paths_config.len());
123115
for p in paths_config.iter() {
124116
let prefix = path_prefix_t::try_from(p)?;
125117
path_prefix.insert(&prefix.into(), 0, 0)?;
126-
self.paths.push(prefix);
118+
new_paths.push(prefix);
127119
}
128120

121+
// Remove old prefixes
122+
for p in self.paths.iter().filter(|p| !new_paths.contains(p)) {
123+
path_prefix.remove(&(*p).into())?;
124+
}
125+
126+
self.paths = new_paths;
127+
129128
Ok(())
130129
}
131130

@@ -147,16 +146,11 @@ impl Bpf {
147146
fn attach_progs(&mut self) -> anyhow::Result<()> {
148147
for (_, prog) in self.obj.programs_mut() {
149148
let prog: &mut Lsm = prog.try_into()?;
150-
let id = prog.attach()?;
151-
self.links.push(prog.take_link(id)?);
149+
prog.attach()?;
152150
}
153151
Ok(())
154152
}
155153

156-
fn detach_progs(&mut self) {
157-
self.links.clear();
158-
}
159-
160154
// Gather events from the ring buffer and print them out.
161155
pub fn start(
162156
mut self,
@@ -199,11 +193,7 @@ impl Bpf {
199193
guard.clear_ready();
200194
},
201195
_ = self.paths_config.changed() => {
202-
info!("Reloading path configuration...");
203-
self.detach_progs();
204196
self.load_paths().context("Failed to load paths")?;
205-
self.attach_progs().context("Failed to re-attach eBPF programs")?;
206-
info!("Done reloading path configuration");
207197
},
208198
_ = running.changed() => {
209199
if !*running.borrow() {

tests/test_config_hotreload.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,45 @@ def test_paths(fact, fact_config, monitored_dir, ignored_dir, server):
160160
print(f'Ignoring: {ignored_event}')
161161

162162
server.wait_events([e], ignored=[ignored_event])
163+
164+
165+
def test_paths_addition(fact, fact_config, monitored_dir, ignored_dir, server):
166+
p = Process()
167+
168+
# Ignored file, must not show up in the server
169+
ignored_file = os.path.join(ignored_dir, 'test.txt')
170+
with open(ignored_file, 'w') as f:
171+
f.write('This is to be ignored')
172+
173+
ignored_event = Event(
174+
process=p, event_type=EventType.CREATION, file=ignored_file)
175+
print(f'Ignoring: {ignored_event}')
176+
177+
# File Under Test
178+
fut = os.path.join(monitored_dir, 'test.txt')
179+
with open(fut, 'w') as f:
180+
f.write('This is a test')
181+
182+
e = Event(process=p, event_type=EventType.CREATION, file=fut)
183+
print(f'Waiting for event: {e}')
184+
185+
server.wait_events([e], ignored=[ignored_event])
186+
187+
config, config_file = fact_config
188+
config['paths'] = [monitored_dir, ignored_dir]
189+
reload_config(fact, config, config_file, delay=0.5)
190+
191+
# At this point, the event in the ignored directory should show up
192+
# alongside the regular event
193+
with open(ignored_file, 'w') as f:
194+
f.write('This is another test')
195+
with open(fut, 'w') as f:
196+
f.write('This is one final event')
197+
198+
events = [
199+
Event(process=p, event_type=EventType.OPEN, file=ignored_file),
200+
Event(process=p, event_type=EventType.OPEN, file=fut)
201+
]
202+
print(f'Waiting for events: {events}')
203+
204+
server.wait_events(events)

0 commit comments

Comments
 (0)