Skip to content

Commit 66fdcde

Browse files
author
Ricardo Maraschini
committed
feat: initial commit
the initial basic implementation is here: 1. capture all signals through the generate_signal trace. 2. pipes the messages from the threads to the main task. 3. uses sync channels.
1 parent 0d7299b commit 66fdcde

File tree

16 files changed

+475
-2
lines changed

16 files changed

+475
-2
lines changed

.gitignore

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
### https://raw.github.com/github/gitignore/master/Rust.gitignore
2+
3+
# Generated by Cargo
4+
# will have compiled files and executables
5+
debug/
6+
target/
7+
8+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
9+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
10+
Cargo.lock
11+
12+
# These are backup files generated by rustfmt
13+
**/*.rs.bk
14+
15+
.vim/
16+
.vscode/

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[workspace]
2+
members = ["signals", "signals-common", "xtask"]

README.md

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,32 @@
1-
# ebpf-signals
2-
An eBPF engine for capturing and processing signals
1+
# Signals
2+
3+
An eBPF based engine to capture and process signals being sent.
4+
5+
6+
## Prerequisites
7+
8+
1. Install a rust stable toolchain: `rustup install stable`
9+
2. Install a rust nightly toolchain: `rustup install nightly`
10+
3. Install bpf-linker: `cargo install bpf-linker`
11+
4. Install rust-src: `rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu`
12+
13+
## Build eBPF
14+
15+
```bash
16+
cargo xtask build-ebpf
17+
```
18+
19+
To perform a release build you can use the `--release` flag.
20+
You may also change the target architecture with the `--target` flag
21+
22+
## Build Userspace
23+
24+
```bash
25+
cargo build
26+
```
27+
28+
## Run
29+
30+
```bash
31+
sudo RUST_LOG=debug ./target/debug/signals
32+
```

signals-common/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "signals-common"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[features]
7+
default = []
8+
user = [ "aya" ]
9+
10+
[dependencies]
11+
aya = { version = ">=0.11", optional=true }
12+
13+
[lib]
14+
path = "src/lib.rs"

signals-common/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#![no_std]
2+
3+
#[repr(C)]
4+
#[derive(Clone, Copy, Debug)]
5+
pub struct Signal {
6+
pub signr: i32,
7+
pub pid: u32,
8+
}

signals-ebpf/.cargo/config.toml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build]
2+
target-dir = "../target"
3+
target = "bpfel-unknown-none"
4+
5+
[unstable]
6+
build-std = ["core"]

signals-ebpf/Cargo.toml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "signals-ebpf"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
aya-bpf = { git = "https://github.com/aya-rs/aya", branch = "main" }
8+
aya-log-ebpf = { git = "https://github.com/aya-rs/aya", branch = "main" }
9+
signals-common = { path = "../signals-common" }
10+
11+
[[bin]]
12+
name = "signals"
13+
path = "src/main.rs"
14+
15+
[profile.dev]
16+
opt-level = 3
17+
debug = false
18+
debug-assertions = false
19+
overflow-checks = false
20+
lto = true
21+
panic = "abort"
22+
incremental = false
23+
codegen-units = 1
24+
rpath = false
25+
26+
[profile.release]
27+
lto = true
28+
panic = "abort"
29+
codegen-units = 1
30+
31+
[workspace]
32+
members = []

signals-ebpf/rust-toolchain.toml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[toolchain]
2+
channel="nightly"

signals-ebpf/src/main.rs

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#![no_std]
2+
#![no_main]
3+
4+
use aya_bpf::macros::map;
5+
use aya_bpf::macros::tracepoint;
6+
use aya_bpf::maps::PerfEventArray;
7+
use aya_bpf::programs::TracePointContext;
8+
use aya_log_ebpf::debug;
9+
use signals_common::Signal;
10+
11+
#[map(name = "SIGNALS")] //
12+
static mut SIGNALS: PerfEventArray<Signal> = PerfEventArray::<Signal>::with_max_entries(1024, 0);
13+
14+
// these offsets are obtained by reading the following file:
15+
// /sys/kernel/debug/tracing/events/signal/signal_generate/format
16+
const SIGNAL_OFFSET: usize = 8;
17+
const PID_OFFSET: usize = 36;
18+
19+
#[tracepoint(name = "signals")]
20+
pub fn signals(ctx: TracePointContext) -> u32 {
21+
match try_signals(ctx) {
22+
Ok(ret) => ret,
23+
Err(ret) => ret,
24+
}
25+
}
26+
27+
fn try_signals(ctx: TracePointContext) -> Result<u32, u32> {
28+
let signr: i32 = unsafe {
29+
match ctx.read_at(SIGNAL_OFFSET) {
30+
Ok(s) => s,
31+
Err(errn) => return Err(errn as u32),
32+
}
33+
};
34+
35+
let pid: u32 = unsafe {
36+
match ctx.read_at(PID_OFFSET) {
37+
Ok(s) => s,
38+
Err(errn) => return Err(errn as u32),
39+
}
40+
};
41+
42+
let s = Signal { signr, pid };
43+
unsafe {
44+
debug!(&ctx, "ebpf: enqueued signal {} for {}", signr, pid);
45+
SIGNALS.output(&ctx, &s, 0);
46+
}
47+
Ok(0)
48+
}
49+
50+
#[panic_handler]
51+
fn panic(_info: &core::panic::PanicInfo) -> ! {
52+
unsafe { core::hint::unreachable_unchecked() }
53+
}

signals/Cargo.toml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "signals"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
aya = { version = ">=0.11", features=["async_tokio"] }
9+
aya-log = "0.1"
10+
signals-common = { path = "../signals-common", features=["user"] }
11+
anyhow = "1.0.42"
12+
clap = { version = "3.1", features = ["derive"] }
13+
env_logger = "0.9"
14+
log = "0.4"
15+
tokio = { version = "1.18", features = ["full"] }
16+
bytes = "1"
17+
prost = "0.10.4"
18+
tonic = "0.7.2"
19+
20+
[[bin]]
21+
name = "signals"
22+
path = "src/main.rs"
23+
24+
[build-dependencies]
25+
tonic-build = "0.7.2"

signals/src/emitters.rs

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use aya::include_bytes_aligned;
2+
use aya::maps::perf::AsyncPerfEventArray;
3+
use aya::programs::TracePoint;
4+
use aya::util::online_cpus;
5+
use aya::Bpf;
6+
use aya_log::BpfLogger;
7+
use bytes::BytesMut;
8+
use log::debug;
9+
use log::error;
10+
use signals_common::Signal;
11+
use std::mem::size_of;
12+
use std::sync::mpsc::SyncSender;
13+
use tokio::task;
14+
15+
pub struct SignalEmitter {
16+
bpf: Bpf,
17+
dst: SyncSender<Signal>,
18+
}
19+
20+
impl SignalEmitter {
21+
// new returns a new Signal that loads a bpf program from disk and loads it
22+
// into the kernel. signals are written to the destination channel.
23+
pub fn new(dst: SyncSender<Signal>) -> Result<Self, anyhow::Error> {
24+
#[cfg(debug_assertions)]
25+
let mut bpf = Bpf::load(include_bytes_aligned!(
26+
"../../target/bpfel-unknown-none/debug/signals"
27+
))?;
28+
29+
#[cfg(not(debug_assertions))]
30+
let mut bpf = Bpf::load(include_bytes_aligned!(
31+
"../../target/bpfel-unknown-none/release/signals"
32+
))?;
33+
34+
BpfLogger::init(&mut bpf).unwrap();
35+
36+
let program: &mut TracePoint = bpf.program_mut("signals").unwrap().try_into()?;
37+
program.load()?;
38+
program.attach("signal", "signal_generate")?;
39+
40+
Ok(Self { bpf, dst })
41+
}
42+
43+
// attach starts to read signal events from the kernel and pipe them through the
44+
// destination channel. spawns a task per cpu, each task process events from its
45+
// own perf array.
46+
pub fn attach(&mut self) -> Result<(), anyhow::Error> {
47+
let cpus = online_cpus()?;
48+
let mut senders: Vec<SyncSender<Signal>> = vec![];
49+
for _ in 0..cpus.len() {
50+
senders.push(self.dst.clone());
51+
}
52+
53+
let signal_struct_size: usize = size_of::<Signal>();
54+
let mut perf_array = AsyncPerfEventArray::try_from(self.bpf.map_mut("SIGNALS")?)?;
55+
for cpu_id in cpus {
56+
debug!("spawning task for cpu {}", cpu_id);
57+
let mut parray = perf_array.open(cpu_id, None)?;
58+
let txcopy = senders.pop().unwrap();
59+
task::spawn(async move {
60+
debug!("task for cpu awaiting for events {}", cpu_id);
61+
let mut buffers = (0..100)
62+
.map(|_| BytesMut::with_capacity(signal_struct_size))
63+
.collect::<Vec<_>>();
64+
65+
loop {
66+
let events = match parray.read_events(&mut buffers).await {
67+
Ok(events) => events,
68+
Err(error) => {
69+
error!("fail to read events from the perf, bailing out: {}", error);
70+
return;
71+
}
72+
};
73+
74+
if events.lost > 0 {
75+
error!("queues are getting full, lost {} signals", events.lost);
76+
}
77+
78+
for i in 0..events.read {
79+
let buf = &mut buffers[i];
80+
let ptr = buf.as_ptr() as *const Signal;
81+
let signal = unsafe { ptr.read_unaligned() };
82+
match txcopy.send(signal) {
83+
Ok(_) => continue,
84+
Err(err) => error!("failed to send signal internally: {}", err),
85+
}
86+
}
87+
}
88+
});
89+
}
90+
91+
Ok(())
92+
}
93+
}

signals/src/main.rs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
pub mod emitters;
2+
3+
use log::info;
4+
use std::sync::mpsc::sync_channel;
5+
6+
#[tokio::main]
7+
async fn main() -> Result<(), anyhow::Error> {
8+
env_logger::init();
9+
10+
let (tx, rx) = sync_channel(100);
11+
12+
let mut _bpf = emitters::SignalEmitter::new(tx)?;
13+
_bpf.attach()?;
14+
15+
info!("tasks started, awaiting for events");
16+
for msg in rx {
17+
info!("{:?}", msg);
18+
}
19+
Ok(())
20+
}

xtask/Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "xtask"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
anyhow = "1"
8+
clap = { version = "3.1", features = ["derive"] }

xtask/src/build_ebpf.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::path::PathBuf;
2+
use std::process::Command;
3+
4+
use clap::Parser;
5+
6+
#[derive(Debug, Copy, Clone)]
7+
pub enum Architecture {
8+
BpfEl,
9+
BpfEb,
10+
}
11+
12+
impl std::str::FromStr for Architecture {
13+
type Err = String;
14+
15+
fn from_str(s: &str) -> Result<Self, Self::Err> {
16+
Ok(match s {
17+
"bpfel-unknown-none" => Architecture::BpfEl,
18+
"bpfeb-unknown-none" => Architecture::BpfEb,
19+
_ => return Err("invalid target".to_owned()),
20+
})
21+
}
22+
}
23+
24+
impl std::fmt::Display for Architecture {
25+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26+
f.write_str(match self {
27+
Architecture::BpfEl => "bpfel-unknown-none",
28+
Architecture::BpfEb => "bpfeb-unknown-none",
29+
})
30+
}
31+
}
32+
33+
#[derive(Debug, Parser)]
34+
pub struct Options {
35+
/// Set the endianness of the BPF target
36+
#[clap(default_value = "bpfel-unknown-none", long)]
37+
pub target: Architecture,
38+
/// Build the release target
39+
#[clap(long)]
40+
pub release: bool,
41+
}
42+
43+
pub fn build_ebpf(opts: Options) -> Result<(), anyhow::Error> {
44+
let dir = PathBuf::from("signals-ebpf");
45+
let target = format!("--target={}", opts.target);
46+
let mut args = vec![
47+
"+nightly",
48+
"build",
49+
"--verbose",
50+
target.as_str(),
51+
"-Z",
52+
"build-std=core",
53+
];
54+
if opts.release {
55+
args.push("--release")
56+
}
57+
let status = Command::new("cargo")
58+
.current_dir(&dir)
59+
.args(&args)
60+
.status()
61+
.expect("failed to build bpf program");
62+
assert!(status.success());
63+
Ok(())
64+
}

0 commit comments

Comments
 (0)