Browse Source

bpf: Add `bpf_strncmp` helper

The `bpf_strncmp` helper allows for better string comparison in eBPF
programs.

Added in https://github.com/torvalds/linux/commit/c5fb19937455095573a19.
Michal Rostecki 1 year ago
parent
commit
0b58d3eb6d

+ 25 - 1
ebpf/aya-ebpf/src/helpers.rs

@@ -9,7 +9,11 @@
 //! helpers, but also expose bindings to the underlying helpers as a fall-back
 //! in case of a missing implementation.
 
-use core::mem::{self, MaybeUninit};
+use core::{
+    cmp::Ordering,
+    ffi::CStr,
+    mem::{self, MaybeUninit},
+};
 
 pub use aya_ebpf_bindings::helpers as gen;
 #[doc(hidden)]
@@ -838,3 +842,23 @@ pub unsafe fn bpf_printk_impl<const FMT_LEN: usize, const NUM_ARGS: usize>(
         _ => gen::bpf_trace_vprintk(fmt_ptr, fmt_size, args.as_ptr() as _, (NUM_ARGS * 8) as _),
     }
 }
+
+/// Compares the given byte `s1` with a [`&CStr`](core::ffi::CStr) `s2`.
+///
+/// # Examples
+///
+/// ```no_run
+/// # use aya_ebpf::helpers::bpf_strncmp;
+/// # let data = b"something";
+/// assert_ne!(bpf_strncmp(data, c"foo"), core::cmp::Ordering::Equal);
+/// ```
+#[inline]
+pub fn bpf_strncmp<const N: usize>(s1: &[u8; N], s2: &CStr) -> Ordering {
+    // NB: s1 does not need to be null-terminated.
+    //
+    // See https://github.com/torvalds/linux/blob/adc218676/include/uapi/linux/bpf.h#L5391-L5393.
+    //
+    // NB: s1's size must be known at compile time to appease the verifier. This is also the typical
+    // usage of strncmp in C programs.
+    unsafe { gen::bpf_strncmp(s1.as_ptr() as *const _, N as u32, s2.as_ptr() as *const _) }.cmp(&0)
+}

+ 4 - 0
test/integration-ebpf/Cargo.toml

@@ -57,6 +57,10 @@ path = "src/ring_buf.rs"
 name = "simple_prog"
 path = "src/simple_prog.rs"
 
+[[bin]]
+name = "strncmp"
+path = "src/strncmp.rs"
+
 [[bin]]
 name = "tcx"
 path = "src/tcx.rs"

+ 38 - 0
test/integration-ebpf/src/strncmp.rs

@@ -0,0 +1,38 @@
+#![no_std]
+#![no_main]
+
+use core::cmp::Ordering;
+
+use aya_ebpf::{
+    cty::c_long,
+    helpers::{bpf_probe_read_user_str_bytes, bpf_strncmp},
+    macros::{map, uprobe},
+    maps::Array,
+    programs::ProbeContext,
+};
+
+#[repr(C)]
+struct TestResult(Ordering);
+
+#[map]
+static RESULT: Array<TestResult> = Array::with_max_entries(1, 0);
+
+#[uprobe]
+pub fn test_bpf_strncmp(ctx: ProbeContext) -> Result<(), c_long> {
+    let s1: *const u8 = ctx.arg(0).ok_or(-1)?;
+    let mut b1 = [0u8; 3];
+    let _: &[u8] = unsafe { bpf_probe_read_user_str_bytes(s1, &mut b1) }?;
+
+    let ptr = RESULT.get_ptr_mut(0).ok_or(-1)?;
+    let dst = unsafe { ptr.as_mut() };
+    let TestResult(dst_res) = dst.ok_or(-1)?;
+    *dst_res = bpf_strncmp(&b1, c"ff");
+
+    Ok(())
+}
+
+#[cfg(not(test))]
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}

+ 1 - 0
test/integration-test/src/lib.rs

