use rustix::mm::{self, MapFlags, MprotectFlags, ProtFlags};
use std::sync::Arc;
use unc_vm_compiler::CompileError;
pub(crate) const ARCH_FUNCTION_ALIGNMENT: u16 = 16;
pub(crate) const DATA_SECTION_ALIGNMENT: u16 = 64;
fn round_up(size: usize, multiple: usize) -> usize {
debug_assert!(multiple.is_power_of_two());
(size + (multiple - 1)) & !(multiple - 1)
}
pub struct CodeMemoryWriter<'a> {
memory: &'a mut CodeMemory,
offset: usize,
}
impl<'a> CodeMemoryWriter<'a> {
pub fn write_data(&mut self, mut alignment: u16, input: &[u8]) -> Result<usize, CompileError> {
if self.offset == self.memory.executable_end {
alignment = u16::try_from(rustix::param::page_size()).expect("page size > u16::MAX");
}
self.write_inner(alignment, input)
}
pub fn write_executable(
&mut self,
alignment: u16,
input: &[u8],
) -> Result<usize, CompileError> {
assert_eq!(
self.memory.executable_end, self.offset,
"may not interleave executable and data in the same map"
);
let result = self.write_inner(alignment, input);
self.memory.executable_end = self.offset;
result
}
fn write_inner(&mut self, alignment: u16, input: &[u8]) -> Result<usize, CompileError> {
let entry_offset = self.offset;
let aligned_offset = round_up(entry_offset, usize::from(alignment));
let final_offset = aligned_offset + input.len();
let out_buffer = self.memory.as_slice_mut();
out_buffer
.get_mut(entry_offset..aligned_offset)
.ok_or_else(|| CompileError::Resource("out of code memory space".into()))?
.fill(0);
out_buffer
.get_mut(aligned_offset..final_offset)
.ok_or_else(|| CompileError::Resource("out of code memory space".into()))?
.copy_from_slice(input);
self.offset = final_offset;
Ok(aligned_offset)
}
pub fn position(&self) -> usize {
self.offset
}
}
pub struct CodeMemory {
source_pool: Option<Arc<crossbeam_queue::ArrayQueue<Self>>>,
map: *mut u8,
size: usize,
executable_end: usize,
}
impl CodeMemory {
fn create(size: usize) -> rustix::io::Result<Self> {
assert!(size != 0);
let size = round_up(size, rustix::param::page_size());
let map = unsafe {
mm::mmap_anonymous(
std::ptr::null_mut(),
size,
ProtFlags::WRITE | ProtFlags::READ,
MapFlags::SHARED,
)?
};
Ok(Self { source_pool: None, map: map.cast(), executable_end: 0, size })
}
fn as_slice_mut(&mut self) -> &mut [u8] {
unsafe {
std::slice::from_raw_parts_mut(self.map, self.size)
}
}
pub fn resize(mut self, size: usize) -> rustix::io::Result<Self> {
if self.size < size {
let source_pool = unsafe {
mm::munmap(self.map.cast(), self.size)?;
let source_pool = self.source_pool.take();
std::mem::forget(self);
source_pool
};
Self::create(size).map(|mut m| {
m.source_pool = source_pool;
m
})
} else {
self.executable_end = 0;
Ok(self)
}
}
pub unsafe fn writer(&mut self) -> CodeMemoryWriter<'_> {
self.executable_end = 0;
CodeMemoryWriter { memory: self, offset: 0 }
}
pub unsafe fn publish(&mut self) -> Result<(), CompileError> {
mm::mprotect(
self.map.cast(),
self.executable_end,
MprotectFlags::EXEC | MprotectFlags::READ,
)
.map_err(|e| {
CompileError::Resource(format!("could not make code memory executable: {}", e))
})
}
pub unsafe fn executable_address(&self, offset: usize) -> *const u8 {
debug_assert!(offset <= isize::MAX as usize);
self.map.offset(offset as isize)
}
pub unsafe fn writable_address(&self, offset: usize) -> *mut u8 {
debug_assert!(offset <= isize::MAX as usize);
self.map.offset(offset as isize)
}
}
impl Drop for CodeMemory {
fn drop(&mut self) {
if let Some(source_pool) = self.source_pool.take() {
unsafe {
let result = mm::mprotect(
self.map.cast(),
self.size,
MprotectFlags::WRITE | MprotectFlags::READ,
);
if let Err(e) = result {
panic!(
"could not mprotect mapping before returning it to the memory pool: \
map={:?}, size={:?}, error={}",
self.map, self.size, e
);
}
}
drop(source_pool.push(Self {
source_pool: None,
map: self.map,
size: self.size,
executable_end: 0,
}));
} else {
unsafe {
if let Err(e) = mm::munmap(self.map.cast(), self.size) {
tracing::error!(
target: "unc_vm",
message="could not unmap mapping",
map=?self.map, size=self.size, error=%e
);
}
}
}
}
}
unsafe impl Send for CodeMemory {}
#[derive(Clone)]
pub struct LimitedMemoryPool {
pool: Arc<crossbeam_queue::ArrayQueue<CodeMemory>>,
}
impl LimitedMemoryPool {
pub fn new(count: usize, default_memory_size: usize) -> rustix::io::Result<Self> {
let pool = Arc::new(crossbeam_queue::ArrayQueue::new(count));
let this = Self { pool };
for _ in 0..count {
this.pool
.push(CodeMemory::create(default_memory_size)?)
.unwrap_or_else(|_| panic!("ArrayQueue could not accomodate {count} memories!"));
}
Ok(this)
}
pub fn get(&self, size: usize) -> rustix::io::Result<CodeMemory> {
let mut memory = self.pool.pop().ok_or(rustix::io::Errno::NOMEM)?;
memory.source_pool = Some(Arc::clone(&self.pool));
if memory.size < size {
Ok(memory.resize(size)?)
} else {
Ok(memory)
}
}
}
#[cfg(test)]
mod tests {
use super::CodeMemory;
fn _assert() {
fn _assert_send<T: Send>() {}
_assert_send::<CodeMemory>();
}
}