Skip to content

Commit 216d80c

Browse files
committed
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 <[email protected]>
1 parent fe7021b commit 216d80c

File tree

7 files changed

+280
-8
lines changed

7 files changed

+280
-8
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,6 @@ name = "to_json"
7878

7979
[[example]]
8080
name = "rbpf_plugin"
81+
82+
[[example]]
83+
name = "allowed_memory"

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,16 @@ registers in a hashmap, so the key can be any `u32` value you want. It may be
209209
useful for programs that should be compatible with the Linux kernel and
210210
therefore must use specific helper numbers.
211211

212+
```rust,ignore
213+
pub fn register_allowed_memory(&mut self,, addr: &[u64]) -> ()
214+
```
215+
216+
This function adds a list of memory addresses that the ebpf program is allowed
217+
to load and store. Multiple calls to this function will append the addresses to
218+
an internal HashSet. At the moment rbpf only validates memory accesses when
219+
using the interpreter. This function is useful when using kernel helpers which
220+
return pointers to objects stored in ebpf maps.
221+
212222
```rust,ignore
213223
// for struct EbpfVmMbuff
214224
pub fn execute_program(&self,

examples/allowed-memory.o

45.5 KB
Binary file not shown.

examples/allowed_memory.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#![cfg_attr(feature = "cargo-clippy", allow(clippy::unreadable_literal))]
2+
3+
extern crate elf;
4+
use std::{iter::FromIterator, ptr::addr_of};
5+
6+
extern crate rbpf;
7+
8+
// The following example uses an ELF file that was compiled from the ebpf-allowed-memory.rs file
9+
// It is built using the [aya framework](https://aya-rs.dev/).
10+
// Once the aya dependencies (rust-nightly, latest llvm and latest bpf-linker) are installed, it
11+
// can be compiled via
12+
//
13+
// ```bash
14+
// cargo build --target=bpfel-unknown-none -Z build-std=core
15+
// ```
16+
17+
const BPF_MAP_LOOKUP_ELEM_IDX: u32 = 1;
18+
19+
#[repr(C, packed)]
20+
#[derive(Clone, Copy)]
21+
pub struct Key {
22+
pub protocol: u8,
23+
}
24+
25+
#[repr(C, packed)]
26+
pub struct Value {
27+
pub result: i32,
28+
}
29+
30+
static MAP_VALUE: Value = Value { result: 1 };
31+
32+
fn bpf_lookup_elem(_map: u64, key_addr: u64, _flags: u64, _u4: u64, _u5: u64) -> u64 {
33+
let key: Key = unsafe { *(key_addr as *const Key) };
34+
if key.protocol == 1 {
35+
return addr_of!(MAP_VALUE) as u64;
36+
}
37+
0
38+
}
39+
40+
fn main() {
41+
let file = elf::File::open_path("examples/allowed-memory.o").unwrap();
42+
let func = file.get_section("classifier").unwrap();
43+
44+
let mut vm = rbpf::EbpfVmNoData::new(Some(&func.data)).unwrap();
45+
vm.register_helper(BPF_MAP_LOOKUP_ELEM_IDX, bpf_lookup_elem)
46+
.unwrap();
47+
48+
let start = addr_of!(MAP_VALUE) as u64;
49+
let addrs = Vec::from_iter(start..start + size_of::<Value>() as u64);
50+
vm.register_allowed_memory(&addrs);
51+
52+
let res = vm.execute_program().unwrap();
53+
assert_eq!(res, 1);
54+
}

