#![allow(clippy::arithmetic_side_effects)]
use crate::{
ebpf,
elf::Executable,
error::{EbpfError, ProgramResult},
interpreter::Interpreter,
memory_region::MemoryMapping,
program::{BuiltinFunction, BuiltinProgram, FunctionRegistry, SBPFVersion},
static_analysis::{Analysis, DummyContextObject, RegisterTraceEntry},
};
pub use defaults::get_stack_frame_size;
use std::{collections::BTreeMap, fmt::Debug, marker::PhantomData, mem::offset_of, ptr};
#[cfg(feature = "shuttle-test")]
use shuttle::sync::Arc;
#[cfg(not(feature = "shuttle-test"))]
use std::sync::Arc;
#[cfg(all(feature = "jit", not(feature = "shuttle-test")))]
use rand::{thread_rng, Rng};
#[cfg(all(feature = "jit", feature = "shuttle-test"))]
use shuttle::rand::{thread_rng, Rng};
#[cfg(feature = "jit")]
pub fn get_runtime_environment_key() -> i32 {
static RUNTIME_ENVIRONMENT_KEY: std::sync::OnceLock<i32> = std::sync::OnceLock::new();
*RUNTIME_ENVIRONMENT_KEY.get_or_init(|| thread_rng().gen::<i32>() >> 1)
}
#[cfg(not(feature = "jit"))]
pub fn get_runtime_environment_key() -> i32 {
0
}
pub(crate) mod defaults {
const DEFAULT_STACK_FRAME_SIZE: usize = 4_096;
#[cfg(feature = "conf-stack-frame-size")]
#[inline(always)]
pub fn get_stack_frame_size() -> usize {
static STACK_FRAME_SIZE_CACHE: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*STACK_FRAME_SIZE_CACHE.get_or_init(|| {
let size = std::env::var("VM_STACK_FRAME_SIZE")
.ok()
.and_then(|v| {
v.parse::<usize>().ok().filter(|sfz| *sfz > 0).or_else(|| {
log::warn!(
"Invalid VM_STACK_FRAME_SIZE={}, falling back to {}.",
v,
DEFAULT_STACK_FRAME_SIZE
);
None
})
})
.unwrap_or(DEFAULT_STACK_FRAME_SIZE);
if size != DEFAULT_STACK_FRAME_SIZE {
log::warn!(
"VM_STACK_FRAME_SIZE is set to {} (default: {}).",
size,
DEFAULT_STACK_FRAME_SIZE
);
}
size
})
}
#[cfg(not(feature = "conf-stack-frame-size"))]
pub const fn get_stack_frame_size() -> usize {
DEFAULT_STACK_FRAME_SIZE
}
}
pub enum ExecutionMode {
Interpreted,
Jit,
PreferJit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub max_call_depth: usize,
pub stack_frame_size: usize,
pub enable_address_translation: bool,
pub enable_stack_frame_gaps: bool,
pub instruction_meter_checkpoint_distance: usize,
pub enable_instruction_meter: bool,
pub enable_register_tracing: bool,
pub enable_symbol_and_section_labels: bool,
pub reject_broken_elfs: bool,
#[cfg(feature = "jit")]
pub noop_instruction_rate: u32,
#[cfg(feature = "jit")]
pub sanitize_user_provided_values: bool,
pub optimize_rodata: bool,
pub allow_memory_region_zero: bool,
pub aligned_memory_mapping: bool,
pub enabled_sbpf_versions: std::ops::RangeInclusive<SBPFVersion>,
}
impl Config {
pub fn stack_size(&self) -> usize {
self.stack_frame_size * self.max_call_depth
}
}
impl Default for Config {
fn default() -> Self {
Self {
max_call_depth: 64,
stack_frame_size: defaults::get_stack_frame_size(),
enable_address_translation: true,
enable_stack_frame_gaps: true,
instruction_meter_checkpoint_distance: 10000,
enable_instruction_meter: true,
enable_register_tracing: false,
enable_symbol_and_section_labels: false,
reject_broken_elfs: false,
#[cfg(feature = "jit")]
noop_instruction_rate: 256,
#[cfg(feature = "jit")]
sanitize_user_provided_values: true,
optimize_rodata: true,
allow_memory_region_zero: true,
aligned_memory_mapping: false,
enabled_sbpf_versions: SBPFVersion::V0..=SBPFVersion::V4,
}
}
}
impl<C: ContextObject> Executable<C> {
pub fn from_elf(elf_bytes: &[u8], loader: Arc<BuiltinProgram<C>>) -> Result<Self, EbpfError> {
let executable = Executable::load(elf_bytes, loader)?;
Ok(executable)
}
pub fn from_text_bytes(
text_bytes: &[u8],
loader: Arc<BuiltinProgram<C>>,
sbpf_version: SBPFVersion,
function_registry: FunctionRegistry<usize>,
) -> Result<Self, EbpfError> {
Executable::new_from_text_bytes(text_bytes, loader, sbpf_version, function_registry)
.map_err(EbpfError::ElfError)
}
}
pub trait ContextObject {
fn consume(&mut self, amount: u64);
fn get_remaining(&self) -> u64;
fn active_mapping_ptr(&mut self) -> ptr::NonNull<MemoryMapping>;
}
pub struct DynamicAnalysis {
pub edge_counter_max: usize,
pub edges: BTreeMap<usize, BTreeMap<usize, usize>>,
}
impl DynamicAnalysis {
pub fn new(register_trace: &[[u64; 12]], analysis: &Analysis) -> Self {
let mut result = Self {
edge_counter_max: 0,
edges: BTreeMap::new(),
};
let mut last_basic_block = usize::MAX;
for traced_instruction in register_trace.iter() {
let pc = traced_instruction[11] as usize;
if analysis.cfg_nodes.contains_key(&pc) {
let counter = result
.edges
.entry(last_basic_block)
.or_default()
.entry(pc)
.or_insert(0);
*counter += 1;
result.edge_counter_max = result.edge_counter_max.max(*counter);
last_basic_block = pc;
}
}
result
}
}
#[derive(Clone, Default)]
pub struct CallFrame {
pub caller_saved_registers: [u64; ebpf::SCRATCH_REGS],
pub frame_pointer: u64,
pub target_pc: u64,
}
pub enum RuntimeEnvironmentSlot {
HostStackPointer = offset_of!(EbpfVm<DummyContextObject>, host_stack_pointer) as isize,
CallDepth = offset_of!(EbpfVm<DummyContextObject>, call_depth) as isize,
ContextObjectPointer = offset_of!(EbpfVm<DummyContextObject>, context_object_pointer) as isize,
PreviousInstructionMeter =
offset_of!(EbpfVm<DummyContextObject>, previous_instruction_meter) as isize,
DueInsnCount = offset_of!(EbpfVm<DummyContextObject>, due_insn_count) as isize,
StopwatchNumerator = offset_of!(EbpfVm<DummyContextObject>, stopwatch_numerator) as isize,
StopwatchDenominator = offset_of!(EbpfVm<DummyContextObject>, stopwatch_denominator) as isize,
Registers = offset_of!(EbpfVm<DummyContextObject>, registers) as isize,
ProgramResult = offset_of!(EbpfVm<DummyContextObject>, program_result) as isize,
MemoryMapping = offset_of!(EbpfVm<DummyContextObject>, memory_mapping) as isize,
RegisterTrace = offset_of!(EbpfVm<DummyContextObject>, register_trace) as isize,
}
#[repr(C)]
pub struct EbpfVm<'a, C: ContextObject> {
pub host_stack_pointer: *mut u64,
pub call_depth: u64,
pub(crate) context_object_pointer: ptr::NonNull<C>,
context_object_lifetime: PhantomData<&'a mut C>,
pub previous_instruction_meter: u64,
pub due_insn_count: u64,
pub stopwatch_numerator: u64,
pub stopwatch_denominator: u64,
pub registers: [u64; 12],
pub program_result: ProgramResult,
pub(crate) memory_mapping: ptr::NonNull<MemoryMapping>,
pub loader: Arc<BuiltinProgram<C>>,
pub register_trace: Vec<RegisterTraceEntry>,
#[cfg(feature = "debugger")]
pub debug_port: Option<u16>,
#[cfg(feature = "debugger")]
pub debug_metadata: Option<String>,
}
impl<'a, C: ContextObject> EbpfVm<'a, C> {
pub fn new(
loader: Arc<BuiltinProgram<C>>,
sbpf_version: SBPFVersion,
context_object: &'a mut C,
stack_len: usize,
) -> Self {
let config = loader.get_config();
let mut registers = [0u64; 12];
registers[ebpf::FRAME_PTR_REG] =
ebpf::MM_STACK_START.saturating_add(if !sbpf_version.manual_stack_frame_bump() {
config.stack_frame_size
} else {
stack_len
} as u64);
let memory_mapping = context_object.active_mapping_ptr();
EbpfVm {
host_stack_pointer: std::ptr::null_mut(),
call_depth: 0,
context_object_pointer: ptr::NonNull::from_mut(context_object),
context_object_lifetime: PhantomData,
previous_instruction_meter: 0,
due_insn_count: 0,
stopwatch_numerator: 0,
stopwatch_denominator: 0,
registers,
program_result: ProgramResult::Ok(0),
memory_mapping,
loader,
#[cfg(feature = "debugger")]
debug_port: std::env::var("VM_DEBUG_PORT")
.ok()
.and_then(|v| v.parse::<u16>().ok()),
#[cfg(feature = "debugger")]
debug_metadata: None,
register_trace: Vec::default(),
}
}
pub fn execute_program(
&mut self,
executable: &Executable<C>,
mode: &mut ExecutionMode,
call_frames: &mut [CallFrame],
) -> (u64, ProgramResult) {
debug_assert!(Arc::ptr_eq(&self.loader, executable.get_loader()));
self.registers[11] = executable.get_entrypoint_instruction_offset() as u64;
let config = executable.get_config();
let initial_insn_count = self.context().get_remaining();
self.previous_instruction_meter = initial_insn_count;
self.due_insn_count = 0;
self.program_result = ProgramResult::Ok(0);
'execute: {
match *mode {
ExecutionMode::Interpreted => {}
#[cfg(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64"))]
ExecutionMode::PreferJit => {
if let Some(compiled_program) = executable.get_compiled_program() {
*mode = ExecutionMode::Jit;
break 'execute compiled_program.invoke(config, self, self.registers);
}
}
#[cfg(not(all(
feature = "jit",
not(target_os = "windows"),
target_arch = "x86_64"
)))]
ExecutionMode::PreferJit => {}
#[cfg(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64"))]
ExecutionMode::Jit => {
let Some(compiled_program) = executable.get_compiled_program() else {
return (0, ProgramResult::Err(EbpfError::JitNotCompiled));
};
*mode = ExecutionMode::Jit;
break 'execute compiled_program.invoke(config, self, self.registers);
}
#[cfg(not(all(
feature = "jit",
not(target_os = "windows"),
target_arch = "x86_64"
)))]
ExecutionMode::Jit => return (0, ProgramResult::Err(EbpfError::JitNotCompiled)),
}
*mode = ExecutionMode::Interpreted;
let interpreter = Interpreter::new(self, executable, self.registers, call_frames);
break 'execute run_interpreter(interpreter);
}
let instruction_count = if config.enable_instruction_meter {
let due_insn_count = self.due_insn_count;
let context = self.context();
context.consume(due_insn_count);
initial_insn_count.saturating_sub(context.get_remaining())
} else {
0
};
let mut result = ProgramResult::Ok(0);
std::mem::swap(&mut result, &mut self.program_result);
(instruction_count, result)
}
pub fn invoke_function(&mut self, function: BuiltinFunction<C>) {
function(
self.encrypted_host_address(),
self.registers[1],
self.registers[2],
self.registers[3],
self.registers[4],
self.registers[5],
);
}
pub(crate) fn encrypted_host_address(&mut self) -> EncryptedHostAddressToEbpfVm<C> {
let addr = (&raw mut *self).expose_provenance() as isize;
EncryptedHostAddressToEbpfVm(
addr.wrapping_add(get_runtime_environment_key() as isize) as usize as u64,
PhantomData,
)
}
pub fn context(&mut self) -> &mut C {
unsafe { self.context_object_pointer.as_mut() }
}
pub(crate) fn memory(&mut self) -> &mut MemoryMapping {
unsafe { self.memory_mapping.as_mut() }
}
}
#[repr(transparent)]
pub struct EncryptedHostAddressToEbpfVm<C>(
)
pub(crate) u64,
PhantomData<C>,
);
impl<C: ContextObject> EncryptedHostAddressToEbpfVm<C> {
pub unsafe fn with_vm<R>(&mut self, cb: impl FnOnce(&mut EbpfVm<'_, C>) -> R) -> R {
let addr = (self.0 as usize as isize)
.wrapping_sub(crate::vm::get_runtime_environment_key() as isize);
let vm = unsafe {
std::ptr::with_exposed_provenance_mut::<crate::vm::EbpfVm<C>>(addr as usize)
.as_mut()
.unwrap()
};
cb(vm)
}
}
#[cold]
#[inline(never)]
#[cfg(feature = "debugger")]
fn run_interpreter<C: ContextObject>(mut interpreter: Interpreter<C>) {
let debug_port = interpreter.vm.debug_port.clone();
if let Some(debug_port) = debug_port {
crate::debugger::execute(&mut interpreter, debug_port);
} else {
while interpreter.step() {}
}
}
#[cold]
#[inline(never)]
#[cfg(not(feature = "debugger"))]
fn run_interpreter<C: ContextObject>(mut interpreter: Interpreter<C>) {
while interpreter.step() {}
}
#[cfg(test)]
mod tests {
use crate::{
memory_region::MemoryMapping,
program::{BuiltinProgram, SBPFVersion},
vm::{Config, ContextObject, RuntimeEnvironmentSlot},
};
use std::{ptr::NonNull, sync::Arc};
#[test]
fn test_runtime_environment_slots() {
struct DummyContextObject(MemoryMapping);
impl ContextObject for DummyContextObject {
fn consume(&mut self, _: u64) {
todo!()
}
fn get_remaining(&self) -> u64 {
todo!()
}
fn active_mapping_ptr(&mut self) -> NonNull<MemoryMapping> {
NonNull::from_mut(&mut self.0)
}
}
let version = SBPFVersion::V4;
let config = Config::default();
let mut context_object =
unsafe { DummyContextObject(MemoryMapping::new(vec![], &config, version).unwrap()) };
let env = super::EbpfVm::new(
Arc::new(BuiltinProgram::new_mock()),
version,
&mut context_object,
4096,
);
macro_rules! check_slot {
($env:expr, $entry:ident, $slot:ident) => {
assert_eq!(
unsafe {
std::ptr::addr_of!($env.$entry)
.cast::<u8>()
.offset_from(std::ptr::addr_of!($env).cast::<u8>()) as usize
},
RuntimeEnvironmentSlot::$slot as usize,
);
};
}
check_slot!(env, host_stack_pointer, HostStackPointer);
check_slot!(env, call_depth, CallDepth);
check_slot!(env, context_object_pointer, ContextObjectPointer);
check_slot!(env, previous_instruction_meter, PreviousInstructionMeter);
check_slot!(env, due_insn_count, DueInsnCount);
check_slot!(env, stopwatch_numerator, StopwatchNumerator);
check_slot!(env, stopwatch_denominator, StopwatchDenominator);
check_slot!(env, registers, Registers);
check_slot!(env, program_result, ProgramResult);
check_slot!(env, memory_mapping, MemoryMapping);
check_slot!(env, register_trace, RegisterTrace);
}
}