1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::time::Duration;
4use wasmtime::{Config, Engine, Store};
5
6const GC_HEAP_INITIAL_BYTES: u64 = 1 << 30;
9const HOST_STACK_BYTES: usize = 16 << 20;
12
13static WASM_STACK_BYTES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
15
16pub fn deterministic_config() -> Config {
21 let mut config = Config::new();
22 config.wasm_gc(true);
23 config.wasm_function_references(true);
24 config.gc_heap_initial_size(GC_HEAP_INITIAL_BYTES);
25 #[cfg(feature = "compiler")]
28 config.wasm_component_model(false);
29 config.concurrency_support(false);
30 let wasm_stack = WASM_STACK_BYTES.load(std::sync::atomic::Ordering::Relaxed);
31 if wasm_stack > 0 {
32 config.async_stack_size(wasm_stack).max_wasm_stack(wasm_stack);
34 }
35 config
36}
37
38pub fn give_main_stack_to_wasm() {
42 let wasm_stack = main_stack_bytes().saturating_sub(HOST_STACK_BYTES);
43 if wasm_stack > 512 << 10 {
44 WASM_STACK_BYTES.store(wasm_stack, std::sync::atomic::Ordering::Relaxed);
45 }
46}
47
48#[cfg(target_os = "macos")]
49fn main_stack_bytes() -> usize {
50 unsafe { libc::pthread_get_stacksize_np(libc::pthread_self()) }
52}
53
54#[cfg(all(unix, not(target_os = "macos")))]
55fn main_stack_bytes() -> usize {
56 let mut limit = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
57 let known = unsafe { libc::getrlimit(libc::RLIMIT_STACK, &mut limit) } == 0;
59 match known {
60 true if limit.rlim_cur != libc::RLIM_INFINITY => limit.rlim_cur as usize,
61 _ => 0,
62 }
63}
64
65#[cfg(not(unix))]
66fn main_stack_bytes() -> usize {
67 0
68}
69
70pub fn metered_config(fuel: bool) -> Config {
74 let mut config = deterministic_config();
75 config.epoch_interruption(true);
76 config.consume_fuel(fuel);
77 config
78}
79
80pub const EPOCH_TICK: Duration = Duration::from_millis(10);
82
83pub fn ticking(engine: Engine) -> Engine {
86 let ticked = engine.clone();
87 std::thread::spawn(move || loop {
88 std::thread::sleep(EPOCH_TICK);
89 ticked.increment_epoch();
90 });
91 engine
92}
93
94pub fn limited_store<T: 'static>(engine: &Engine, data: T, fuel: u64, time: Duration) -> Store<T> {
98 checked_store(engine, data, fuel, time, || Ok(()))
99}
100
101pub fn checked_store<T: 'static>(
104 engine: &Engine,
105 data: T,
106 fuel: u64,
107 time: Duration,
108 mut checkpoint: impl FnMut() -> wasmtime::Result<()> + Send + Sync + 'static,
109) -> Store<T> {
110 let mut store = Store::new(engine, data);
111 let mut clock = store.set_fuel(fuel).is_err().then(|| ComputingClock::new(time));
112 if let Some(clock) = &clock {
113 let new_turn = clock.new_turn.clone();
114 store.call_hook(move |_, hook| {
115 if matches!(hook, wasmtime::CallHook::CallingWasm) {
116 new_turn.store(true, Ordering::Relaxed);
117 }
118 Ok(())
119 });
120 }
121 store.epoch_deadline_callback(move |_| {
122 checkpoint()?;
123 if let Some(clock) = clock.as_mut() {
124 clock.check()?;
125 }
126 Ok(wasmtime::UpdateDeadline::Continue(1))
127 });
128 store.set_epoch_deadline(1);
129 cap_memory(&mut store);
130 store
131}
132
133#[derive(Debug)]
135pub struct OutOfTime(pub Duration);
136
137impl std::fmt::Display for OutOfTime {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 let variable = crate::fuel::TIME_LIMIT_VARIABLE;
140 write!(f, "out of time after {:?} of computing in one turn (give it more with {variable}=<seconds>)", self.0)
141 }
142}
143
144impl std::error::Error for OutOfTime {}
145
146struct ComputingClock {
148 limit: Duration,
149 started: Option<Duration>,
150 new_turn: Arc<AtomicBool>,
152}
153
154impl ComputingClock {
155 fn new(limit: Duration) -> Self {
156 ComputingClock { limit, started: None, new_turn: Arc::default() }
157 }
158
159 fn check(&mut self) -> wasmtime::Result<()> {
160 let now = thread_cpu_time();
161 if self.new_turn.swap(false, Ordering::Relaxed) {
162 self.started = None;
163 }
164 match now.saturating_sub(*self.started.get_or_insert(now)) > self.limit {
165 true => Err(OutOfTime(self.limit).into()),
166 false => Ok(()),
167 }
168 }
169}
170
171#[cfg(unix)]
172fn thread_cpu_time() -> Duration {
173 let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 };
174 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut time) };
176 Duration::new(time.tv_sec as u64, time.tv_nsec as u32)
177}
178
179#[cfg(not(unix))]
181fn thread_cpu_time() -> Duration {
182 static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
183 START.get_or_init(std::time::Instant::now).elapsed()
184}
185
186pub const DEFAULT_MEMORY_CAP_MB: usize = 2048;
189pub const MEMORY_CAP_VARIABLE: &str = "WARP_MEMORY_CAP_MB";
191const BYTES_PER_MB: usize = 1 << 20;
192
193pub fn memory_cap_bytes() -> usize {
195 static CAP: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
196 *CAP.get_or_init(|| {
197 let megabytes = std::env::var(MEMORY_CAP_VARIABLE).ok().and_then(|value| value.trim().parse().ok());
198 megabytes.unwrap_or(DEFAULT_MEMORY_CAP_MB).saturating_mul(BYTES_PER_MB)
199 })
200}
201
202pub fn cap_memory<T>(store: &mut Store<T>) {
205 store.limiter(|_| Box::leak(Box::new(MemoryCap)));
207}
208
209struct MemoryCap;
210
211impl wasmtime::ResourceLimiter for MemoryCap {
212 fn memory_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> {
213 let cap = memory_cap_bytes();
214 if desired > cap {
215 let refusal = format!(
216 "memory cap: growing a memory to {} MB passes the cap of {} MB ({MEMORY_CAP_VARIABLE})",
217 desired / BYTES_PER_MB,
218 cap / BYTES_PER_MB
219 );
220 eprintln!("{refusal}");
222 wasmtime::bail!(refusal);
223 }
224 Ok(maximum.is_none_or(|maximum| desired <= maximum))
225 }
226
227 fn table_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> {
228 Ok(maximum.is_none_or(|maximum| desired <= maximum))
229 }
230}