Browse Source

feat: Add an option to register allowed ranges of memory

Certain kernel helpers return pointers to kernel managed memory, which
the ebpf program is allowed to load and read.
In order to implement stubs for these using rbpf, we need a way to mark
these ranges of memory as safe for the check_mem function.

The API specifically deals with adresses, because helpers need to be
function types and not closures. This means the pointers to objects
returned from them need to be static, and dealing with references to
static objects gets clunky. So I have chosen to push the obtaining of
the addresses into calling code.

Signed-off-by: Wouter Dullaert <wouter.dullaert@exoscale.ch>
Wouter Dullaert 9 months ago
parent
commit
384c8820b3
7 changed files with 286 additions and 12 deletions
  1. 3 0
      Cargo.toml
  2. 10 0
      README.md
  3. BIN
      examples/allowed-memory.o
  4. 57 0
      examples/allowed_memory.rs
  5. 39 0
      examples/ebpf-allowed-memory.rs
  6. 16 7
      src/interpreter.rs
  7. 161 5
      src/lib.rs

+ 3 - 0
Cargo.toml

@@ -78,3 +78,6 @@ name = "to_json"
 
 [[example]]
 name = "rbpf_plugin"
+
+[[example]]
+name = "allowed_memory"

+ 10 - 0
README.md

@@ -209,6 +209,16 @@ registers in a hashmap, so the key can be any `u32` value you want. It may be
 useful for programs that should be compatible with the Linux kernel and
 therefore must use specific helper numbers.
 
+```rust,ignore
+pub fn register_allowed_memory(&mut self,, addr: &[u64]) -> ()
+```
+
+This function adds a list of memory addresses that the eBPF program is allowed
+to load and store. Multiple calls to this function will append the addresses to
+an internal HashSet. At the moment rbpf only validates memory accesses when
+using the interpreter. This function is useful when using kernel helpers which
+return pointers to objects stored in eBPF maps.
+
 ```rust,ignore
 // for struct EbpfVmMbuff
 pub fn execute_program(&self,

BIN
examples/allowed-memory.o


+ 57 - 0
examples/allowed_memory.rs

@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+// Copyright 2024 Akenes SA <wouter.dullaert@exoscale.ch>
+
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::unreadable_literal))]
+
+extern crate elf;
+use std::{iter::FromIterator, ptr::addr_of};
+
+extern crate rbpf;
+
+// The following example uses an ELF file that was compiled from the ebpf-allowed-memory.rs file
+// It is built using the [aya framework](https://aya-rs.dev/).
+// Once the aya dependencies (rust-nightly, latest llvm and latest bpf-linker) are installed, it
+// can be compiled via
+//
+// ```bash
+// cargo build --target=bpfel-unknown-none -Z build-std=core
+// ```
+
+const BPF_MAP_LOOKUP_ELEM_IDX: u32 = 1;
+
+#[repr(C, packed)]
+#[derive(Clone, Copy)]
+pub struct Key {
+    pub protocol: u8,
+}
+
+#[repr(C, packed)]
+pub struct Value {
+    pub result: i32,
+}
+
+static MAP_VALUE: Value = Value { result: 1 };
+
+fn bpf_lookup_elem(_map: u64, key_addr: u64, _flags: u64, _u4: u64, _u5: u64) -> u64 {
+    let key: Key = unsafe { *(key_addr as *const Key) };
+    if key.protocol == 1 {
+        return addr_of!(MAP_VALUE) as u64;
+    }
+    0
+}
+
+fn main() {
+    let file = elf::File::open_path("examples/allowed-memory.o").unwrap();
+    let func = file.get_section("classifier").unwrap();
+
+    let mut vm = rbpf::EbpfVmNoData::new(Some(&func.data)).unwrap();
+    vm.register_helper(BPF_MAP_LOOKUP_ELEM_IDX, bpf_lookup_elem)
+        .unwrap();
+
+    let start = addr_of!(MAP_VALUE) as u64;
+    let addrs = Vec::from_iter(start..start + size_of::<Value>() as u64);
+    vm.register_allowed_memory(&addrs);
+
+    let res = vm.execute_program().unwrap();
+    assert_eq!(res, 1);
+}

+ 39 - 0
examples/ebpf-allowed-memory.rs