examples/ebpf-allowed-memory.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#![no_std]
2+
#![no_main]
3+
4+
use aya_ebpf::{
5+
bindings::{BPF_F_NO_PREALLOC, TC_ACT_PIPE},
6+
macros::{classifier, map},
7+
maps::HashMap,
8+
programs::TcContext,
9+
};
10+
11+
#[no_mangle]
12+
#[link_section = "license"]
13+
pub static LICENSE: [u8; 13] = *b"Dual MIT/GPL\0";
14+
15+
#[map]
16+
static RULES: HashMap<Key, Value> = HashMap::<Key, Value>::with_max_entries(1, BPF_F_NO_PREALLOC);
17+
18+
#[repr(C, packed)]
19+
pub struct Key {
20+
pub protocol: u8,
21+
}
22+
23+
#[repr(C, packed)]
24+
pub struct Value {
25+
pub result: i32,
26+
}
27+
28+
#[classifier]
29+
pub fn ingress_tc(_ctx: TcContext) -> i32 {
30+
let key = Key { protocol: 1 };
31+
if let Some(action) = unsafe { RULES.get(&key) } {
32+
return action.result;
33+
}
34+
return TC_ACT_PIPE;
35+
}
36+
37+
#[panic_handler]
38+
fn panic(_info: &core::panic::PanicInfo) -> ! {
39+
unsafe { core::hint::unreachable_unchecked() }
40+
}

src/interpreter.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ use ebpf;
99
use crate::lib::*;
1010

