#![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, TraceLogEntry},
};
use std::{collections::BTreeMap, fmt::Debug};
#[cfg(not(feature = "shuttle-test"))]
use {
rand::{thread_rng, Rng},
std::sync::Arc,
};
#[cfg(feature = "shuttle-test")]
use shuttle::{
rand::{thread_rng, Rng},
sync::Arc,
};
const PROGRAM_ENVIRONMENT_KEY_SHIFT: u32 = 4;
static RUNTIME_ENVIRONMENT_KEY: std::sync::OnceLock<i32> = std::sync::OnceLock::<i32>::new();
pub fn get_runtime_environment_key() -> i32 {
*RUNTIME_ENVIRONMENT_KEY
.get_or_init(|| thread_rng().gen::<i32>() >> PROGRAM_ENVIRONMENT_KEY_SHIFT)
}
#[derive(Debug, Clone, Copy, 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_instruction_tracing: bool,
pub enable_symbol_and_section_labels: bool,
pub reject_broken_elfs: bool,
pub noop_instruction_rate: u32,
pub sanitize_user_provided_values: bool,
pub external_internal_function_hash_collision: bool,
pub reject_callx_r10: bool,
pub optimize_rodata: bool,
pub aligned_memory_mapping: bool,
pub enable_sbpf_v1: bool,
pub enable_sbpf_v2: bool,
}
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: 4_096,
enable_address_translation: true,
enable_stack_frame_gaps: true,
instruction_meter_checkpoint_distance: 10000,
enable_instruction_meter: true,
enable_instruction_tracing: false,
enable_symbol_and_section_labels: false,
reject_broken_elfs: false,
noop_instruction_rate: 256,
sanitize_user_provided_values: true,
external_internal_function_hash_collision: true,
reject_callx_r10: true,
optimize_rodata: true,
aligned_memory_mapping: true,
enable_sbpf_v1: true,
enable_sbpf_v2: true,
}
}
}
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 trace(&mut self, state: [u64; 12]);
fn consume(&mut self, amount: u64);
fn get_remaining(&self) -> u64;
}
#[derive(Debug, Clone, Default)]
pub struct TestContextObject {
pub trace_log: Vec<TraceLogEntry>,
pub remaining: u64,
}
impl ContextObject for TestContextObject {
fn trace(&mut self, state: [u64; 12]) {
self.trace_log.push(state);
}
fn consume(&mut self, amount: u64) {
self.remaining = self.remaining.saturating_sub(amount);
}
fn get_remaining(&self) -> u64 {
self.remaining
}
}
impl TestContextObject {
pub fn new(remaining: u64) -> Self {
Self {
trace_log: Vec::new(),
remaining,
}
}
pub fn compare_trace_log(interpreter: &Self, jit: &Self) -> bool {
let interpreter = interpreter.trace_log.as_slice();
let mut jit = jit.trace_log.as_slice();
if jit.len() > interpreter.len() {
jit = &jit[0..interpreter.len()];
}
interpreter == jit
}
}
pub struct DynamicAnalysis {
pub edge_counter_max: usize,
pub edges: BTreeMap<usize, BTreeMap<usize, usize>>,
}
impl DynamicAnalysis {
pub fn new(trace_log: &[[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 trace_log.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,
}
#[repr(C)]
pub struct EbpfVm<'a, C: ContextObject> {
pub host_stack_pointer: *mut u64,
pub call_depth: u64,
pub stack_pointer: u64,
pub context_object_pointer: &'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 memory_mapping: MemoryMapping<'a>,
pub call_frames: Vec<CallFrame>,
pub loader: Arc<BuiltinProgram<C>>,
#[cfg(feature = "debugger")]
pub debug_port: Option<u16>,
}
impl<'a, C: ContextObject> EbpfVm<'a, C> {
pub fn new(
loader: Arc<BuiltinProgram<C>>,
sbpf_version: &SBPFVersion,
context_object: &'a mut C,
mut memory_mapping: MemoryMapping<'a>,
stack_len: usize,
) -> Self {
let config = loader.get_config();
let stack_pointer =
ebpf::MM_STACK_START.saturating_add(if sbpf_version.dynamic_stack_frames() {
stack_len
} else {
config.stack_frame_size
} as u64);
if !config.enable_address_translation {
memory_mapping = MemoryMapping::new_identity();
}
EbpfVm {
host_stack_pointer: std::ptr::null_mut(),
call_depth: 0,
stack_pointer,
context_object_pointer: context_object,
previous_instruction_meter: 0,
due_insn_count: 0,
stopwatch_numerator: 0,
stopwatch_denominator: 0,
registers: [0u64; 12],
program_result: ProgramResult::Ok(0),
memory_mapping,
call_frames: vec![CallFrame::default(); config.max_call_depth],
loader,
#[cfg(feature = "debugger")]
debug_port: None,
}
}
pub fn execute_program(
&mut self,
executable: &Executable<C>,
interpreted: bool,
) -> (u64, ProgramResult) {
debug_assert!(Arc::ptr_eq(&self.loader, executable.get_loader()));
self.registers[1] = ebpf::MM_INPUT_START;
self.registers[ebpf::FRAME_PTR_REG] = self.stack_pointer;
self.registers[11] = executable.get_entrypoint_instruction_offset() as u64;
let config = executable.get_config();
let initial_insn_count = if config.enable_instruction_meter {
self.context_object_pointer.get_remaining()
} else {
0
};
self.previous_instruction_meter = initial_insn_count;
self.due_insn_count = 0;
self.program_result = ProgramResult::Ok(0);
if interpreted {
#[cfg(feature = "debugger")]
let debug_port = self.debug_port.clone();
let mut interpreter = Interpreter::new(self, executable, self.registers);
#[cfg(feature = "debugger")]
if let Some(debug_port) = debug_port {
crate::debugger::execute(&mut interpreter, debug_port);
} else {
while interpreter.step() {}
}
#[cfg(not(feature = "debugger"))]
while interpreter.step() {}
} else {
#[cfg(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64"))]
{
let compiled_program = match executable
.get_compiled_program()
.ok_or_else(|| EbpfError::JitNotCompiled)
{
Ok(compiled_program) => compiled_program,
Err(error) => return (0, ProgramResult::Err(error)),
};
compiled_program.invoke(config, self, self.registers);
}
#[cfg(not(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64")))]
{
return (0, ProgramResult::Err(EbpfError::JitNotCompiled));
}
};
let instruction_count = if config.enable_instruction_meter {
self.context_object_pointer.consume(self.due_insn_count);
initial_insn_count.saturating_sub(self.context_object_pointer.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(
unsafe {
std::ptr::addr_of_mut!(*self)
.cast::<u64>()
.offset(get_runtime_environment_key() as isize)
.cast::<Self>()
},
self.registers[1],
self.registers[2],
self.registers[3],
self.registers[4],
self.registers[5],
);
}
}