@@ -23,6 +23,7 @@ pub const REDIRECT: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/re
 pub const RELOCATIONS: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/relocations"));
 pub const RING_BUF: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/ring_buf"));
 pub const SIMPLE_PROG: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/simple_prog"));
+pub const STRNCMP: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/strncmp"));
 pub const TCX: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/tcx"));
 pub const TEST: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/test"));
 pub const TWO_PROGS: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/two_progs"));

+ 1 - 0
test/integration-test/src/tests.rs

@@ -8,5 +8,6 @@ mod rbpf;
 mod relocations;
 mod ring_buf;
 mod smoke;
+mod strncmp;
 mod tcx;
 mod xdp;

+ 55 - 0
test/integration-test/src/tests/strncmp.rs

@@ -0,0 +1,55 @@
+use std::{
+    cmp::Ordering,
+    ffi::{c_char, CStr},
+};
+
+use aya::{
+    maps::{Array, MapData},
+    programs::UProbe,
+    Ebpf,
+};
+
+#[derive(Copy, Clone)]
+#[repr(C)]
+struct TestResult(Ordering);
+
+unsafe impl aya::Pod for TestResult {}
+
+#[test]
+fn bpf_strncmp() {
+    let mut bpf = Ebpf::load(crate::STRNCMP).unwrap();
+
+    {
+        let prog: &mut UProbe = bpf
+            .program_mut("test_bpf_strncmp")
+            .unwrap()
+            .try_into()
+            .unwrap();
+        prog.load().unwrap();
+
+        prog.attach(Some("trigger_bpf_strncmp"), 0, "/proc/self/exe", None)
+            .unwrap();
+    }
+
+    let array = Array::<_, TestResult>::try_from(bpf.map("RESULT").unwrap()).unwrap();
+
+    assert_eq!(do_bpf_strncmp(&array, c"ff"), Ordering::Equal);
+
+    // This is truncated in BPF; the buffer size is 3 including the null terminator.
+    assert_eq!(do_bpf_strncmp(&array, c"fff"), Ordering::Equal);
+
+    assert_eq!(do_bpf_strncmp(&array, c"aa"), Ordering::Less);
+    assert_eq!(do_bpf_strncmp(&array, c"zz"), Ordering::Greater);
+}
+
+fn do_bpf_strncmp(array: &Array<&MapData, TestResult>, s1: &CStr) -> Ordering {
+    trigger_bpf_strncmp(s1.as_ptr());
+    let TestResult(ord) = array.get(&0, 0).unwrap();
+    ord
+}
+
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn trigger_bpf_strncmp(s1: *const c_char) {
+    core::hint::black_box(s1);
+}

+ 1 - 0
xtask/public-api/aya-ebpf.txt

@@ -76,6 +76,7 @@ pub unsafe fn aya_ebpf::helpers::bpf_probe_read_user_buf(src: *const u8, dst: &m
 pub unsafe fn aya_ebpf::helpers::bpf_probe_read_user_str(src: *const u8, dest: &mut [u8]) -> core::result::Result<usize, aya_ebpf_cty::od::c_long>
 pub unsafe fn aya_ebpf::helpers::bpf_probe_read_user_str_bytes(src: *const u8, dest: &mut [u8]) -> core::result::Result<&[u8], aya_ebpf_cty::od::c_long>
 pub unsafe fn aya_ebpf::helpers::bpf_probe_write_user<T>(dst: *mut T, src: *const T) -> core::result::Result<(), aya_ebpf_cty::od::c_long>
+pub fn aya_ebpf::helpers::bpf_strncmp<const N: usize>(s1: &[u8; N], s2: &core::ffi::c_str::CStr) -> core::cmp::Ordering
 pub mod aya_ebpf::maps
 pub mod aya_ebpf::maps::array
 #[repr(transparent)] pub struct aya_ebpf::maps::array::Array<T>