Skip to content
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

Detect system info + available BPF features on startup #70

Merged
merged 25 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
52e340a
Detect system info
patnebe Sep 17, 2024
b01a025
Merge branch 'main' into feature-detection
patnebe Sep 17, 2024
3fe5193
Prevent panic
patnebe Sep 17, 2024
6b8313d
cleanup
patnebe Sep 17, 2024
abdb93d
Merge branch 'main' into feature-detection
javierhonduco Sep 17, 2024
73d04a0
Merge branch 'main' into feature-detection
javierhonduco Sep 18, 2024
109ff7f
Hook up to main + some cleanup
patnebe Sep 23, 2024
15def31
Merge branch 'main' into feature-detection
patnebe Sep 23, 2024
371fe01
clippy fix
patnebe Sep 23, 2024
72ebbd6
Merge branch 'feature-detection' of github.com:patnebe/lightswitch in…
patnebe Sep 23, 2024
6d93054
Apply review feedback
patnebe Sep 25, 2024
e6c6f24
Merge branch 'main' into feature-detection
patnebe Sep 25, 2024
19c0d0b
Hook into sched_switch event for feat detection
patnebe Sep 25, 2024
aebaebd
Merge branch 'feature-detection' of github.com:patnebe/lightswitch in…
patnebe Sep 25, 2024
26eef57
fix mount detection path
patnebe Sep 26, 2024
20f7bde
Create top level lightswitch-sys-probe crate
patnebe Sep 26, 2024
4e9d7db
Create top level lightswitch-sys-probe crate
patnebe Sep 26, 2024
ebd57a4
Delete lightswitch-sys-probe/src/bpf/features_skel.rs
patnebe Sep 26, 2024
e2ce4d5
Merge branch 'feature-detection' of github.com:patnebe/lightswitch in…
patnebe Sep 26, 2024
048ac80
missing newline
patnebe Sep 26, 2024
3f5cd25
Delete lightswitch-sys-probe/src/bpf/features_skel.rs
patnebe Sep 26, 2024
aa68f52
nit
patnebe Sep 26, 2024
ac3ae35
symlink vmlinux headers + remove ringbuf from min reqs
patnebe Sep 30, 2024
246459e
Merge branch 'feature-detection' of github.com:patnebe/lightswitch in…
patnebe Sep 30, 2024
9149ce5
Rename crate to lightswitch-capabilities
patnebe Sep 30, 2024
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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/target
flame.svg
profile.pb
src/bpf/*_skel.rs
**/bpf/*_skel.rs
.vmtest.log
/result
/result
19 changes: 19 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ nix = { version = "0.29.0", features = ["user"] }
prost = "0.12" # Needed to encode protocol buffers to bytes.
reqwest = { version = "0.12", features = ["blocking"] }
lightswitch-proto = { path = "./lightswitch-proto"}
lightswitch-capabilities = {path = "./lightswitch-capabilities"}
ctrlc = "3.4.5"
v = "0.1.0"
crossbeam-channel = "0.5.13"
Expand Down
20 changes: 20 additions & 0 deletions lightswitch-capabilities/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "lightswitch-capabilities"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.89"
thiserror = "1.0.63"
libbpf-rs = { version = "0.23.3", features = ["static"] }
perf-event-open-sys = "4.0.0"
libc = "0.2.158"
errno = "0.3.9"
procfs = "0.16.0"
tracing = "0.1.40"
nix = { version = "0.29.0", features = ["user"] }

[build-dependencies]
bindgen = "0.69.4"
libbpf-cargo = "0.23.3"
glob = "0.3.1"
26 changes: 26 additions & 0 deletions lightswitch-capabilities/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use glob::glob;
use libbpf_cargo::SkeletonBuilder;
use std::path::Path;

const FEATURES_BPF_SOURCE: &str = "./src/bpf/features.bpf.c";
const FEATURES_SKELETON: &str = "./src/bpf/features_skel.rs";

fn main() {
// Inform cargo of when to re build
for path in glob("src/bpf/*[hc]").unwrap().flatten() {
println!("cargo:rerun-if-changed={}", path.display());
}

let skel = Path::new(FEATURES_SKELETON);
SkeletonBuilder::new()
.source(FEATURES_BPF_SOURCE)
.clang_args([
"-Wextra",
"-Wall",
"-Werror",
"-Wno-unused-command-line-argument",
"-Wno-unused-parameter",
])
.build_and_generate(skel)
.expect("run skeleton builder");
}
34 changes: 34 additions & 0 deletions lightswitch-capabilities/src/bpf/features.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-2.0-only

#include "vmlinux.h"

#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>

bool feature_check_done = false;

bool has_tail_call = false;
bool has_ringbuf = false;
bool has_map_of_maps = false;
bool has_batch_map_operations = false;

SEC("tracepoint/sched/sched_switch")
int detect_bpf_features(void *ctx) {
has_tail_call = bpf_core_enum_value_exists(
enum bpf_func_id, BPF_FUNC_tail_call);

has_ringbuf = bpf_core_enum_value_exists(
enum bpf_map_type, BPF_MAP_TYPE_RINGBUF);

has_map_of_maps = bpf_core_enum_value_exists(
enum bpf_map_type, BPF_MAP_TYPE_HASH_OF_MAPS);

has_batch_map_operations = bpf_core_enum_value_exists(
enum bpf_cmd, BPF_MAP_LOOKUP_AND_DELETE_BATCH);

feature_check_done = true;

return 0;
}

char LICENSE[] SEC("license") = "Dual MIT/GPL";
1 change: 1 addition & 0 deletions lightswitch-capabilities/src/bpf/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod features_skel;
1 change: 1 addition & 0 deletions lightswitch-capabilities/src/bpf/vmlinux.h
1 change: 1 addition & 0 deletions lightswitch-capabilities/src/bpf/vmlinux_arm64.h
1 change: 1 addition & 0 deletions lightswitch-capabilities/src/bpf/vmlinux_x86.h
2 changes: 2 additions & 0 deletions lightswitch-capabilities/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod bpf;
pub mod system_info;
244 changes: 244 additions & 0 deletions lightswitch-capabilities/src/system_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
use std::fs::read_to_string;
use std::os::raw::c_int;
use std::path::Path;
use std::thread;
use std::time::Duration;
use thiserror::Error;
use tracing::{error, warn};

use crate::bpf::features_skel::FeaturesSkelBuilder;

use anyhow::Result;
use errno::errno;
use libbpf_rs::skel::{OpenSkel, Skel, SkelBuilder};
use libc::close;
use nix::sys::utsname;
use perf_event_open_sys as sys;
use perf_event_open_sys::bindings::perf_event_attr;

const PROCFS_PATH: &str = "/proc";
const TRACEFS_PATH: &str = "/sys/kernel/debug/tracing";

#[derive(Debug, Default)]
pub struct BpfFeatures {
pub can_load_trivial_bpf_program: bool,
pub has_ring_buf: bool,
pub has_tail_call: bool,
pub has_map_of_maps: bool,
pub has_batch_map_operations: bool,
}

#[derive(Debug)]
pub struct SystemInfo {
pub os_release: String,
pub procfs_mount_detected: bool,
pub tracefs_mount_detected: bool,
pub tracepoints_support_detected: bool,
pub software_perfevents_support_detected: bool,
pub available_bpf_features: BpfFeatures,
}

#[derive(Debug, Error)]
pub enum SystemInfoError {
#[error("File could not be opened {0}")]
ErrorOpeningFile(String),

#[error("Failed to detect tracefs mount")]
ErrorTracefsNotMounted,

#[error("Id for trace event {0} could not be read, err={1}")]
ErrorReadingTraceEventId(String, String),

#[error("BPF feature detection failed, err={0}")]
ErrorDetectingBpfFeatures(String),
}

struct DroppableFiledescriptor {
fd: i32,
}

impl Drop for DroppableFiledescriptor {
fn drop(&mut self) {
if { unsafe { close(self.fd) } } != 0 {
warn!("Failed to close file descriptor={}", self.fd);
}
}
}

fn tracefs_mount_detected() -> bool {
return Path::new(TRACEFS_PATH).exists();
}

fn get_trace_sched_event_id(trace_event: &str) -> Result<u32> {
if !tracefs_mount_detected() {
return Err(SystemInfoError::ErrorTracefsNotMounted.into());
}

let event_id_path = format!("{}/events/sched/{}/id", TRACEFS_PATH, trace_event);
let path = Path::new(&event_id_path);
if !path.exists() {
return Err(SystemInfoError::ErrorOpeningFile(event_id_path).into());
}

read_to_string(path)
.map_err(|err| {
SystemInfoError::ErrorReadingTraceEventId(trace_event.to_string(), err.to_string())
})?
.trim()
.parse::<u32>()
.map_err(|err| {
SystemInfoError::ErrorReadingTraceEventId(trace_event.to_string(), err.to_string())
.into()
})
}

fn software_perfevents_detected() -> bool {
let mut attrs: perf_event_attr = perf_event_open_sys::bindings::perf_event_attr {
size: std::mem::size_of::<sys::bindings::perf_event_attr>() as u32,
type_: sys::bindings::PERF_TYPE_SOFTWARE,
config: sys::bindings::PERF_COUNT_SW_CPU_CLOCK as u64,
..Default::default()
};
attrs.__bindgen_anon_1.sample_freq = 0;
attrs.set_disabled(1);
attrs.set_freq(1);

let fd = DroppableFiledescriptor {
fd: unsafe {
sys::perf_event_open(
&mut attrs, -1, /* pid */
/* cpu */ 0, -1, /* group_fd */
0, /* flags */
)
} as c_int,
};

