Ergonomic bounded collections for eBPF

2 min read

The eBPF verifier enforces strict limits on memory allocation within eBPF programs. Since there is no heap allocator in eBPF programs, using standard Rust collections like String or Vec will be rejected by the verifier. All memory needs to be statically bounded and verifiable at load time.

Bounded containers

I wrote custom containers as a potential solution to this issue: BoundedStr and BoundedVec. They use fixed-size memory but keep track of their actual length internally. Because they are fundamentally just fixed-size arrays, they are safe to allocate on the eBPF stack or store in maps without choking the verifier.

We need to make sure we never read past len elements when accessing these collections.

The BoundedStr implementation:

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct BoundedStr<const MAX_SIZE: usize> {
    pub len: u32,
    pub data: [u8; MAX_SIZE],
}

The BoundedVec looks almost identical, but takes a generic type T for its value:

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct BoundedVec<T, const MAX_SIZE: usize>
where
    T: Copy + Clone + Default,
{
    pub len: u32,
    pub data: [T; MAX_SIZE],
}

Ergonomics with Deref and DerefMut

Despite the collections being simple, constantly accessing the .data field when trying to read or mutate the underlying data might get tedious.

impl<const MAX_SIZE: usize> Deref for BoundedStr<MAX_SIZE> {
    type Target = [u8; MAX_SIZE];

    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

impl<const MAX_SIZE: usize> DerefMut for BoundedStr<MAX_SIZE> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.data
    }
}

With Deref in place, any method that works on a [u8; MAX_SIZE] or a standard slice now works on our BoundedStr.