Skip to content

Commit f43a667

Browse files
committed
feat: add exectrack cli
1 parent 2ced094 commit f43a667

15 files changed

Lines changed: 535 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ rstest_reuse = "0.7.0"
8080
shell-quote = "0.7.2"
8181

8282
[workspace]
83-
members = ["crates/runner-shared", "crates/memtrack", "crates/codspeed-bpf"]
83+
members = ["crates/runner-shared", "crates/memtrack", "crates/codspeed-bpf", "crates/exectrack"]
8484

8585
[workspace.dependencies]
8686
anyhow = "1.0"

crates/codspeed-bpf/src/build.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub fn build_bpf(program_name: &str, source_file: &str) {
4040
.expect("CARGO_CFG_TARGET_ARCH must be set in build script");
4141

4242
let output =
43-
PathBuf::from(env::var("OUT_DIR").unwrap()).join(format!("{}.skel.rs", program_name));
43+
PathBuf::from(env::var("OUT_DIR").unwrap()).join(format!("{program_name}.skel.rs"));
4444

4545
// Get the path to codspeed-bpf's C headers
4646
let codspeed_bpf_include = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
@@ -57,7 +57,7 @@ pub fn build_bpf(program_name: &str, source_file: &str) {
5757
&codspeed_bpf_include.to_string_lossy(),
5858
])
5959
.build_and_generate(&output)
60-
.expect(&format!("Failed to build {}.bpf.c", program_name));
60+
.unwrap_or_else(|_| panic!("Failed to build {program_name}.bpf.c"));
6161
}
6262

6363
/// Generate Rust bindings for a C header file

crates/exectrack/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "exectrack"
3+
version = "0.1.0"
4+
edition = "2024"
5+
repository = "https://github.com/CodSpeedHQ/runner"
6+
7+
[lib]
8+
name = "exectrack"
9+
path = "src/lib.rs"
10+
11+
[[bin]]
12+
name = "codspeed-exectrack"
13+
path = "src/main.rs"
14+
15+
[dependencies]
16+
anyhow = { workspace = true }
17+
clap = { workspace = true }
18+
libc = { workspace = true }
19+
log = { workspace = true }
20+
env_logger = { workspace = true }
21+
serde_json = { workspace = true }
22+
serde = { workspace = true }
23+
static_assertions = "1.1"
24+
libbpf-rs = { version = "0.25.0", features = ["vendored"] }
25+
codspeed-bpf = { path = "../codspeed-bpf" }
26+
runner-shared = { path = "../runner-shared" }
27+
28+
[build-dependencies]
29+
codspeed-bpf = { path = "../codspeed-bpf", features = ["build"] }

