#![cfg_attr(target_os = "windows", allow(dead_code))]
use std::marker::PhantomData;
use std::ops::Range;
use std::ptr::NonNull;
use std::sync::{Arc, LazyLock, Mutex};
use crate::error::EbpfError;
#[cfg(not(target_os = "windows"))]
extern crate libc;
#[cfg(not(target_os = "windows"))]
use libc::c_void;
#[cfg(target_os = "windows")]
use {
core::ffi::c_void,
windows_sys::Win32::{
Foundation::GetLastError,
System::{
Memory::{
VirtualAlloc, VirtualFree, VirtualProtect, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE,
MEM_RESET, PAGE_EXECUTE_READ, PAGE_PROTECTION_FLAGS, PAGE_READONLY, PAGE_READWRITE,
},
SystemInformation::{GetSystemInfo, SYSTEM_INFO},
},
},
};
struct FreeList {
mem: Mutex<Vec<Pages>>,
size: usize,
}
impl FreeList {
fn new(size: usize) -> Self {
Self {
mem: Mutex::new(Vec::new()),
size,
}
}
fn alloc(&self) -> Pages {
let pages = { self.mem.lock().unwrap_or_else(|e| e.into_inner()).pop() };
let mut pages = pages.unwrap_or_else(|| Pages::new(self.size).expect("allocation failed"));
pages.pooled = true;
pages
}
fn free(&self, pages: Pages) {
const MADV_DONTNEED_THRESHOLD: usize = 1024 * 1024 * 128;
if pages.len != self.size {
panic!("size mismatch: expected {}, got {}", self.size, pages.len);
}
if self.size >= MADV_DONTNEED_THRESHOLD {
if let Err(e) = unsafe { madvise(pages.raw.as_ptr(), pages.len, Advice::DontNeed) } {
log::error!("FreeList: unable to advise returned allocation: {e}");
}
}
self.mem
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(pages);
}
}
const BUCKET_MIN: usize = 1024 * 128; const BUCKET_MAX: usize = 1024 * 1024 * 256; const BUCKET_COUNT: usize =
(BUCKET_MAX.trailing_zeros() - BUCKET_MIN.trailing_zeros()) as usize + 1;
const _: () = assert!(BUCKET_MIN.is_power_of_two());
const _: () = assert!(BUCKET_MAX.is_power_of_two());
const _: () = assert!(BUCKET_MIN <= BUCKET_MAX);
const _: () = assert!(BUCKET_MAX == BUCKET_MIN * (1 << (BUCKET_COUNT - 1)));
struct BucketedFreeList {
buckets: [FreeList; BUCKET_COUNT],
}
impl BucketedFreeList {
#[expect(clippy::arithmetic_side_effects)]
fn new() -> Self {
Self {
buckets: core::array::from_fn(|i| FreeList::new(BUCKET_MIN * (1 << i))),
}
}
#[inline]
#[expect(clippy::arithmetic_side_effects)]
fn bucket_idx(size: usize) -> usize {
let bucket_bits = usize::BITS - (size.max(BUCKET_MIN) - 1).leading_zeros();
bucket_bits as usize - const { BUCKET_MIN.trailing_zeros() as usize }
}
fn alloc(&self, size: usize) -> Pages {
self.buckets[Self::bucket_idx(size)].alloc()
}
fn free(&self, pages: Pages) {
self.buckets[Self::bucket_idx(pages.len)].free(pages)
}
}
static ALLOCATOR: LazyLock<BucketedFreeList> = LazyLock::new(BucketedFreeList::new);
struct Pages {
raw: NonNull<u8>,
len: usize,
pooled: bool,
}
unsafe impl Send for Pages {}
unsafe impl Sync for Pages {}
impl Pages {
fn new(len: usize) -> Result<Self, EbpfError> {
let raw = allocate_pages(len)?;
Ok(Self {
raw: NonNull::new(raw).expect("mapped pages at a null address"),
len,
pooled: false,
})
}
}
impl Drop for Pages {
fn drop(&mut self) {
let raw = self.raw.as_ptr();
if self.pooled {
match unsafe { protect_pages(raw, self.len, PagePermissions::ReadWrite) } {
Ok(()) => {
return ALLOCATOR.free(Pages {
raw: self.raw,
len: self.len,
pooled: false,
})
}
Err(error) => {
log::error!("unable to return pages to the pool: {error}");
}
}
}
if let Err(error) = unsafe { free_pages(raw, self.len) } {
log::error!("unable to free pages: {error}");
}
}
}
pub(crate) trait Access {
const PERMISSIONS: PagePermissions;
}
pub(crate) enum Read {}
impl Access for Read {
const PERMISSIONS: PagePermissions = PagePermissions::Read;
}
pub(crate) enum ReadWrite {}
impl Access for ReadWrite {
const PERMISSIONS: PagePermissions = PagePermissions::ReadWrite;
}
pub(crate) enum ReadExecute {}
impl Access for ReadExecute {
const PERMISSIONS: PagePermissions = PagePermissions::ReadExecute;
}
pub(crate) struct Mapping<A = ReadWrite> {
pages: Arc<Pages>,
ptr: NonNull<u8>,
len: usize,
access: PhantomData<A>,
}
unsafe impl<A> Send for Mapping<A> {}
unsafe impl<A> Sync for Mapping<A> {}
impl Mapping {
pub(crate) fn pooled(len: usize) -> Self {
Self::whole(ALLOCATOR.alloc(len))
}
fn whole(pages: Pages) -> Self {
Self {
ptr: pages.raw,
len: pages.len,
pages: Arc::new(pages),
access: PhantomData,
}
}
pub(crate) fn split<const N: usize>(
mut self,
ranges: [Range<usize>; N],
) -> Result<[Self; N], Self> {
let page_size = get_system_page_size();
for range in &ranges {
if !range.start.is_multiple_of(page_size) {
return Err(self);
}
}
let Ok(parts) = self.get_disjoint_mut(ranges.clone()) else {
return Err(self);
};
let parts = parts.map(|part| NonNull::from(part).cast::<u8>());
Ok(std::array::from_fn(|i| Self {
pages: Arc::clone(&self.pages),
ptr: parts[i],
len: ranges[i].len(),
access: PhantomData,
}))
}
}
impl<A> Mapping<A> {
pub(crate) fn protect<B: Access>(self) -> Result<Mapping<B>, EbpfError> {
if self.len != 0 {
unsafe { protect_pages(self.ptr.as_ptr(), self.len, B::PERMISSIONS) }?;
}
Ok(Mapping {
pages: self.pages,
ptr: self.ptr,
len: self.len,
access: PhantomData,
})
}
pub(crate) fn truncate(&mut self, len: usize) {
self.len = self.len.min(len);
}
pub(crate) fn mapped_len(&self) -> usize {
self.pages.len
}
}
macro_rules! impl_many {
(impl $trait:path [$(for $ty:ty),*] $body:tt) => {
$( impl $trait for $ty $body )*
};
}
impl_many! {
impl std::ops::Deref [
for Mapping<Read>,
for Mapping<ReadWrite>,
for Mapping<ReadExecute>
] {
type Target = [u8];
#[inline]
fn deref(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
}
}
impl std::ops::DerefMut for Mapping<ReadWrite> {
#[inline]
fn deref_mut(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
}
}
#[cfg(not(target_os = "windows"))]
macro_rules! libc_error_guard {
(succeeded?, mmap, $addr:expr, $($arg:expr),*) => {{
*$addr = libc::mmap(*$addr, $($arg),*);
*$addr != libc::MAP_FAILED
}};
(succeeded?, $function:ident, $($arg:expr),*) => {
libc::$function($($arg),*) == 0
};
($function:ident, $($arg:expr),* $(,)?) => {{
const RETRY_COUNT: usize = 3;
for i in 0..RETRY_COUNT {
if libc_error_guard!(succeeded?, $function, $($arg),*) {
break;
} else if i.saturating_add(1) == RETRY_COUNT {
let args = vec![$(format!("{:?}", $arg)),*];
#[cfg(any(target_os = "freebsd", target_os = "ios", target_os = "macos"))]
let errno = *libc::__error();
#[cfg(any(target_os = "android", target_os = "netbsd", target_os = "openbsd"))]
let errno = *libc::__errno();
#[cfg(target_os = "linux")]
let errno = *libc::__errno_location();
return Err(EbpfError::LibcInvocationFailed(stringify!($function), args, errno));
}
}
}};
}
#[cfg(target_os = "windows")]
macro_rules! winapi_error_guard {
(succeeded?, VirtualAlloc, $addr:expr, $($arg:expr),*) => {{
*$addr = VirtualAlloc(*$addr, $($arg),*);
!(*$addr).is_null()
}};
(succeeded?, $function:ident, $($arg:expr),*) => {
$function($($arg),*) != 0
};
($function:ident, $($arg:expr),* $(,)?) => {{
if !winapi_error_guard!(succeeded?, $function, $($arg),*) {
let args = vec![$(format!("{:?}", $arg)),*];
let errno = GetLastError();
return Err(EbpfError::LibcInvocationFailed(stringify!($function), args, errno as i32));
}
}};
}
pub(crate) fn get_system_page_size() -> usize {
#[cfg(not(target_os = "windows"))]
unsafe {
libc::sysconf(libc::_SC_PAGESIZE) as usize
}
#[cfg(target_os = "windows")]
unsafe {
let mut system_info: SYSTEM_INFO = std::mem::zeroed();
GetSystemInfo(&mut system_info);
system_info.dwPageSize as usize
}
}
pub(crate) fn round_to_page_size(value: usize, page_size: usize) -> usize {
value
.saturating_add(page_size)
.saturating_sub(1)
.checked_div(page_size)
.unwrap()
.saturating_mul(page_size)
}
fn allocate_pages(size_in_bytes: usize) -> Result<*mut u8, EbpfError> {
unsafe {
let mut raw: *mut c_void = std::ptr::null_mut();
cfg_select! {
windows => {
winapi_error_guard!(
VirtualAlloc,
&mut raw,
size_in_bytes,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE,
);
}
target_os = "linux" => {
libc_error_guard!(
mmap,
&mut raw,
size_in_bytes,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_ANONYMOUS | libc::MAP_SHARED,
-1,
0,
);
libc_error_guard!(madvise, raw, size_in_bytes, libc::MADV_DONTFORK);
}
_ => {
libc_error_guard!(
mmap,
&mut raw,
size_in_bytes,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
-1,
0,
);
}
}
Ok(raw.cast())
}
}
unsafe fn free_pages(raw: *mut u8, size_in_bytes: usize) -> Result<(), EbpfError> {
#[cfg(not(target_os = "windows"))]
libc_error_guard!(munmap, raw.cast::<c_void>(), size_in_bytes);
#[cfg(target_os = "windows")]
winapi_error_guard!(
VirtualFree,
raw.cast::<c_void>(),
size_in_bytes,
MEM_RELEASE,
);
Ok(())
}
#[derive(Copy, Clone)]
pub(crate) enum PagePermissions {
Read,
ReadWrite,
ReadExecute,
}
unsafe fn protect_pages(
raw: *mut u8,
size_in_bytes: usize,
permissions: PagePermissions,
) -> Result<(), EbpfError> {
#[cfg(not(target_os = "windows"))]
{
let prot = match permissions {
PagePermissions::Read => libc::PROT_READ,
PagePermissions::ReadWrite => libc::PROT_READ | libc::PROT_WRITE,
PagePermissions::ReadExecute => libc::PROT_READ | libc::PROT_EXEC,
};
libc_error_guard!(mprotect, raw.cast::<c_void>(), size_in_bytes, prot);
}
#[cfg(target_os = "windows")]
{
let mut old: PAGE_PROTECTION_FLAGS = 0;
let ptr_old: *mut PAGE_PROTECTION_FLAGS = &mut old;
let prot = match permissions {
PagePermissions::Read => PAGE_READONLY,
PagePermissions::ReadWrite => PAGE_READWRITE,
PagePermissions::ReadExecute => PAGE_EXECUTE_READ,
};
winapi_error_guard!(
VirtualProtect,
raw.cast::<c_void>(),
size_in_bytes,
prot,
ptr_old,
);
}
Ok(())
}
#[derive(Clone, Copy)]
enum Advice {
DontNeed,
}
unsafe fn madvise(raw: *mut u8, size_in_bytes: usize, advice: Advice) -> Result<(), EbpfError> {
#[cfg(not(target_os = "windows"))]
{
let advice = match advice {
Advice::DontNeed => libc::MADV_DONTNEED,
};
libc_error_guard!(madvise, raw.cast::<c_void>(), size_in_bytes, advice);
}
#[cfg(target_os = "windows")]
{
let mut ptr = raw.cast::<c_void>();
let advice = match advice {
Advice::DontNeed => MEM_RESET,
};
winapi_error_guard!(
VirtualAlloc,
&mut ptr,
size_in_bytes,
advice,
PAGE_READWRITE,
);
}
Ok(())
}
#[cfg(test)]
#[expect(clippy::single_range_in_vec_init)]
mod tests {
use super::*;
fn new(len: usize) -> Mapping {
Mapping::whole(Pages::new(len).unwrap())
}
#[test]
fn mapping_is_zeroed() {
let mapping = new(get_system_page_size());
assert!(mapping.iter().all(|&byte| byte == 0));
}
#[test]
fn split_and_protect() {
let page_size = get_system_page_size();
let mapping = new(4 * page_size);
let start = mapping.as_ptr();
let Ok([mut a, b, mut c, empty]) = mapping.split([
0..4,
page_size..2 * page_size + 1,
3 * page_size..4 * page_size,
4 * page_size..4 * page_size,
]) else {
unreachable!()
};
assert_eq!(a.len(), 4);
assert_eq!(b.len(), page_size + 1);
assert_eq!(c.as_ptr(), start.wrapping_add(3 * page_size));
assert!(empty.is_empty());
a.copy_from_slice(&[1, 2, 3, 4]);
let a = a.protect::<Read>().unwrap();
let empty = empty.protect::<ReadExecute>().unwrap();
c.fill(5);
drop(b);
assert_eq!(*a, [1, 2, 3, 4]);
let Ok([c0, c1]) = c.split([0..0, 0..2]) else {
unreachable!()
};
let c1 = c1.protect::<ReadExecute>().unwrap();
let mut c1 = c1.protect::<ReadWrite>().unwrap();
c1[1] = 6;
assert_eq!(*c1, [5, 6]);
assert_eq!(a.mapped_len(), 4 * page_size);
c1.truncate(1);
assert_eq!(*c1, [5]);
drop((a, c0, empty));
}
#[test]
fn split_within_page() {
assert!(new(get_system_page_size()).split([1..2]).is_err());
}
#[test]
fn split_sharing_page() {
let page_size = get_system_page_size();
let ranges = [0..page_size + 1, page_size..2 * page_size];
assert!(new(2 * page_size).split(ranges).is_err());
}
#[test]
fn split_out_of_bounds() {
let page_size = get_system_page_size();
assert!(new(page_size).split([0..page_size + 1]).is_err());
}
#[test]
fn split_reversed() {
let page_size = get_system_page_size();
assert!(new(2 * page_size).split([page_size..1]).is_err());
}
#[test]
fn pooled_mapping_returns_writable() {
for _ in 0..4 {
let mut mapping = Mapping::pooled(BUCKET_MIN);
assert_eq!(mapping.len(), BUCKET_MIN);
mapping.fill(0xcc);
let Ok([head, tail]) = mapping.split([0..1, BUCKET_MIN / 2..BUCKET_MIN]) else {
unreachable!()
};
let head = head.protect::<Read>().unwrap();
let tail = tail.protect::<ReadExecute>().unwrap();
assert_eq!(tail[BUCKET_MIN / 2 - 1], 0xcc);
drop(tail);
assert_eq!(*head, [0xcc]);
}
}
}