soroban_env_host/budget/wasmi_helper.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
use crate::{
budget::{AsBudget, Budget},
host::error::TryBorrowOrErr,
xdr::ContractCostType,
Host, HostError,
};
use wasmi::{errors, CompilationMode, EnforcedLimits, FuelCosts, ResourceLimiter};
pub(crate) struct WasmiLimits {
pub table_elements: u32,
pub instances: usize,
// The `tables` limit is only relevant if `wasmi_reference_type` is enabled
pub tables: usize,
// The `memories` limit is irrelevant. At the current version of WASM, at
// most one memory may be defined or imported in a single module
pub memories: usize,
}
pub(crate) const WASMI_LIMITS_CONFIG: WasmiLimits = WasmiLimits {
table_elements: 1000,
instances: 1,
tables: 1,
memories: 1,
};
impl ResourceLimiter for Host {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> Result<bool, errors::MemoryError> {
let host_limit = self
.as_budget()
.get_mem_bytes_remaining()
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)?;
let delta = (desired as u64).saturating_sub(current as u64);
let allow = if delta > host_limit {
false
} else {
match maximum {
Some(max) => desired <= max,
None => true,
}
};
if allow {
#[cfg(any(test, feature = "testutils", feature = "bench"))]
{
self.as_budget()
.track_wasm_mem_alloc(delta)
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)?;
}
self.as_budget()
.charge(ContractCostType::MemAlloc, Some(delta))
.map(|_| true)
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)
} else {
Err(errors::MemoryError::OutOfBoundsGrowth)
}
}
fn table_growing(
&mut self,
current: u32,
desired: u32,
maximum: Option<u32>,
) -> Result<bool, errors::TableError> {
let allow = if desired > WASMI_LIMITS_CONFIG.table_elements {
false
} else {
match maximum {
Some(max) => desired <= max,
None => true,
}
};
if allow {
Ok(allow)
} else {
Err(errors::TableError::GrowOutOfBounds {
maximum: maximum.unwrap_or(u32::MAX),
current,
delta: desired - current,
})
}
}
fn instances(&self) -> usize {
WASMI_LIMITS_CONFIG.instances
}
fn tables(&self) -> usize {
WASMI_LIMITS_CONFIG.tables
}
fn memories(&self) -> usize {
WASMI_LIMITS_CONFIG.memories
}
}
pub(crate) fn load_calibrated_fuel_costs() -> FuelCosts {
let fuel_costs = FuelCosts::default();
// Wasmi 0.36 has a simplified fuel-cost schedule, based on its new
// register-machine architecture. It is simply this: 1 fuel per wasm
// instruction, and each fuel represents moving 8 registers or 64 bytes.
//
// All this is hard-wired now (see FuelCosts::default) and it seems broadly
// _correct_ in terms of the actual runtime costs we see in wasmi: it costs
// _about_ 8-16 CPU instructions per fuel when we look at instructions we
// can even calibrate, and wasmi's own benchmarks suggest it runs about
// 8-16x slower than native code. So we will just leave their calibration
// as-is and hope it's not too wildly off in practice.
fuel_costs
}
pub(crate) fn get_wasmi_config(
budget: &Budget,
mut cmode: CompilationMode,
) -> Result<wasmi::Config, HostError> {
let mut config = wasmi::Config::default();
let fuel_costs = budget.0.try_borrow_or_err()?.fuel_costs;
let enforced_limits = if cfg!(feature = "bench") {
// Disable limits when benchmarking, to allow large inputs.
EnforcedLimits::default()
} else {
let mut limits = EnforcedLimits::strict();
// We mostly use the new "strict" limits, which are designed to minimize
// the possibility of DoS Wasms, but we turn off one: the one that
// rejects Wasms when the average size of functions is too small. This
// is a potential DoS, but only when there are in fact lots of
// functions; we expect that given the total size limit of the Wasms in
// the network it's not going to be a real DoS for us, and it's fairly
// easy to trigger in practice with benign inputs like test wasms.
limits.min_avg_bytes_per_function = None;
limits
};
if cfg!(feature = "bench") {
// Allow overriding compilation mode for special benchmark mode.
if std::env::var("CHECK_LAZY_COMPILATION_COSTS").is_ok() {
cmode = CompilationMode::Lazy;
}
}
// Turn off most optional wasm features, leaving on some post-MVP features
// commonly enabled by Rust and Clang. Make sure all unused features are
// explicited turned off, so that we don't get "opted in" by a future wasmi
// version.
config
.consume_fuel(true)
.wasm_bulk_memory(true)
.wasm_mutable_global(true)
.wasm_sign_extension(true)
.wasm_saturating_float_to_int(false)
.wasm_multi_value(false)
.wasm_reference_types(false)
.wasm_tail_call(false)
.wasm_extended_const(false)
.floats(false)
.set_fuel_costs(fuel_costs)
.enforced_limits(enforced_limits)
.compilation_mode(cmode);
Ok(config)
}