diff --git a/Cargo.toml b/Cargo.toml index f37652338..da1986d7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,3 +78,6 @@ name = "to_json" [[example]] name = "rbpf_plugin" + +[[example]] +name = "allowed_memory" diff --git a/README.md b/README.md index f15d9500e..337411be2 100644 --- a/README.md +++ b/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, diff --git a/examples/allowed-memory.o b/examples/allowed-memory.o new file mode 100644 index 000000000..9b396d6cc Binary files /dev/null and b/examples/allowed-memory.o differ diff --git a/examples/allowed_memory.rs b/examples/allowed_memory.rs new file mode 100644 index 000000000..89be62a0d --- /dev/null +++ b/examples/allowed_memory.rs @@ -0,0 +1,54 @@ +#![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::() as u64); + vm.register_allowed_memory(&addrs); + + let res = vm.execute_program().unwrap(); + assert_eq!(res, 1); +} diff --git a/examples/ebpf-allowed-memory.rs b/examples/ebpf-allowed-memory.rs new file mode 100644 index 000000000..88ff3cbf7 --- /dev/null +++ b/examples/ebpf-allowed-memory.rs @@ -0,0 +1,40 @@ +#![no_std] +#![no_main] + +use aya_ebpf::{ + bindings::{BPF_F_NO_PREALLOC, TC_ACT_PIPE}, + macros::{classifier, map}, + maps::HashMap, + programs::TcContext, +}; + +#[no_mangle] +#[link_section = "license"] +pub static LICENSE: [u8; 13] = *b"Dual MIT/GPL\0"; + +#[map] +static RULES: HashMap = HashMap::::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() } +} diff --git a/src/interpreter.rs b/src/interpreter.rs index 33a406226..1e83ee204 100644 --- a/src/interpreter.rs +++ b/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) -> 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) -> Result { +pub fn execute_program( + prog_: Option<&[u8]>, + mem: &[u8], + mbuff: &[u8], + helpers: &HashMap, + allowed: &HashSet, +) -> Result { 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) }; 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 diff --git a/src/lib.rs b/src/lib.rs index f348fef25..78d5c88d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,7 @@ pub struct EbpfVmMbuff<'a> { #[cfg(feature = "cranelift")] cranelift_prog: Option, helpers: HashMap, + allowed_memory: HashSet, } 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::() 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 { - 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::() 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::() 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::() 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