Skip to content

Commit

Permalink
feat: Add an option to register allowed ranges of memory
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
wdullaer committed Sep 9, 2024
1 parent fe7021b commit 6ba9627
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 8 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
23 changes: 16 additions & 7 deletions src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(());
}
}

Expand All @@ -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: &HashSet<u64>,
) -> Result<u64, Error> {
const U32MAX: u64 = u32::MAX as u64;
const SHIFT_MASK_64: u64 = 0x3f;

Expand All @@ -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)
};
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)
};

// Loop on instructions
Expand Down
158 changes: 157 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -213,6 +214,7 @@ impl<'a> EbpfVmMbuff<'a> {
#[cfg(feature = "cranelift")]
cranelift_prog: None,
helpers: HashMap::new(),
allowed_memory: HashSet::new(),
})
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6ba9627

Please sign in to comment.