@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+// Copyright 2024 Akenes SA <wouter.dullaert@exoscale.ch>
+
+#![no_std]
+#![no_main]
+
+use aya_ebpf::{
+    bindings::{BPF_F_NO_PREALLOC, TC_ACT_PIPE},
+    macros::{classifier, map},
+    maps::HashMap,
+    programs::TcContext,
+};
+
+#[map]
+static RULES: HashMap<Key, Value> = HashMap::<Key, Value>::with_max_entries(1, BPF_F_NO_PREALLOC);
+
+#[repr(C, packed)]
+pub struct Key {
+    pub protocol: u8,
+}
+
+#[repr(C, packed)]
+pub struct Value {
+    pub result: i32,
+}
+
+#[classifier]
+pub fn ingress_tc(_ctx: TcContext) -> i32 {
+    let key = Key { protocol: 1 };
+    if let Some(action) = unsafe { RULES.get(&key) } {
+        return action.result;
+    }
+    return TC_ACT_PIPE;
+}
+
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    unsafe { core::hint::unreachable_unchecked() }
+}

+ 16 - 7
src/interpreter.rs

@@ -9,16 +9,19 @@ use ebpf;
 use crate::lib::*;
 
 fn check_mem(addr: u64, len: usize, access_type: &str, insn_ptr: usize,
-             mbuff: &[u8], mem: &[u8], stack: &[u8]) -> Result<(), Error> {
+             mbuff: &[u8], mem: &[u8], stack: &[u8], allowed_memory: &HashSet<u64>) -> Result<(), Error> {
     if let Some(addr_end) = addr.checked_add(len as u64) {
       if mbuff.as_ptr() as u64 <= addr && addr_end <= mbuff.as_ptr() as u64 + mbuff.len() as u64 {
-          return Ok(())
+          return Ok(());
       }
       if mem.as_ptr() as u64 <= addr && addr_end <= mem.as_ptr() as u64 + mem.len() as u64 {
-          return Ok(())
+          return Ok(());
       }
       if stack.as_ptr() as u64 <= addr && addr_end <= stack.as_ptr() as u64 + stack.len() as u64 {
-          return Ok(())
+          return Ok(());
+      }
+      if allowed_memory.contains(&addr) {
+          return Ok(());
       }
     }
 
@@ -33,7 +36,13 @@ fn check_mem(addr: u64, len: usize, access_type: &str, insn_ptr: usize,
 
 #[allow(unknown_lints)]
 #[allow(cyclomatic_complexity)]
-pub fn execute_program(prog_: Option<&[u8]>, mem: &[u8], mbuff: &[u8], helpers: &HashMap<u32, ebpf::Helper>) -> Result<u64, Error> {
+pub fn execute_program(
+    prog_: Option<&[u8]>,
+    mem: &[u8],
+    mbuff: &[u8],
+    helpers: &HashMap<u32, ebpf::Helper>,
+    allowed_memory: &HashSet<u64>,
+) -> Result<u64, Error> {
     const U32MAX: u64 = u32::MAX as u64;
     const SHIFT_MASK_64: u64 = 0x3f;
 
@@ -56,10 +65,10 @@ pub fn execute_program(prog_: Option<&[u8]>, mem: &[u8], mbuff: &[u8], helpers:
     }
 
     let check_mem_load = | addr: u64, len: usize, insn_ptr: usize | {
-        check_mem(addr, len, "load", insn_ptr, mbuff, mem, &stack)
+        check_mem(addr, len, "load", insn_ptr, mbuff, mem, &stack, allowed_memory)
     };
     let check_mem_store = | addr: u64, len: usize, insn_ptr: usize | {
-        check_mem(addr, len, "store", insn_ptr, mbuff, mem, &stack)
+        check_mem(addr, len, "store", insn_ptr, mbuff, mem, &stack, allowed_memory)
     };
 
     // Loop on instructions

+ 161 - 5
src/lib.rs

@@ -31,9 +31,9 @@
 
 extern crate byteorder;
 extern crate combine;
+extern crate log;
 #[cfg(feature = "std")]
 extern crate time;
-extern crate log;
 
 #[cfg(not(feature = "std"))]
 extern crate alloc;
@@ -49,8 +49,8 @@ extern crate cranelift_module;
 #[cfg(feature = "cranelift")]
 extern crate cranelift_native;
 
-use byteorder::{ByteOrder, LittleEndian};
 use crate::lib::*;
+use byteorder::{ByteOrder, LittleEndian};
 
 mod asm_parser;
 pub mod assembler;
@@ -63,9 +63,9 @@ pub mod insn_builder;
 mod interpreter;
 #[cfg(all(not(windows), feature = "std"))]
 mod jit;
-mod verifier;
 #[cfg(not(feature = "std"))]
 mod no_std_error;
+mod verifier;
 
 /// Reexports all the types needed from the `std`, `core`, and `alloc`
 /// crates. This avoids elaborate import wrangling having to happen in every
@@ -83,7 +83,7 @@ pub mod lib {
     pub use self::core::mem::ManuallyDrop;
     pub use self::core::ptr;
 
-    pub use self::core::{u32, u64, f64};
+    pub use self::core::{f64, u32, u64};
 
     #[cfg(feature = "std")]
     pub use std::println;
@@ -182,6 +182,7 @@ pub struct EbpfVmMbuff<'a> {
     #[cfg(feature = "cranelift")]
     cranelift_prog: Option<cranelift::CraneliftProgram>,
     helpers: HashMap<u32, ebpf::Helper>,
+    allowed_memory: HashSet<u64>,
 }
 
 impl<'a> EbpfVmMbuff<'a> {
@@ -213,6 +214,7 @@ impl<'a> EbpfVmMbuff<'a> {
             #[cfg(feature = "cranelift")]
             cranelift_prog: None,
             helpers: HashMap::new(),
+            allowed_memory: HashSet::new(),
         })
     }
 
@@ -320,6 +322,46 @@ impl<'a> EbpfVmMbuff<'a> {
         Ok(())
     }
 
+    /// Register a set of addresses that the eBPF program is allowed to load and store.
+    ///
+    /// When using certain helpers, typically map lookups, the Linux kernel will return pointers
+    /// to structs that the eBPF program needs to interact with. By default rbpf only allows the
+    /// program to interact with its stack, the memory buffer and the program itself, making it
+    /// impossible to supply functional implementations of these helpers.
+    /// This option allows you to pass in a list of addresses that rbpf will allow the program
+    /// to load and store to. Given Rust's memory model you will always know these addresses up
+    /// front when implementing the helpers.
+    ///
+    /// Each invocation of this method will append to the set of allowed addresses.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use std::iter::FromIterator;
+    /// use std::ptr::addr_of;
+    ///
+    /// struct MapValue {
+    ///     data: u8
+    /// }
+    /// static VALUE: MapValue = MapValue { data: 1 };
+    ///
+    /// let prog = &[
+    ///     0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
+    ///     0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // exit
+    /// ];
+    ///
+    /// // Instantiate a VM.
+    /// let mut vm = rbpf::EbpfVmMbuff::new(Some(prog)).unwrap();
+    /// let start = addr_of!(VALUE) as u64;
+    /// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
+    /// vm.register_allowed_memory(&addrs);
+    /// ```
+    pub fn register_allowed_memory(&mut self, addrs: &[u64]) -> () {
+        for i in addrs {
+            self.allowed_memory.insert(*i);
+        }
+    }
+
     /// Execute the program loaded, with the given packet data and metadata buffer.
     ///
     /// If the program is made to be compatible with Linux kernel, it is expected to load the
@@ -357,7 +399,7 @@ impl<'a> EbpfVmMbuff<'a> {
     /// assert_eq!(res, 0x2211);
     /// ```
     pub fn execute_program(&self, mem: &[u8], mbuff: &[u8]) -> Result<u64, Error> {
-        interpreter::execute_program(self.prog, mem, mbuff, &self.helpers)
+        interpreter::execute_program(self.prog, mem, mbuff, &self.helpers, &self.allowed_memory)
     }
 
     /// JIT-compile the loaded program. No argument required for this.
@@ -826,6 +868,44 @@ impl<'a> EbpfVmFixedMbuff<'a> {
         self.parent.register_helper(key, function)
     }
 
+    /// Register an object that the eBPF program is allowed to load and store.
+    ///
+    /// When using certain helpers, typically map lookups, the Linux kernel will return pointers
+    /// to structs that the eBPF program needs to interact with. By default rbpf only allows the
+    /// program to interact with its stack, the memory buffer and the program itself, making it
+    /// impossible to supply functional implementations of these helpers.
+    /// This option allows you to pass in a list of addresses that rbpf will allow the program
+    /// to load and store to. Given Rust's memory model you will always know these addresses up
+    /// front when implementing the helpers.
+    ///
+    /// Each invocation of this method will append to the set of allowed addresses.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use std::iter::FromIterator;
+    /// use std::ptr::addr_of;
+    ///
+    /// struct MapValue {
+    ///     data: u8
+    /// }
+    /// static VALUE: MapValue = MapValue { data: 1 };
+    ///
+    /// let prog = &[
+    ///     0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
+    ///     0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // exit
+    /// ];
+    ///
+    /// // Instantiate a VM.
+    /// let mut vm = rbpf::EbpfVmFixedMbuff::new(Some(prog), 0x40, 0x50).unwrap();
+    /// let start = addr_of!(VALUE) as u64;
+    /// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
+    /// vm.register_allowed_memory(&addrs);
+    /// ```
+    pub fn register_allowed_memory(&mut self, allowed: &[u64]) -> () {
+        self.parent.register_allowed_memory(allowed)
+    }
+
     /// Execute the program loaded, with the given packet data.
     ///
     /// If the program is made to be compatible with Linux kernel, it is expected to load the
@@ -1260,6 +1340,44 @@ impl<'a> EbpfVmRaw<'a> {
         self.parent.register_helper(key, function)
     }
 
+    /// Register an object that the eBPF program is allowed to load and store.
+    ///
+    /// When using certain helpers, typically map lookups, the Linux kernel will return pointers
+    /// to structs that the eBPF program needs to interact with. By default rbpf only allows the
+    /// program to interact with its stack, the memory buffer and the program itself, making it
+    /// impossible to supply functional implementations of these helpers.
+    /// This option allows you to pass in a list of addresses that rbpf will allow the program
+    /// to load and store to. Given Rust's memory model you will always know these addresses up
+    /// front when implementing the helpers.
+    ///
+    /// Each invocation of this method will append to the set of allowed addresses.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use std::iter::FromIterator;
+    /// use std::ptr::addr_of;
+    ///
+    /// struct MapValue {
+    ///     data: u8
+    /// }
+    /// static VALUE: MapValue = MapValue { data: 1 };
+    ///
+    /// let prog = &[
+    ///     0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
+    ///     0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // exit
+    /// ];
+    ///
+    /// // Instantiate a VM.
+    /// let mut vm = rbpf::EbpfVmRaw::new(Some(prog)).unwrap();
+    /// let start = addr_of!(VALUE) as u64;
+    /// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
+    /// vm.register_allowed_memory(&addrs);
+    /// ```
+    pub fn register_allowed_memory(&mut self, allowed: &[u64]) -> () {
+        self.parent.register_allowed_memory(allowed)
+    }
+
     /// Execute the program loaded, with the given packet data.
     ///
     /// # Examples
@@ -1602,6 +1720,44 @@ impl<'a> EbpfVmNoData<'a> {
         self.parent.register_helper(key, function)
     }
 
+    /// Register an object that the eBPF program is allowed to load and store.
+    ///
+    /// When using certain helpers, typically map lookups, the Linux kernel will return pointers
+    /// to structs that the eBPF program needs to interact with. By default rbpf only allows the
+    /// program to interact with its stack, the memory buffer and the program itself, making it
+    /// impossible to supply functional implementations of these helpers.
+    /// This option allows you to pass in a list of addresses that rbpf will allow the program
+    /// to load and store to. Given Rust's memory model you will always know these addresses up
+    /// front when implementing the helpers.
+    ///
+    /// Each invocation of this method will append to the set of allowed addresses.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use std::iter::FromIterator;
+    /// use std::ptr::addr_of;
+    ///
+    /// struct MapValue {
+    ///     data: u8
+    /// }
+    /// static VALUE: MapValue = MapValue { data: 1 };
+    ///
+    /// let prog = &[
+    ///     0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
+    ///     0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // exit
+    /// ];
+    ///
+    /// // Instantiate a VM.
+    /// let mut vm = rbpf::EbpfVmNoData::new(Some(prog)).unwrap();
+    /// let start = addr_of!(VALUE) as u64;
+    /// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
+    /// vm.register_allowed_memory(&addrs);
+    /// ```
+    pub fn register_allowed_memory(&mut self, allowed: &[u64]) -> () {
+        self.parent.register_allowed_memory(allowed)
+    }
+
     /// JIT-compile the loaded program. No argument required for this.
     ///
     /// If using helper functions, be sure to register them into the VM before calling this