use crate::parsed_wasm::ParsedWasm;
use std::sync::{Arc, Mutex};
use wasmer::wasmparser::{BlockType, Operator};
use wasmer::{
ExportIndex, FunctionMiddleware, GlobalInit, GlobalType, LocalFunctionIndex, MiddlewareError,
MiddlewareReaderState, ModuleMiddleware, Mutability, Type,
};
use wasmer_types::{GlobalIndex, ModuleInfo};
const CHARGED_LOCALS_THRESHOLD: usize = 30;
#[derive(Debug, Clone)]
struct MeteringGlobalIndexes(GlobalIndex, GlobalIndex);
impl MeteringGlobalIndexes {
fn remaining_points(&self) -> GlobalIndex {
self.0
}
fn points_exhausted(&self) -> GlobalIndex {
self.1
}
}
pub struct Metering<F: Fn(&Operator) -> u64 + Send + Sync> {
initial_limit: u64,
cost_function: Arc<F>,
global_indexes: Mutex<Option<MeteringGlobalIndexes>>,
function_locals: Vec<usize>,
}
impl<F: Fn(&Operator) -> u64 + Send + Sync> std::fmt::Debug for Metering<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Metering")
.field("initial_limit", &self.initial_limit)
.field("cost_function", &"<cost_function>")
.field("global_indexes", &self.global_indexes)
.field("function_locals", &self.function_locals)
.finish()
}
}
impl<F: Fn(&Operator) -> u64 + Send + Sync> Metering<F> {
pub fn new(initial_limit: u64, cost_function: F, parsed_wasm: Option<ParsedWasm>) -> Self {
Self {
initial_limit,
cost_function: Arc::new(cost_function),
global_indexes: Mutex::new(None),
function_locals: parsed_wasm.map_or_else(Vec::new, |inner| inner.func_locals),
}
}
}
impl<F: Fn(&Operator) -> u64 + Send + Sync + 'static> ModuleMiddleware for Metering<F> {
fn generate_function_middleware(&self, idx: LocalFunctionIndex) -> Box<dyn FunctionMiddleware> {
let locals_count = self
.function_locals
.get(idx.as_u32() as usize)
.copied()
.unwrap_or_default();
Box::new(FunctionMetering {
is_first_operator: true,
charged_locals_count: locals_count.saturating_sub(CHARGED_LOCALS_THRESHOLD - 1) as u64,
cost_function: self.cost_function.clone(),
global_indexes: self.global_indexes.lock().unwrap().clone().unwrap(),
accumulated_cost: 0,
})
}
fn transform_module_info(&self, module_info: &mut ModuleInfo) -> Result<(), MiddlewareError> {
let mut global_indexes = self.global_indexes.lock().unwrap();
if global_indexes.is_some() {
panic!("Metering::transform_module_info: Attempting to use a `Metering` middleware from multiple modules.");
}
let remaining_points_global_index = module_info
.globals
.push(GlobalType::new(Type::I64, Mutability::Var));
module_info
.global_initializers
.push(GlobalInit::I64Const(self.initial_limit as i64));
module_info.exports.insert(
"wasmer_metering_remaining_points".to_string(),
ExportIndex::Global(remaining_points_global_index),
);
let points_exhausted_global_index = module_info
.globals
.push(GlobalType::new(Type::I32, Mutability::Var));
module_info
.global_initializers
.push(GlobalInit::I32Const(0));
module_info.exports.insert(
"wasmer_metering_points_exhausted".to_string(),
ExportIndex::Global(points_exhausted_global_index),
);
*global_indexes = Some(MeteringGlobalIndexes(
remaining_points_global_index,
points_exhausted_global_index,
));
Ok(())
}
}
pub struct FunctionMetering<F: Fn(&Operator) -> u64 + Send + Sync> {
is_first_operator: bool,
cost_function: Arc<F>,
global_indexes: MeteringGlobalIndexes,
accumulated_cost: u64,
charged_locals_count: u64,
}
impl<F: Fn(&Operator) -> u64 + Send + Sync> std::fmt::Debug for FunctionMetering<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FunctionMetering")
.field("is_first_operator", &self.is_first_operator)
.field("cost_function", &"<cost_function>")
.field("global_indexes", &self.global_indexes)
.field("accumulated_cost", &self.accumulated_cost)
.field("charged_locals_count", &self.charged_locals_count)
.finish()
}
}
impl<F: Fn(&Operator) -> u64 + Send + Sync> FunctionMiddleware for FunctionMetering<F> {
fn feed<'a>(
&mut self,
operator: Operator<'a>,
state: &mut MiddlewareReaderState<'a>,
) -> Result<(), MiddlewareError> {
if self.is_first_operator && self.charged_locals_count > 0 {
let locals_cost =
(self.cost_function)(&Operator::Nop).saturating_mul(self.charged_locals_count);
if is_accounting(&operator) {
self.accumulated_cost += locals_cost;
} else {
state.extend(gas_check_wasm_code(&self.global_indexes, locals_cost));
}
}
self.accumulated_cost += (self.cost_function)(&operator);
if is_accounting(&operator) && self.accumulated_cost > 0 {
state.extend(gas_check_wasm_code(
&self.global_indexes,
self.accumulated_cost,
));
self.accumulated_cost = 0;
}
state.push_operator(operator);
self.is_first_operator = false;
Ok(())
}
}
pub fn is_accounting(operator: &Operator) -> bool {
matches!(
operator,
Operator::Loop { .. } | Operator::End | Operator::If { .. } | Operator::Else | Operator::Br { .. } | Operator::BrTable { .. } | Operator::BrIf { .. } | Operator::Call { .. } | Operator::CallIndirect { .. } | Operator::Return | Operator::Throw { .. } | Operator::ThrowRef | Operator::Rethrow { .. } | Operator::Delegate { .. } | Operator::Catch { .. } | Operator::ReturnCall { .. } | Operator::ReturnCallIndirect { .. } | Operator::BrOnCast { .. } | Operator::BrOnCastFail { .. } | Operator::CallRef { .. } | Operator::ReturnCallRef { .. } | Operator::BrOnNull { .. } | Operator::BrOnNonNull { .. } )
}
fn gas_check_wasm_code<'a>(
global_indexes: &MeteringGlobalIndexes,
cost: u64,
) -> [Operator<'a>; 12] {
let idx_remaining_points = global_indexes.remaining_points().as_u32();
let idx_points_exhausted = global_indexes.points_exhausted().as_u32();
[
Operator::GlobalGet {
global_index: idx_remaining_points,
},
Operator::I64Const { value: cost as i64 },
Operator::I64LtU,
Operator::If {
blockty: BlockType::Empty,
},
Operator::I32Const { value: 1 },
Operator::GlobalSet {
global_index: idx_points_exhausted,
},
Operator::Unreachable,
Operator::End,
Operator::GlobalGet {
global_index: idx_remaining_points,
},
Operator::I64Const { value: cost as i64 },
Operator::I64Sub,
Operator::GlobalSet {
global_index: idx_remaining_points,
},
]
}
#[cfg(test)]
mod tests {
use super::*;
fn cost(_: &Operator) -> u64 {
1
}
#[test]
fn debug_for_metering_works() {
assert_eq!(1, cost(&Operator::Nop));
assert_eq!(
"Metering { initial_limit: 0, cost_function: \"<cost_function>\", global_indexes: Mutex { data: None, poisoned: false, .. }, function_locals: [] }",
format!("{:?}", Metering::new(0, cost, None))
);
}
#[test]
fn debug_for_function_metering_works() {
assert_eq!(1, cost(&Operator::Nop));
let metering = Metering::new(0, cost, None);
metering
.transform_module_info(&mut ModuleInfo::new())
.unwrap();
assert_eq!(
"FunctionMetering { is_first_operator: true, cost_function: \"<cost_function>\", global_indexes: MeteringGlobalIndexes(GlobalIndex(0), GlobalIndex(1)), accumulated_cost: 0, charged_locals_count: 0 }",
format!("{:?}", metering.generate_function_middleware(LocalFunctionIndex::from_u32(0)))
);
}
#[test]
#[should_panic(
expected = "Metering::transform_module_info: Attempting to use a `Metering` middleware from multiple modules."
)]
fn using_metering_multiple_times_should_panic() {
assert_eq!(1, cost(&Operator::Nop));
let metering = Metering::new(0, cost, None);
let mut module_1 = ModuleInfo::new();
let mut module_2 = ModuleInfo::new();
metering.transform_module_info(&mut module_1).unwrap();
metering.transform_module_info(&mut module_2).unwrap();
}
}