1111
fn check_mem(addr: u64, len: usize, access_type: &str, insn_ptr: usize,
12-
mbuff: &[u8], mem: &[u8], stack: &[u8]) -> Result<(), Error> {
12+
mbuff: &[u8], mem: &[u8], stack: &[u8], allowed_memory: &HashSet<u64>) -> Result<(), Error> {
1313
if let Some(addr_end) = addr.checked_add(len as u64) {
1414
if mbuff.as_ptr() as u64 <= addr && addr_end <= mbuff.as_ptr() as u64 + mbuff.len() as u64 {
15-
return Ok(())
15+
return Ok(());
1616
}
1717
if mem.as_ptr() as u64 <= addr && addr_end <= mem.as_ptr() as u64 + mem.len() as u64 {
18-
return Ok(())
18+
return Ok(());
1919
}
2020
if stack.as_ptr() as u64 <= addr && addr_end <= stack.as_ptr() as u64 + stack.len() as u64 {
21-
return Ok(())
21+
return Ok(());
22+
}
23+
if allowed_memory.contains(&addr) {
24+
return Ok(());
2225
}
2326
}
2427

@@ -33,7 +36,13 @@ fn check_mem(addr: u64, len: usize, access_type: &str, insn_ptr: usize,
3336

3437
#[allow(unknown_lints)]
3538
#[allow(cyclomatic_complexity)]
36-
pub fn execute_program(prog_: Option<&[u8]>, mem: &[u8], mbuff: &[u8], helpers: &HashMap<u32, ebpf::Helper>) -> Result<u64, Error> {
39+
pub fn execute_program(
40+
prog_: Option<&[u8]>,
41+
mem: &[u8],
42+
mbuff: &[u8],
43+
helpers: &HashMap<u32, ebpf::Helper>,
44+
allowed: &HashSet<u64>,
45+
) -> Result<u64, Error> {
3746
const U32MAX: u64 = u32::MAX as u64;
3847
const SHIFT_MASK_64: u64 = 0x3f;
3948

@@ -56,10 +65,10 @@ pub fn execute_program(prog_: Option<&[u8]>, mem: &[u8], mbuff: &[u8], helpers:
5665
}
5766

5867
let check_mem_load = | addr: u64, len: usize, insn_ptr: usize | {
59-
check_mem(addr, len, "load", insn_ptr, mbuff, mem, &stack)
68+
check_mem(addr, len, "load", insn_ptr, mbuff, mem, &stack, allowed)
6069
};
6170
let check_mem_store = | addr: u64, len: usize, insn_ptr: usize | {
62-
check_mem(addr, len, "store", insn_ptr, mbuff, mem, &stack)
71+
check_mem(addr, len, "store", insn_ptr, mbuff, mem, &stack, allowed)
6372
};
6473

6574
// Loop on instructions

src/lib.rs

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ pub struct EbpfVmMbuff<'a> {
182182
#[cfg(feature = "cranelift")]
183183
cranelift_prog: Option<cranelift::CraneliftProgram>,
184184
helpers: HashMap<u32, ebpf::Helper>,
185+
allowed_memory: HashSet<u64>,
185186
}
186187

187188
impl<'a> EbpfVmMbuff<'a> {
@@ -213,6 +214,7 @@ impl<'a> EbpfVmMbuff<'a> {
213214
#[cfg(feature = "cranelift")]
214215
cranelift_prog: None,
215216
helpers: HashMap::new(),
217+
allowed_memory: HashSet::new(),
216218
})
217219
}
218220

@@ -320,6 +322,46 @@ impl<'a> EbpfVmMbuff<'a> {
320322
Ok(())
321323
}
322324

325+
/// Register a set of addresses that the ebpf program is allowed to load and store.
326+
///
327+
/// When using certain helpers, typically map lookups, the linux kernel will return pointers
328+
/// to structs that the ebpf program needs to interact with. By default rbpf only allows the
329+
/// program to interact with its stack, the memory buffer and the program itself, making it
330+
/// impossible to supply functional implementations of these helpers.
331+
/// This option allows you to pass in a list of addresses that rbpf will allow the program
332+
/// to load and store to. Given rust's memory model you will always know these addresses up
333+
/// front when implementing the helpers.
334+
///
335+
/// Each invocation of this method will append to the set of allowed addresses.
336+
///
337+
/// # Examples
338+
///
339+
/// ```
340+
/// use std::iter::FromIterator;
341+
/// use std::ptr::addr_of;
342+
///
343+
/// struct MapValue {
344+
/// data: u8
345+
/// }
346+
/// static VALUE: MapValue = MapValue { data: 1 };
347+
///
348+
/// let prog = &[
349+
/// 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
350+
/// 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // exit
351+
/// ];
352+
///
353+
/// // Instantiate a VM.
354+
/// let mut vm = rbpf::EbpfVmMbuff::new(Some(prog)).unwrap();
355+
/// let start = addr_of!(VALUE) as u64;
356+
/// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
357+
/// vm.register_allowed_memory(&addrs);
358+
/// ```
359+
pub fn register_allowed_memory(&mut self, addrs: &[u64]) -> () {
360+
for i in addrs {
361+
self.allowed_memory.insert(*i);
362+
}
363+
}
364+
323365
/// Execute the program loaded, with the given packet data and metadata buffer.
324366
///
325367
/// 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> {
357399
/// assert_eq!(res, 0x2211);
358400
/// ```
359401
pub fn execute_program(&self, mem: &[u8], mbuff: &[u8]) -> Result<u64, Error> {
360-
interpreter::execute_program(self.prog, mem, mbuff, &self.helpers)
402+
interpreter::execute_program(self.prog, mem, mbuff, &self.helpers, &self.allowed_memory)
361403
}
362404

363405
/// JIT-compile the loaded program. No argument required for this.
@@ -826,6 +868,44 @@ impl<'a> EbpfVmFixedMbuff<'a> {
826868
self.parent.register_helper(key, function)
827869
}
828870

871+
/// Register an object that the ebpf program is allowed to load and store.
872+
///
873+
/// When using certain helpers, typically map lookups, the linux kernel will return pointers
874+
/// to structs that the ebpf program needs to interact with. By default rbpf only allows the
875+
/// program to interact with its stack, the memory buffer and the program itself, making it
876+
/// impossible to supply functional implementations of these helpers.
877+
/// This option allows you to pass in a list of addresses that rbpf will allow the program
878+
/// to load and store to. Given rust's memory model you will always know these addresses up
879+
/// front when implementing the helpers.
880+
///
881+
/// Each invocation of this method will append to the set of allowed addresses.
882+
///
883+
/// # Examples
884+
///
885+
/// ```
886+
/// use std::iter::FromIterator;
887+
/// use std::ptr::addr_of;
888+
///
889+
/// struct MapValue {
890+
/// data: u8
891+
/// }
892+
/// static VALUE: MapValue = MapValue { data: 1 };
893+
///
894+
/// let prog = &[
895+
/// 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
896+
/// 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // exit
897+
/// ];
898+
///
899+
/// // Instantiate a VM.
900+
/// let mut vm = rbpf::EbpfVmFixedMbuff::new(Some(prog), 0x40, 0x50).unwrap();
901+
/// let start = addr_of!(VALUE) as u64;
902+
/// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
903+
/// vm.register_allowed_memory(&addrs);
904+
/// ```
905+
pub fn register_allowed_memory(&mut self, allowed: &[u64]) -> () {
906+
self.parent.register_allowed_memory(allowed)
907+
}
908+
829909
/// Execute the program loaded, with the given packet data.
830910
///
831911
/// 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> {
12601340
self.parent.register_helper(key, function)
12611341
}
12621342

1343+
/// Register an object that the ebpf program is allowed to load and store.
1344+
///
1345+
/// When using certain helpers, typically map lookups, the linux kernel will return pointers
1346+
/// to structs that the ebpf program needs to interact with. By default rbpf only allows the
1347+
/// program to interact with its stack, the memory buffer and the program itself, making it
1348+
/// impossible to supply functional implementations of these helpers.
1349+
/// This option allows you to pass in a list of addresses that rbpf will allow the program
1350+
/// to load and store to. Given rust's memory model you will always know these addresses up
1351+
/// front when implementing the helpers.
1352+
///
1353+
/// Each invocation of this method will append to the set of allowed addresses.
1354+
///
1355+
/// # Examples
1356+
///
1357+
/// ```
1358+
/// use std::iter::FromIterator;
1359+
/// use std::ptr::addr_of;
1360+
///
1361+
/// struct MapValue {
1362+
/// data: u8
1363+
/// }
1364+
/// static VALUE: MapValue = MapValue { data: 1 };
1365+
///
1366+
/// let prog = &[
1367+
/// 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
1368+
/// 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // exit
1369+
/// ];
1370+
///
1371+
/// // Instantiate a VM.
1372+
/// let mut vm = rbpf::EbpfVmRaw::new(Some(prog)).unwrap();
1373+
/// let start = addr_of!(VALUE) as u64;
1374+
/// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
1375+
/// vm.register_allowed_memory(&addrs);
1376+
/// ```
1377+
pub fn register_allowed_memory(&mut self, allowed: &[u64]) -> () {
1378+
self.parent.register_allowed_memory(allowed)
1379+
}
1380+
12631381
/// Execute the program loaded, with the given packet data.
12641382
///
12651383
/// # Examples
@@ -1602,6 +1720,44 @@ impl<'a> EbpfVmNoData<'a> {
16021720
self.parent.register_helper(key, function)
16031721
}
16041722

1723+
/// Register an object that the ebpf program is allowed to load and store.
1724+
///
1725+
/// When using certain helpers, typically map lookups, the linux kernel will return pointers
1726+
/// to structs that the ebpf program needs to interact with. By default rbpf only allows the
1727+
/// program to interact with its stack, the memory buffer and the program itself, making it
1728+
/// impossible to supply functional implementations of these helpers.
1729+
/// This option allows you to pass in a list of addresses that rbpf will allow the program
1730+
/// to load and store to. Given rust's memory model you will always know these addresses up
1731+
/// front when implementing the helpers.
1732+
///
1733+
/// Each invocation of this method will append to the set of allowed addresses.
1734+
///
1735+
/// # Examples
1736+
///
1737+
/// ```
1738+
/// use std::iter::FromIterator;
1739+
/// use std::ptr::addr_of;
1740+
///
1741+
/// struct MapValue {
1742+
/// data: u8
1743+
/// }
1744+
/// static VALUE: MapValue = MapValue { data: 1 };
1745+
///
1746+
/// let prog = &[
1747+
/// 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
1748+
/// 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // exit
1749+
/// ];
1750+
///
1751+
/// // Instantiate a VM.
1752+
/// let mut vm = rbpf::EbpfVmNoData::new(Some(prog)).unwrap();
1753+
/// let start = addr_of!(VALUE) as u64;
1754+
/// let addrs = Vec::from_iter(start..start+size_of::<MapValue>() as u64);
1755+
/// vm.register_allowed_memory(&addrs);
1756+
/// ```
1757+
pub fn register_allowed_memory(&mut self, allowed: &[u64]) -> () {
1758+
self.parent.register_allowed_memory(allowed)
1759+
}
1760+
16051761
/// JIT-compile the loaded program. No argument required for this.
16061762
///
16071763
/// If using helper functions, be sure to register them into the VM before calling this

0 commit comments

Comments
 (0)