#![expect(
unsafe_code,
reason = "generated code reads the page table and calls back into Rust through raw pointers"
)]
use std::cell::Cell;
use std::collections::{BTreeMap, BTreeSet};
use std::ops::RangeInclusive;
use cranelift_codegen::ir::{
self, AbiParam, Endianness, InstBuilder, MemFlagsData, Signature, Value, types,
};
use cranelift_codegen::isa::CallConv;
use cranelift_codegen::settings::{self, Configurable};
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_jit::{BranchProtection, JITBuilder, JITMemoryKind, JITMemoryProvider, JITModule};
use cranelift_module::{Linkage, Module, ModuleError};
use super::{DataModel, Error};
use crate::soc::Endian;
pub(crate) const ADDRESSES: u64 = 1 << 32;
pub(crate) const PAGE_BITS: u32 = 10;
pub(crate) const PAGE_SIZE: u64 = 1 << PAGE_BITS;
const READ: usize = 1;
const WRITE: usize = 2;
const CODE: usize = 4;
const MAPPED: usize = 8;
const WRITABLE: usize = 16;
const ROUTED: usize = 32;
pub(crate) struct Pages {
entries: Box<[Cell<usize>]>,
mapped: BTreeMap<u64, (usize, bool)>,
views: BTreeMap<usize, Vec<u64>>,
routed: BTreeSet<u64>,
code: BTreeSet<u64>,
checked: bool,
allowed: BTreeMap<u64, Inline>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum Inline {
#[default]
Neither,
Reads,
Both,
}
impl std::fmt::Debug for Pages {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Pages")
.field("mapped", &self.mapped.len())
.field("routed", &self.routed)
.field("code", &self.code.len())
.finish_non_exhaustive()
}
}
impl Pages {
pub(crate) fn new(model: DataModel) -> Result<Self, Error> {
if u64::from(model.pointer().bits()) > u64::from(ADDRESSES.trailing_zeros()) {
return Err(Error::Machine(format!(
"the core's page table holds {}-bit addresses, and a family with {}-bit pointers needs a wider one",
ADDRESSES.trailing_zeros(),
model.pointer().bits()
)));
}
let pages = usize::try_from(ADDRESSES >> PAGE_BITS).expect("a table the host can hold");
let zeros = vec![0_usize; pages].into_boxed_slice();
Ok(Self {
entries: unsafe { Box::from_raw(Box::into_raw(zeros) as *mut [Cell<usize>]) },
mapped: BTreeMap::new(),
views: BTreeMap::new(),
routed: BTreeSet::new(),
code: BTreeSet::new(),
checked: false,
allowed: BTreeMap::new(),
})
}
pub(crate) fn holds(start: u64, length: u64) -> bool {
start
.checked_add(length)
.is_some_and(|end| end <= ADDRESSES)
}
pub(crate) fn table(&self) -> *const usize {
self.entries.as_ptr().cast()
}
pub(crate) unsafe fn map(&mut self, start: u64, length: u64, host: *mut u8, writable: bool) {
assert!(
start.is_multiple_of(PAGE_SIZE) && length.is_multiple_of(PAGE_SIZE),
"whole pages"
);
assert!(Self::holds(start, length), "addresses the table covers");
assert!(
(host as usize).is_multiple_of(usize::try_from(PAGE_SIZE).expect("a page")),
"a page-aligned host page"
);
for offset in (0..length).step_by(usize::try_from(PAGE_SIZE).expect("a page")) {
let page = (start + offset) >> PAGE_BITS;
let at = host as usize + usize::try_from(offset).expect("inside a region in memory");
if let Some((before, _)) = self.mapped.insert(page, (at, writable))
&& let Some(views) = self.views.get_mut(&before)
{
views.retain(|&view| view != page);
}
self.views.entry(at).or_default().push(page);
self.refresh_views(page);
}
}
pub(crate) fn views(&self, start: u64, end: u64) -> Vec<(u64, u64)> {
let mut ranges = Vec::new();
let mut from = start;
while from < end {
let page = from >> PAGE_BITS;
let to = end.min((page + 1) << PAGE_BITS);
match self.mapped.get(&page) {
Some((at, _)) => {
for &view in &self.views[at] {
let moved = view << PAGE_BITS | from & (PAGE_SIZE - 1);
ranges.push((moved, moved + (to - from)));
}
}
None => ranges.push((from, to)),
}
from = to;
}
ranges
}
pub(crate) fn touches_code(&self, start: u64, end: u64) -> bool {
if self.code.is_empty() || end <= start {
return false;
}
(start >> PAGE_BITS..=(end - 1) >> PAGE_BITS).any(|page| {
match usize::try_from(page)
.ok()
.and_then(|page| self.entries.get(page))
.map(Cell::get)
{
Some(entry) if entry & MAPPED != 0 => entry & CODE != 0,
_ => self.code.contains(&page),
}
})
}
pub(crate) fn host(&self, address: u64) -> Option<(*mut u8, bool)> {
let entry = self.entry(address);
if entry & MAPPED == 0 {
return None;
}
let at = entry & !usize::try_from(PAGE_SIZE - 1).expect("a page");
let offset = usize::try_from(address & (PAGE_SIZE - 1)).expect("inside a page");
Some(((at + offset) as *mut u8, entry & WRITABLE != 0))
}
fn entry(&self, address: u64) -> usize {
usize::try_from(address >> PAGE_BITS)
.ok()
.and_then(|page| self.entries.get(page))
.map_or(0, Cell::get)
}
pub(crate) fn routed(&self, address: u64) -> bool {
let entry = self.entry(address);
if entry & MAPPED != 0 {
return entry & ROUTED != 0;
}
self.routed.contains(&(address >> PAGE_BITS))
}
pub(crate) fn route(&mut self, address: u64, routed: bool) {
let page = address >> PAGE_BITS;
if routed {
self.routed.insert(page);
} else {
self.routed.remove(&page);
}
self.refresh(page);
}
pub(crate) fn hold_code(&mut self, address: u64) {
let page = address >> PAGE_BITS;
if self.code.insert(page) {
self.refresh_views(page);
}
}
pub(crate) fn release_code(&mut self, address: u64) {
let page = address >> PAGE_BITS;
if self.code.remove(&page) {
self.refresh_views(page);
}
}
pub(crate) fn release_all_code(&mut self) {
for page in std::mem::take(&mut self.code) {
self.refresh_views(page);
}
}
pub(crate) fn check_all(&mut self, checked: bool) {
if self.checked != checked || !self.allowed.is_empty() {
self.checked = checked;
self.allowed.clear();
let pages: Vec<u64> = self.mapped.keys().copied().collect();
for page in pages {
self.refresh(page);
}
}
}
pub(crate) fn decided(&self, address: u64) -> bool {
self.allowed.contains_key(&(address >> PAGE_BITS))
}
pub(crate) fn allow(&mut self, address: u64, inline: Inline) {
let page = address >> PAGE_BITS;
self.allowed.insert(page, inline);
self.refresh(page);
}
#[cfg(test)]
pub(crate) fn inline(&self, address: u64) -> Inline {
let entry = self.entries[usize::try_from(address >> PAGE_BITS).expect("a page")].get();
match (entry & READ != 0, entry & WRITE != 0) {
(false, _) => Inline::Neither,
(true, false) => Inline::Reads,
(true, true) => Inline::Both,
}
}
fn refresh_views(&mut self, page: u64) {
let Some(&(at, _)) = self.mapped.get(&page) else {
return;
};
for view in self.views[&at].clone() {
self.refresh(view);
}
}
fn refresh(&mut self, page: u64) {
let entry = match self.mapped.get(&page) {
Some(&(at, writable)) => {
let inline = if self.routed.contains(&page) {
Inline::Neither
} else if self.checked {
self.allowed.get(&page).copied().unwrap_or_default()
} else {
Inline::Both
};
let code = self.views[&at].iter().any(|view| self.code.contains(view));
let mut entry = match inline {
Inline::Neither => at,
Inline::Both if writable && !code => at | READ | WRITE,
Inline::Reads | Inline::Both => at | READ,
} | MAPPED;
for (holds, bit) in [
(code, CODE),
(writable, WRITABLE),
(self.routed.contains(&page), ROUTED),
] {
if holds {
entry |= bit;
}
}
entry
}
None => 0,
};
if let Some(slot) = usize::try_from(page)
.ok()
.and_then(|page| self.entries.get(page))
{
slot.set(entry);
}
}
}
pub(crate) fn guest_memory() -> MemFlagsData {
MemFlagsData::new()
.with_notrap()
.with_endianness(Endianness::Little)
}
pub(crate) fn load_guest(
builder: &mut FunctionBuilder<'_>,
kind: ir::Type,
host: Value,
endian: Endian,
) -> Value {
let value = builder.ins().load(kind, guest_memory(), host, 0);
in_order(builder, value, endian)
}
pub(crate) fn store_guest(
builder: &mut FunctionBuilder<'_>,
value: Value,
host: Value,
endian: Endian,
) {
let value = in_order(builder, value, endian);
builder.ins().store(guest_memory(), value, host, 0);
}
fn in_order(builder: &mut FunctionBuilder<'_>, value: Value, endian: Endian) -> Value {
if endian == Endian::Big && builder.func.dfg.value_type(value).bytes() > 1 {
builder.ins().bswap(value)
} else {
value
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Lookup {
Read(u32),
Write(u32),
}
pub(crate) fn emit_lookup(
builder: &mut FunctionBuilder<'_>,
table: Value,
address: Value,
lookup: Lookup,
slow: ir::Block,
) -> Value {
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
const _: () = assert!(size_of::<usize>() == 8, "a table entry is eight bytes");
let (bytes, need) = match lookup {
Lookup::Read(bytes) => (bytes, READ),
Lookup::Write(bytes) => (bytes, WRITE),
};
let wide = builder.ins().uextend(types::I64, address);
let page = builder.ins().ushr_imm_u(wide, i64::from(PAGE_BITS));
let slot = builder.ins().ishl_imm_s(page, 3);
let slot = builder.ins().iadd(table, slot);
let entry = builder
.ins()
.load(types::I64, MemFlagsData::trusted(), slot, 0);
let allowed = builder
.ins()
.band_imm_s(entry, i64::try_from(need).expect("a flag"));
let allowed = builder
.ins()
.icmp_imm_s(ir::condcodes::IntCC::NotEqual, allowed, 0);
let allowed = builder.ins().uextend(types::I64, allowed);
let offset = builder
.ins()
.band_imm_s(wide, i64::try_from(PAGE_SIZE - 1).expect("a page"));
let misaligned = builder.ins().band_imm_s(wide, i64::from(bytes) - 1);
let aligned = builder
.ins()
.icmp_imm_s(ir::condcodes::IntCC::Equal, misaligned, 0);
let aligned = builder.ins().uextend(types::I64, aligned);
let fast = builder.ins().band(allowed, aligned);
let inline = builder.create_block();
builder.ins().brif(fast, inline, &[], slow, &[]);
builder.seal_block(inline);
builder.switch_to_block(inline);
let base = builder
.ins()
.band_imm_s(entry, !i64::try_from(PAGE_SIZE - 1).expect("a page"));
builder.ins().iadd(base, offset)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(transparent)]
pub(crate) struct Block(*const u8);
impl Block {
pub(crate) fn address(self) -> usize {
self.0 as usize
}
}
pub(crate) fn host_convention() -> CallConv {
if cfg!(all(windows, target_arch = "x86_64")) {
CallConv::WindowsFastcall
} else if cfg!(all(target_vendor = "apple", target_arch = "aarch64")) {
CallConv::AppleAarch64
} else {
CallConv::SystemV
}
}
type Enter = unsafe extern "C" fn(*mut u8, *const u8) -> u32;
const CODE_SPACE: usize = 1 << 30;
struct Packed {
region: region::Allocation,
used: usize,
pending: Option<std::ops::Range<usize>>,
}
unsafe impl Send for Packed {}
impl Packed {
fn new() -> Result<Self, region::Error> {
Ok(Self {
region: region::alloc(CODE_SPACE, region::Protection::NONE)?,
used: 0,
pending: None,
})
}
fn pages(&self, range: &std::ops::Range<usize>) -> (*const u8, usize) {
let page = region::page::size();
let start = range.start / page * page;
let end = range.end.div_ceil(page) * page;
(self.region.as_ptr::<u8>().wrapping_add(start), end - start)
}
}
impl JITMemoryProvider for Packed {
fn allocate(
&mut self,
size: usize,
align: u64,
kind: JITMemoryKind,
) -> std::io::Result<*mut u8> {
if !matches!(kind, JITMemoryKind::Executable) {
return Err(std::io::Error::other(
"the core's code space holds only code",
));
}
let align = usize::try_from(align).map_err(std::io::Error::other)?;
let start = self.used.next_multiple_of(align.max(1));
let end = start + size;
if end > CODE_SPACE {
return Err(std::io::Error::new(
std::io::ErrorKind::OutOfMemory,
"the core's code space is full",
));
}
let pending = self.pending.take().unwrap_or(start..start);
let pending = pending.start.min(start)..end;
let (pages, length) = self.pages(&pending);
unsafe { region::protect(pages, length, region::Protection::READ_WRITE) }
.map_err(std::io::Error::other)?;
self.pending = Some(pending);
self.used = end;
Ok(self.region.as_mut_ptr::<u8>().wrapping_add(start))
}
fn finalize(&mut self, _protection: BranchProtection) -> cranelift_module::ModuleResult<()> {
let Some(pending) = self.pending.take() else {
return Ok(());
};
let (pages, length) = self.pages(&pending);
let backend = |error: &dyn std::fmt::Display| ModuleError::Allocation {
err: std::io::Error::other(error.to_string()),
};
unsafe { wasmtime_internal_jit_icache_coherence::clear_cache(pages.cast(), length) }
.map_err(|error| backend(&error))?;
unsafe { region::protect(pages, length, region::Protection::READ_EXECUTE) }
.map_err(|error| backend(&error))
}
unsafe fn free_memory(&mut self) {
self.used = 0;
self.pending = None;
unsafe {
region::protect(
self.region.as_ptr::<u8>(),
CODE_SPACE,
region::Protection::NONE,
)
}
.ok();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Tier {
Quick,
Best,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Fma {
Instruction,
Library,
}
#[derive(Clone, Copy)]
pub(crate) struct Entry(Enter);
impl Entry {
pub(crate) unsafe fn run(self, core: *mut u8, block: Block) -> u32 {
unsafe { (self.0)(core, block.0) }
}
}
pub(crate) struct Jit {
module: JITModule,
quick: JITModule,
context: cranelift_codegen::Context,
builder: FunctionBuilderContext,
signature: Signature,
enter: Enter,
made: u64,
out_of_room: bool,
}
impl std::fmt::Debug for Jit {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Jit")
.field("made", &self.made)
.finish_non_exhaustive()
}
}
fn failed(error: &dyn std::fmt::Display) -> Error {
Error::Machine(format!("the core couldn't translate a block: {error}"))
}
impl Jit {
pub(crate) fn new() -> Result<Self, Error> {
if !cfg!(any(target_arch = "x86_64", target_arch = "aarch64")) {
return Err(Error::Machine(
"the core generates code only on an x86-64 or AArch64 host".to_owned(),
));
}
let mut module = Self::module(Tier::Best)?;
let quick = Self::module(Tier::Quick)?;
let pointer = module.isa().pointer_type();
let mut signature = Signature::new(CallConv::Tail);
signature.params.push(AbiParam::new(pointer));
signature.returns.push(AbiParam::new(types::I32));
let mut context = module.make_context();
let mut builder = FunctionBuilderContext::new();
let enter = Self::make_enter(&mut module, &mut context, &mut builder, &signature)?;
Ok(Self {
context,
module,
quick,
builder,
signature,
enter,
made: 0,
out_of_room: false,
})
}
fn module(tier: Tier) -> Result<JITModule, Error> {
let unsupported = |error: &dyn std::fmt::Display| {
Error::Machine(format!(
"the core can't generate code for this host: {error}"
))
};
let (level, allocator) = match tier {
Tier::Quick => ("none", "single_pass"),
Tier::Best => ("none", "backtracking"),
};
let mut flags = settings::builder();
flags
.set("opt_level", level)
.map_err(|error| unsupported(&error))?;
flags
.set("regalloc_algorithm", allocator)
.map_err(|error| unsupported(&error))?;
flags
.set(
"enable_verifier",
if cfg!(debug_assertions) {
"true"
} else {
"false"
},
)
.map_err(|error| unsupported(&error))?;
flags
.set("preserve_frame_pointers", "true")
.map_err(|error| unsupported(&error))?;
flags
.set("enable_pinned_reg", "true")
.map_err(|error| unsupported(&error))?;
let isa = cranelift_native::builder()
.map_err(|error| unsupported(&error))?
.finish(settings::Flags::new(flags))
.map_err(|error| unsupported(&error))?;
let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
builder.memory_provider(Box::new(
Packed::new().map_err(|error| unsupported(&error))?,
));
Ok(JITModule::new(builder))
}
#[expect(
clippy::disallowed_methods,
reason = "a function pointer isn't plain old data, and only transmute makes one from code"
)]
fn make_enter(
module: &mut JITModule,
context: &mut cranelift_codegen::Context,
builder: &mut FunctionBuilderContext,
block: &Signature,
) -> Result<Enter, Error> {
let pointer = module.isa().pointer_type();
let mut signature = module.make_signature();
signature.params.push(AbiParam::new(pointer));
signature.params.push(AbiParam::new(pointer));
signature.returns.push(AbiParam::new(types::I32));
module.clear_context(context);
context.func.signature = signature.clone();
let config = module.target_config();
{
let mut builder = FunctionBuilder::new(&mut context.func, builder);
let entry = builder.create_block();
builder.append_block_params_for_function_params(entry);
builder.switch_to_block(entry);
builder.seal_block(entry);
let (core, code) = (
builder.block_params(entry)[0],
builder.block_params(entry)[1],
);
let block = builder.import_signature(block.clone());
let kept = builder.ins().get_pinned_reg(pointer);
builder.ins().set_pinned_reg(core);
let call = builder.ins().call_indirect(block, code, &[core]);
let answer = builder.inst_results(call)[0];
builder.ins().set_pinned_reg(kept);
builder.ins().return_(&[answer]);
builder.finalize(config);
}
let id = module
.declare_function("enter", Linkage::Local, &signature)
.map_err(|error| failed(&error))?;
module
.define_function(id, context)
.map_err(|error| failed(&error))?;
module
.finalize_definitions()
.map_err(|error| failed(&error))?;
let code = module.get_finalized_function(id);
Ok(unsafe { std::mem::transmute::<*const u8, Enter>(code) })
}
pub(crate) fn pointer(&self) -> ir::Type {
self.module.isa().pointer_type()
}
pub(crate) fn fma(&self) -> Fma {
if self.module.isa().has_native_fma() {
Fma::Instruction
} else {
Fma::Library
}
}
pub(crate) fn compile(
&mut self,
tier: Tier,
build: impl FnOnce(&mut FunctionBuilder<'_>, Value, &Signature),
) -> Result<Block, Error> {
self.out_of_room = false;
let module = match tier {
Tier::Quick => &mut self.quick,
Tier::Best => &mut self.module,
};
module.clear_context(&mut self.context);
self.context.func.signature = self.signature.clone();
let config = module.target_config();
{
let mut builder = FunctionBuilder::new(&mut self.context.func, &mut self.builder);
let entry = builder.create_block();
builder.append_block_params_for_function_params(entry);
builder.switch_to_block(entry);
builder.seal_block(entry);
let core = builder.block_params(entry)[0];
build(&mut builder, core, &self.signature);
builder.seal_all_blocks();
builder.finalize(config);
}
self.made += 1;
let id = module
.declare_function(&format!("b{}", self.made), Linkage::Local, &self.signature)
.map_err(|error| failed(&error))?;
module
.define_function(id, &mut self.context)
.map_err(|error| {
self.out_of_room = matches!(&error, ModuleError::Allocation { err }
if err.kind() == std::io::ErrorKind::OutOfMemory);
failed(&error)
})?;
module
.finalize_definitions()
.map_err(|error| failed(&error))?;
Ok(Block(module.get_finalized_function(id)))
}
pub(crate) fn entry(&self) -> Entry {
Entry(self.enter)
}
pub(crate) fn made(&self) -> u64 {
self.made
}
pub(crate) fn out_of_room(&self) -> bool {
self.out_of_room
}
pub(crate) unsafe fn clear(&mut self) -> Result<(), Error> {
let mut module = Self::module(Tier::Best)?;
let quick = std::mem::replace(&mut self.quick, Self::module(Tier::Quick)?);
self.builder = FunctionBuilderContext::new();
unsafe { quick.free_memory() };
self.enter = Self::make_enter(
&mut module,
&mut self.context,
&mut self.builder,
&self.signature,
)?;
let old = std::mem::replace(&mut self.module, module);
unsafe { old.free_memory() };
self.made = 0;
Ok(())
}
}
const RECENT: usize = 1 << 12;
#[derive(Clone, Copy, Debug)]
#[repr(C)]
struct Recent {
key: u64,
block: Block,
}
const EMPTY: Recent = Recent {
key: 0,
block: Block(std::ptr::null()),
};
const CELLS: usize = 1 << 12;
#[derive(Debug)]
pub(crate) struct Blocks {
by_key: BTreeMap<u64, (Block, u64, u64)>,
by_page: BTreeMap<u64, Vec<(u64, u64, u64)>>,
recent: Box<[Cell<Recent>]>,
cells: Vec<Box<[Cell<usize>]>>,
leads_to: Vec<u64>,
linked: BTreeMap<u64, Vec<u32>>,
}
impl Default for Blocks {
fn default() -> Self {
Self {
by_key: BTreeMap::new(),
by_page: BTreeMap::new(),
recent: vec![Cell::new(EMPTY); RECENT].into_boxed_slice(),
cells: Vec::new(),
leads_to: Vec::new(),
linked: BTreeMap::new(),
}
}
}
fn slot(key: u64) -> usize {
let mixed = (key >> 1) ^ (key >> 32);
usize::try_from(mixed & (RECENT as u64 - 1)).expect("below the cache's size")
}
pub(crate) fn emit_find(
builder: &mut FunctionBuilder<'_>,
pointer: ir::Type,
recent: usize,
key: Value,
) -> Value {
let low = builder.ins().ushr_imm_u(key, 1);
let high = builder.ins().ushr_imm_u(key, 32);
let mixed = builder.ins().bxor(low, high);
let slot = builder
.ins()
.band_imm_s(mixed, i64::try_from(RECENT - 1).expect("a mask"));
let offset = builder
.ins()
.imul_imm_u(slot, i64::try_from(size_of::<Recent>()).expect("a size"));
let base = builder
.ins()
.iconst(pointer, i64::try_from(recent).expect("an address"));
let entry = builder.ins().iadd(base, offset);
let flags = MemFlagsData::trusted();
let cached = builder.ins().load(
types::I64,
flags,
entry,
i32::try_from(std::mem::offset_of!(Recent, key)).expect("an offset"),
);
let code = builder.ins().load(
pointer,
flags,
entry,
i32::try_from(std::mem::offset_of!(Recent, block)).expect("an offset"),
);
let hit = builder.ins().icmp(ir::condcodes::IntCC::Equal, cached, key);
let none = builder.ins().iconst(pointer, 0);
builder.ins().select(hit, code, none)
}
impl Blocks {
pub(crate) fn get(&mut self, key: u64) -> Option<Block> {
let slot = slot(key);
let cached = self.recent[slot].get();
if cached.key == key && !cached.block.0.is_null() {
return Some(cached.block);
}
let (block, _, _) = *self.by_key.get(&key)?;
self.recent[slot].set(Recent { key, block });
Some(block)
}
pub(crate) fn recent(&self) -> usize {
self.recent.as_ptr() as usize
}
pub(crate) fn covers(&self, address: u64) -> bool {
self.by_page
.get(&(address >> PAGE_BITS))
.is_some_and(|held| {
held.iter()
.any(|&(_, first, last)| (first..=last).contains(&address))
})
}
pub(crate) fn starting_at(&self, address: u64) -> Vec<(u64, u64)> {
self.by_page
.get(&(address >> PAGE_BITS))
.into_iter()
.flatten()
.filter(|&&(_, first, _)| first == address)
.map(|&(key, _, last)| (key, last))
.collect()
}
pub(crate) fn insert(&mut self, key: u64, first: u64, last: u64, block: Block) {
self.unlist(key);
self.by_key.insert(key, (block, first, last));
self.recent[slot(key)].set(Recent { key, block });
for page in first >> PAGE_BITS..=last >> PAGE_BITS {
self.by_page
.entry(page)
.or_default()
.push((key, first, last));
}
}
pub(crate) fn drop_range(&mut self, start: u64, end: u64) -> Vec<(u64, bool)> {
self.drop_range_but(start, end, &BTreeSet::new())
}
pub(crate) fn rekey_at(
&mut self,
address: u64,
moving: &[u64],
to: impl Fn(u64) -> u64,
) -> Vec<(u64, bool)> {
let mut moved = BTreeSet::new();
for &from in moving {
let Some(&(block, first, last)) = self.by_key.get(&from) else {
continue;
};
self.forget(from);
self.insert(to(from), first, last, block);
moved.insert(to(from));
}
self.drop_range_but(address, address.saturating_add(1), &moved)
}
fn drop_range_but(&mut self, start: u64, end: u64, kept: &BTreeSet<u64>) -> Vec<(u64, bool)> {
if end <= start {
return Vec::new();
}
let mut dropped = BTreeSet::new();
for page in start >> PAGE_BITS..=(end - 1) >> PAGE_BITS {
for &(key, first, last) in self.by_page.get(&page).into_iter().flatten() {
if first < end && start <= last && !kept.contains(&key) {
dropped.insert(key);
}
}
}
let mut pages = BTreeSet::new();
for key in dropped {
pages.extend(self.forget(key).into_iter().flatten());
}
pages
.into_iter()
.map(|page| (page << PAGE_BITS, self.by_page.contains_key(&page)))
.collect()
}
fn forget(&mut self, key: u64) -> Option<RangeInclusive<u64>> {
let pages = self.unlist(key);
self.by_key.remove(&key);
let slot = slot(key);
if self.recent[slot].get().key == key {
self.recent[slot].set(EMPTY);
}
for cell in self.linked.remove(&key).unwrap_or_default() {
self.cell(cell).set(0);
}
pages
}
fn unlist(&mut self, key: u64) -> Option<RangeInclusive<u64>> {
let &(_, first, last) = self.by_key.get(&key)?;
let pages = first >> PAGE_BITS..=last >> PAGE_BITS;
for page in pages.clone() {
if let Some(held) = self.by_page.get_mut(&page) {
held.retain(|&(each, _, _)| each != key);
if held.is_empty() {
self.by_page.remove(&page);
}
}
}
Some(pages)
}
#[cfg(test)]
pub(crate) fn listed_on(&self, address: u64) -> usize {
self.by_page
.get(&(address >> PAGE_BITS))
.map_or(0, Vec::len)
}
pub(crate) fn clear(&mut self) {
self.by_key.clear();
self.by_page.clear();
for slot in &self.recent {
slot.set(EMPTY);
}
self.linked.clear();
for cell in self.cells.iter().flatten() {
cell.set(0);
}
self.leads_to.fill(u64::MAX);
}
pub(crate) fn free_cells(&mut self) {
self.cells.clear();
self.leads_to.clear();
}
pub(crate) fn new_cell(&mut self, key: u64) -> (u32, usize) {
let number = self.leads_to.len();
if number.is_multiple_of(CELLS) {
self.cells
.push(vec![Cell::new(0); CELLS].into_boxed_slice());
}
self.leads_to.push(key);
let number = u32::try_from(number).expect("a cell number");
(number, self.cell(number).as_ptr() as usize)
}
pub(crate) fn new_counter(&mut self) -> usize {
self.new_cell(u64::MAX).1
}
pub(crate) fn replace(&mut self, key: u64, block: Block) {
if let Some(kept) = self.by_key.get_mut(&key) {
kept.0 = block;
}
self.recent[slot(key)].set(Recent { key, block });
for &cell in self.linked.get(&key).into_iter().flatten() {
self.cell(cell).set(block.address());
}
}
pub(crate) fn link(&mut self, number: u32, key: u64, block: Block) {
if self.leads_to.get(number as usize) == Some(&key) && self.by_key.contains_key(&key) {
self.cell(number).set(block.address());
self.linked.entry(key).or_default().push(number);
}
}
pub(crate) fn cell_number(&self, address: usize) -> Option<u32> {
self.cells.iter().enumerate().find_map(|(chunk, cells)| {
let first = cells.as_ptr() as usize;
let offset = address.checked_sub(first)? / size_of::<usize>();
(offset < CELLS).then(|| u32::try_from(chunk * CELLS + offset).expect("a cell number"))
})
}
fn cell(&self, number: u32) -> &Cell<usize> {
let number = number as usize;
&self.cells[number / CELLS][number % CELLS]
}
pub(crate) fn len(&self) -> usize {
self.by_key.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dropped_block_leaves_every_page_it_came_from() {
let mut blocks = Blocks::default();
let block = Block(std::ptr::null());
let (first, last) = (PAGE_SIZE - 4, PAGE_SIZE + 3);
blocks.insert(7, first, last, block);
blocks.insert(7, first, last, block);
assert_eq!((blocks.listed_on(first), blocks.listed_on(last)), (1, 1));
assert_eq!(
blocks.drop_range(first, first + 1),
[(0, false), (PAGE_SIZE, false)]
);
assert_eq!((blocks.listed_on(first), blocks.listed_on(last)), (0, 0));
assert!(!blocks.covers(last));
}
fn run(jit: &Jit, core: &mut [*const usize; 1], block: Block) -> u32 {
let entry = jit.entry();
unsafe { entry.run(core.as_mut_ptr().cast(), block) }
}
#[test]
fn a_block_reads_a_mapped_page_inline_and_takes_the_slow_branch_elsewhere() {
#[repr(C, align(4096))]
struct Page([u8; 4096]);
let mut page = Box::new(Page([0; 4096]));
page.0[8..12].copy_from_slice(&0xDEAD_BEEF_u32.to_le_bytes());
let mut pages = Pages::new(DataModel::Ilp32).unwrap();
unsafe { pages.map(0x2000_0000, PAGE_SIZE, page.0.as_mut_ptr(), true) };
let mut jit = Jit::new().unwrap();
let block = |jit: &mut Jit, address: u32| {
jit.compile(Tier::Best, |builder, core, _| {
let table = builder
.ins()
.load(types::I64, MemFlagsData::trusted(), core, 0);
let address = builder.ins().iconst(types::I32, i64::from(address));
let slow = builder.create_block();
let host = emit_lookup(builder, table, address, Lookup::Read(4), slow);
let value = builder.ins().load(types::I32, guest_memory(), host, 0);
builder.ins().return_(&[value]);
builder.switch_to_block(slow);
let missed = builder.ins().iconst(types::I32, 7);
builder.ins().return_(&[missed]);
})
.unwrap()
};
let mut core = [pages.table()];
let read = block(&mut jit, 0x2000_0008);
assert_eq!(run(&jit, &mut core, read), 0xDEAD_BEEF);
let unmapped = block(&mut jit, 0x3000_0000);
assert_eq!(run(&jit, &mut core, unmapped), 7);
let across = block(
&mut jit,
u32::try_from(0x2000_0000 + PAGE_SIZE - 2).unwrap(),
);
let answer = run(&jit, &mut core, across);
assert_eq!(answer, 7, "across a page");
pages.route(0x2000_0000, true);
assert_eq!(run(&jit, &mut core, read), 7, "routed");
}
#[test]
fn the_table_holds_the_32_bit_space_and_nothing_above_it() {
assert!(matches!(
Pages::new(DataModel::Lp64),
Err(Error::Machine(_))
));
assert!(Pages::holds(0, ADDRESSES));
assert!(Pages::holds(ADDRESSES - PAGE_SIZE, PAGE_SIZE));
assert!(!Pages::holds(ADDRESSES - PAGE_SIZE, 2 * PAGE_SIZE));
assert!(!Pages::holds(ADDRESSES, PAGE_SIZE));
assert!(!Pages::holds(u64::MAX, 2));
}
#[test]
fn a_big_endian_load_and_store_keep_the_most_significant_byte_first() {
#[repr(C, align(4096))]
struct Page([u8; 4096]);
let mut page = Box::new(Page([0; 4096]));
page.0[8..12].copy_from_slice(&0xDEAD_BEEF_u32.to_be_bytes());
let mut pages = Pages::new(DataModel::Ilp32).unwrap();
unsafe { pages.map(0x2000_0000, PAGE_SIZE, page.0.as_mut_ptr(), true) };
let mut jit = Jit::new().unwrap();
let block = jit
.compile(Tier::Best, |builder, core, _| {
let table = builder
.ins()
.load(types::I64, MemFlagsData::trusted(), core, 0);
let slow = builder.create_block();
let from = builder.ins().iconst(types::I32, 0x2000_0008);
let host = emit_lookup(builder, table, from, Lookup::Read(4), slow);
let value = load_guest(builder, types::I32, host, Endian::Big);
let to = builder.ins().iconst(types::I32, 0x2000_0010);
let host = emit_lookup(builder, table, to, Lookup::Write(4), slow);
store_guest(builder, value, host, Endian::Big);
builder.ins().return_(&[value]);
builder.switch_to_block(slow);
let missed = builder.ins().iconst(types::I32, 7);
builder.ins().return_(&[missed]);
})
.unwrap();
let mut core = [pages.table()];
assert_eq!(run(&jit, &mut core, block), 0xDEAD_BEEF);
assert_eq!(page.0[16..20], [0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn a_block_stores_to_a_writable_page_inline() {
#[repr(C, align(4096))]
struct Page([u8; 4096]);
let mut page = Box::new(Page([0; 4096]));
let mut pages = Pages::new(DataModel::Ilp32).unwrap();
unsafe { pages.map(0x2000_0000, PAGE_SIZE, page.0.as_mut_ptr(), true) };
let mut jit = Jit::new().unwrap();
let store = jit
.compile(Tier::Best, |builder, core, _| {
let table = builder
.ins()
.load(types::I64, MemFlagsData::trusted(), core, 0);
let address = builder.ins().iconst(types::I32, 0x2000_0008);
let slow = builder.create_block();
let host = emit_lookup(builder, table, address, Lookup::Write(4), slow);
let value = builder.ins().iconst(types::I32, 0x1234);
builder.ins().store(guest_memory(), value, host, 0);
let stored = builder.ins().iconst(types::I32, 0);
builder.ins().return_(&[stored]);
builder.switch_to_block(slow);
let missed = builder.ins().iconst(types::I32, 7);
builder.ins().return_(&[missed]);
})
.unwrap();
let mut core = [pages.table()];
assert_eq!(run(&jit, &mut core, store), 0);
assert_eq!(page.0[8..12], 0x1234_u32.to_le_bytes());
pages.hold_code(0x2000_0000);
assert_eq!(run(&jit, &mut core, store), 7, "code");
}
#[test]
fn a_page_of_code_takes_no_inline_store_until_its_code_is_dropped() {
#[repr(C, align(4096))]
struct Page([u8; 4096]);
let mut page = Box::new(Page([0; 4096]));
let mut pages = Pages::new(DataModel::Ilp32).unwrap();
unsafe { pages.map(0, PAGE_SIZE, page.0.as_mut_ptr(), true) };
let entry = |pages: &Pages| pages.entries[0].get() & (READ | WRITE);
assert_eq!(entry(&pages), READ | WRITE);
pages.hold_code(0x10);
assert_eq!(entry(&pages), READ);
pages.route(0, true);
assert_eq!(entry(&pages), 0);
pages.release_all_code();
assert_eq!(entry(&pages), 0, "still routed");
pages.route(0, false);
assert_eq!(entry(&pages), READ | WRITE);
}
#[test]
fn code_through_one_view_takes_the_inline_store_from_every_view() {
#[repr(C, align(4096))]
struct Page([u8; 4096]);
const VIEW: u64 = 0x0800_0000;
let mut page = Box::new(Page([0; 4096]));
let mut pages = Pages::new(DataModel::Ilp32).unwrap();
unsafe { pages.map(0, 2 * PAGE_SIZE, page.0.as_mut_ptr(), true) };
unsafe { pages.map(VIEW, 2 * PAGE_SIZE, page.0.as_mut_ptr(), true) };
pages.hold_code(VIEW + 0x10);
assert_eq!(pages.inline(0), Inline::Reads);
assert_eq!(pages.inline(VIEW), Inline::Reads);
assert_eq!(pages.inline(PAGE_SIZE), Inline::Both, "the next page");
assert_eq!(
pages.views(PAGE_SIZE - 2, PAGE_SIZE + 2),
[
(PAGE_SIZE - 2, PAGE_SIZE),
(VIEW + PAGE_SIZE - 2, VIEW + PAGE_SIZE),
(PAGE_SIZE, PAGE_SIZE + 2),
(VIEW + PAGE_SIZE, VIEW + PAGE_SIZE + 2),
]
);
assert_eq!(
pages.views(0x3000_0000, 0x3000_0004),
[(0x3000_0000, 0x3000_0004)]
);
pages.release_code(VIEW);
assert_eq!(pages.inline(0), Inline::Both);
}
#[test]
fn a_checked_page_is_inline_only_as_far_as_it_was_let_be() {
#[repr(C, align(4096))]
struct Page([u8; 4096]);
let mut page = Box::new(Page([0; 4096]));
let mut pages = Pages::new(DataModel::Ilp32).unwrap();
unsafe { pages.map(0, PAGE_SIZE, page.0.as_mut_ptr(), true) };
pages.check_all(true);
assert_eq!(pages.inline(0), Inline::Neither);
assert!(!pages.decided(0));
pages.allow(0, Inline::Reads);
assert!(pages.decided(0));
assert_eq!(pages.inline(0), Inline::Reads);
pages.allow(0, Inline::Both);
assert_eq!(pages.inline(0), Inline::Both);
pages.check_all(true);
assert_eq!(pages.inline(0), Inline::Neither, "forgotten");
assert!(!pages.decided(0));
pages.check_all(false);
assert_eq!(pages.inline(0), Inline::Both);
}
}