crates/exectrack/build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fn main() {
2+
codspeed_bpf::build::build_bpf("exectrack", "src/ebpf/c/exectrack.bpf.c");
3+
codspeed_bpf::build::generate_bindings("wrapper.h");
4+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#ifndef __EXECTRACK_EVENT_H__
2+
#define __EXECTRACK_EVENT_H__
3+
4+
#define EVENT_TYPE_FORK 1
5+
#define EVENT_TYPE_EXEC 2
6+
#define EVENT_TYPE_EXIT 3
7+
8+
/* Event structure - shared between BPF and userspace */
9+
struct event {
10+
uint8_t event_type; /* See EVENT_TYPE_* constants above */
11+
uint64_t timestamp; /* monotonic time in nanoseconds (CLOCK_MONOTONIC) */
12+
uint32_t pid; /* Process ID */
13+
uint32_t tid; /* Thread ID */
14+
uint32_t ppid; /* Parent Process ID (for fork events) */
15+
char comm[16]; /* Command name (null-terminated) */
16+
};
17+
18+
#endif /* __EXECTRACK_EVENT_H__ */
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// clang-format off
2+
#include "vmlinux.h"
3+
// clang-format on
4+
5+
#include <bpf/bpf_core_read.h>
6+
#include <bpf/bpf_helpers.h>
7+
#include <bpf/bpf_tracing.h>
8+
9+
#include "event.h"
10+
11+
// Include shared BPF utilities from codspeed-bpf
12+
#include "codspeed/common.h"
13+
#include "codspeed/process_tracking.h"
14+
15+
char LICENSE[] SEC("license") = "GPL";
16+
17+
// Define standard process tracking maps using shared macro
18+
PROCESS_TRACKING_MAPS();
19+
20+
// Define ring buffer for events
21+
BPF_RINGBUF(events, 256 * 1024);
22+
23+
/* Helper to submit an event to the ring buffer */
24+
static __always_inline int submit_event(__u8 event_type, __u32 pid, __u32 ppid) {
25+
if (!is_tracked(pid) && !is_tracked(ppid)) {
26+
return 0;
27+
}
28+
29+
struct event* e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
30+
if (!e) {
31+
return 0;
32+
}
33+
34+
__u64 tid_full = bpf_get_current_pid_tgid();
35+
36+
e->timestamp = bpf_ktime_get_ns();
37+
e->pid = pid;
38+
e->tid = tid_full & 0xFFFFFFFF;
39+
e->ppid = ppid;
40+
e->event_type = event_type;
41+
42+
// Get current command name
43+
bpf_get_current_comm(e->comm, sizeof(e->comm));
44+
45+
bpf_ringbuf_submit(e, 0);
46+
return 0;
47+
}
48+
49+
/* Track process creation via fork/clone */
50+
SEC("tracepoint/sched/sched_process_fork")
51+
int tracepoint_sched_fork(struct trace_event_raw_sched_process_fork* ctx) {
52+
__u32 parent_pid = ctx->parent_pid;
53+
__u32 child_pid = ctx->child_pid;
54+
55+
// Use shared fork handler to track child
56+
if (handle_fork(parent_pid, child_pid)) {
57+
// Submit fork event with parent/child relationship
58+
submit_event(EVENT_TYPE_FORK, child_pid, parent_pid);
59+
}
60+
61+
return 0;
62+
}
63+
64+
/* Track process execution via execve */
65+
SEC("tracepoint/sched/sched_process_exec")
66+
int tracepoint_sched_exec(struct trace_event_raw_sched_process_exec* ctx) {
67+
__u64 tid = bpf_get_current_pid_tgid();
68+
__u32 pid = tid >> 32;
69+
70+
if (is_tracked(pid)) {
71+
submit_event(EVENT_TYPE_EXEC, pid, 0);
72+
}
73+
74+
return 0;
75+
}
76+
77+
/* Track process termination via exit */
78+
SEC("tracepoint/sched/sched_process_exit")
79+
int tracepoint_sched_exit(struct trace_event_raw_sched_process_template* ctx) {
80+
__u64 tid = bpf_get_current_pid_tgid();
81+
__u32 pid = tid >> 32;
82+
83+
if (is_tracked(pid)) {
84+
submit_event(EVENT_TYPE_EXIT, pid, 0);
85+
// Use shared exit handler to clean up maps
86+
handle_exit(pid);
87+
}
88+
89+
return 0;
90+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
// Include the bindings for event.h
4+
pub mod bindings {
5+
#![allow(non_upper_case_globals)]
6+
#![allow(non_camel_case_types)]
7+
#![allow(non_snake_case)]
8+
#![allow(dead_code)]
9+
10+
include!(concat!(env!("OUT_DIR"), "/event.rs"));
11+
}
12+
use bindings::*;
13+
14+
#[repr(u8)]
15+
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
16+
pub enum EventType {
17+
Fork = EVENT_TYPE_FORK as u8,
18+
Exec = EVENT_TYPE_EXEC as u8,
19+
Exit = EVENT_TYPE_EXIT as u8,
20+
}
21+
22+
impl From<u8> for EventType {
23+
fn from(val: u8) -> Self {
24+
match val as u32 {
25+
bindings::EVENT_TYPE_FORK => EventType::Fork,
26+
bindings::EVENT_TYPE_EXEC => EventType::Exec,
27+
bindings::EVENT_TYPE_EXIT => EventType::Exit,
28+
_ => panic!("Unknown event type: {val}"),
29+
}
30+
}
31+
}
32+
33+
#[repr(C)]
34+
#[derive(Debug, Clone, Copy)]
35+
pub struct Event {
36+
pub event_type: u8,
37+
pub timestamp: u64,
38+
pub pid: u32,
39+
pub tid: u32,
40+
pub ppid: u32,
41+
pub comm: [u8; 16],
42+
}
43+
44+
impl Event {
45+
/// Get the event type as an enum
46+
pub fn event_type(&self) -> EventType {
47+
EventType::from(self.event_type)
48+
}
49+
50+
/// Get the command name as a string
51+
pub fn comm_str(&self) -> &str {
52+
let len = self.comm.iter().position(|&c| c == 0).unwrap_or(16);
53+
std::str::from_utf8(&self.comm[..len]).unwrap_or("<invalid>")
54+
}
55+
}
56+
57+
// Static assertions for C/Rust ABI safety
58+
mod assertions {
59+
use super::*;
60+
use static_assertions::{assert_eq_align, assert_eq_size, const_assert_eq};
61+
use std::mem::offset_of;
62+
63+
assert_eq_size!(Event, bindings::event);
64+
assert_eq_align!(Event, bindings::event);
65+
66+
const_assert_eq!(
67+
offset_of!(Event, timestamp),
68+
offset_of!(bindings::event, timestamp)
69+
);
70+
const_assert_eq!(offset_of!(Event, pid), offset_of!(bindings::event, pid));
71+
const_assert_eq!(offset_of!(Event, tid), offset_of!(bindings::event, tid));
72+
const_assert_eq!(
73+
offset_of!(Event, event_type),
74+
offset_of!(bindings::event, event_type)
75+
);
76+
const_assert_eq!(offset_of!(Event, comm), offset_of!(bindings::event, comm));
77+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use anyhow::{Context, Result};
2+
use codspeed_bpf::ProcessTracking;
3+
use codspeed_bpf::{RingBufferPoller, attach_tracepoint};
4+
use libbpf_rs::Link;
5+
use libbpf_rs::skel::{OpenSkel, SkelBuilder};
6+
use std::mem::MaybeUninit;
7+
8+
pub mod exectrack_skel {
9+
include!(concat!(env!("OUT_DIR"), "/exectrack.skel.rs"));
10+
}
11+
pub use exectrack_skel::*;
12+
13+
pub struct ExectrackBpf {
14+
skel: Box<ExectrackSkel<'static>>,
15+
probes: Vec<Link>,
16+
}
17+
18+
impl ExectrackBpf {
19+
pub fn new() -> Result<Self> {
20+
let builder = ExectrackSkelBuilder::default();
21+
let open_object = Box::leak(Box::new(MaybeUninit::uninit()));
22+
let open_skel = builder
23+
.open(open_object)
24+
.context("Failed to open exectrack BPF skeleton")?;
25+
26+
let skel = Box::new(
27+
open_skel
28+
.load()
29+
.context("Failed to load exectrack BPF skeleton")?,
30+
);
31+
32+
Ok(Self {
33+
skel,
34+
probes: Vec::new(),
35+
})
36+
}
37+
38+
// Use the shared macro from codspeed-bpf for attaching tracepoints
39+
attach_tracepoint!(attach_sched_fork, tracepoint_sched_fork);
40+
attach_tracepoint!(attach_sched_exec, tracepoint_sched_exec);
41+
attach_tracepoint!(attach_sched_exit, tracepoint_sched_exit);
42+
43+
pub fn attach_tracepoints(&mut self) -> Result<()> {
44+
self.attach_sched_fork()?;
45+
self.attach_sched_exec()?;
46+
self.attach_sched_exit()?;
47+
Ok(())
48+
}
49+
50+
/// Start polling with an mpsc channel for events
51+
pub fn start_polling_with_channel(
52+
&self,
53+
poll_timeout_ms: u64,
54+
) -> Result<(
55+
RingBufferPoller,
56+
std::sync::mpsc::Receiver<super::events::Event>,
57+
)> {
58+
RingBufferPoller::with_channel(&self.skel.maps.events, poll_timeout_ms)
59+
}
60+
}
61+
62+
impl ProcessTracking for ExectrackBpf {
63+
fn tracked_pids_map(&self) -> &impl libbpf_rs::MapCore {
64+
&self.skel.maps.tracked_pids
65+
}
66+
}

crates/exectrack/src/ebpf/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub mod events;
2+
mod exectrack;
3+
mod tracker;
4+
5+
pub use events::{Event, EventType};
6+
pub use exectrack::ExectrackBpf;
7+
pub use tracker::Tracker;

0 commit comments

Comments
 (0)