if fd.fd < 0 {
error!("setup_perf_event failed with errno {}", errno());
return false;
}
true
}

fn tracepoints_detected() -> bool {
let mut attrs = sys::bindings::perf_event_attr {
size: std::mem::size_of::<sys::bindings::perf_event_attr>() as u32,
type_: sys::bindings::PERF_TYPE_TRACEPOINT,
..sys::bindings::perf_event_attr::default()
};

match get_trace_sched_event_id("sched_process_exec") {
Ok(event_id) => attrs.config = event_id as u64,
Err(err) => {
error!("{}", err);
return false;
}
}

let fd = DroppableFiledescriptor {
fd: unsafe {
sys::perf_event_open(
&mut attrs, -1, /* pid */
0, /* cpu */
-1, /* group_fd */
0, /* flags */
) as c_int
},
};

fd.fd >= 0
}

fn check_bpf_features() -> Result<BpfFeatures> {
let skel_builder = FeaturesSkelBuilder::default();
let open_skel = match skel_builder.open() {
Ok(open_skel) => open_skel,
Err(err) => return Err(SystemInfoError::ErrorDetectingBpfFeatures(err.to_string()).into()),
};
let mut bpf_features = match open_skel.load() {
Ok(bpf_features) => bpf_features,
Err(err) => return Err(SystemInfoError::ErrorDetectingBpfFeatures(err.to_string()).into()),
};
match bpf_features.attach() {
Ok(_) => {}
Err(err) => return Err(SystemInfoError::ErrorDetectingBpfFeatures(err.to_string()).into()),
};

thread::sleep(Duration::from_millis(1));

let bpf_features_bss = bpf_features.bss();
if !bpf_features_bss.feature_check_done {
warn!("Failed to detect available bpf features");
return Ok(BpfFeatures {
can_load_trivial_bpf_program: true,
..BpfFeatures::default()
});
}

let features: BpfFeatures = BpfFeatures {
can_load_trivial_bpf_program: true,
has_tail_call: bpf_features_bss.has_tail_call,
has_ring_buf: bpf_features_bss.has_ringbuf,
has_map_of_maps: bpf_features_bss.has_map_of_maps,
has_batch_map_operations: bpf_features_bss.has_batch_map_operations,
};

Ok(features)
}

impl SystemInfo {
pub fn new() -> Result<SystemInfo> {
let available_bpf_features = match check_bpf_features() {
Ok(features) => features,
Err(err) => {
warn!("Failed to detect available BPF features {}", err);
BpfFeatures::default()
}
};
Ok(SystemInfo {
os_release: utsname::uname()?.release().to_string_lossy().to_string(),
procfs_mount_detected: Path::new(PROCFS_PATH).exists(),
tracefs_mount_detected: tracefs_mount_detected(),
tracepoints_support_detected: tracepoints_detected(),
software_perfevents_support_detected: software_perfevents_detected(),
available_bpf_features,
})
}

pub fn has_minimal_requirements(&self) -> bool {
let bpf_features = &self.available_bpf_features;
self.tracefs_mount_detected
&& self.procfs_mount_detected
&& self.software_perfevents_support_detected
&& self.tracepoints_support_detected
&& bpf_features.can_load_trivial_bpf_program
&& bpf_features.has_tail_call
}
}

// TODO: How can we make this an integration/system test?
Copy link
Owner

Choose a reason for hiding this comment

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

It's totally fine to leave it here. Personally I don't think the difference between purely no-IO unittests and integration tests matters that much, unless we need to test a binary, in that case we should use the tool level folder instead. The standard in the Rust world is to add integration tests in test/ so here would go on lightswitch-capabilities/test.

// since it depends on the runtime environment.
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_get_system_info() {
let result = SystemInfo::new();

assert!(result.is_ok());

let system_info = result.unwrap();
assert!(system_info.procfs_mount_detected);
assert!(system_info.tracefs_mount_detected);
assert!(system_info.tracepoints_support_detected);
assert!(system_info.software_perfevents_support_detected);

let bpf_features = system_info.available_bpf_features;
assert!(bpf_features.can_load_trivial_bpf_program);
assert!(bpf_features.has_ring_buf);
assert!(bpf_features.has_tail_call);
assert!(bpf_features.has_map_of_maps);
assert!(bpf_features.has_batch_map_operations);
}
}
Loading