1#![cfg(not(target_arch = "wasm32"))]
2
3use std::cell::RefCell;
4use std::collections::BTreeSet;
5
6use wasmtime::{
7 Config, Engine, Instance, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, Val,
8};
9
10use crate::core::Value;
11
12use super::{
13 inspect_direct, HaraValueType, Lifting, MemoryBindingPlan, MemoryContract, MemoryFunctionPlan,
14 MemoryResultPlan, Ownership, WasmValueType,
15};
16
17#[cfg(test)]
18mod tests;
19
20const MAX_MEMORY_BYTES: usize = 64 * 1024 * 1024;
21const MAX_VALUE_BYTES: usize = 16 * 1024 * 1024;
22const MAX_TOTAL_INPUT_BYTES: usize = 32 * 1024 * 1024;
23const MAX_TOTAL_COPY_BYTES: usize = MAX_TOTAL_INPUT_BYTES + MAX_VALUE_BYTES;
24const INVOCATION_FUEL: u64 = 10_000_000;
25const CLEANUP_FUEL: u64 = 1_000_000;
26
27struct Session {
28 store: Store<StoreLimits>,
29 instance: Instance,
30 memory: Memory,
31}
32
33pub struct WasmtimeMemoryExecutor {
39 plan: MemoryBindingPlan,
40 session: RefCell<Session>,
41}
42
43impl WasmtimeMemoryExecutor {
44 pub fn compile(bytes: &[u8], plan: MemoryBindingPlan) -> Result<Self, String> {
45 let inspection = inspect_direct(bytes)?;
46 plan.verify(&inspection)?;
47
48 let mut config = Config::new();
49 config.consume_fuel(true);
50 let engine = Engine::new(&config)
51 .map_err(|error| format!("extension/engine-unavailable: {error}"))?;
52 let module = Module::new(&engine, bytes)
53 .map_err(|error| format!("extension/module-invalid: {error}"))?;
54 if module.imports().next().is_some() {
55 return Err("extension/module-import-denied: memory.v1 must be import-free".into());
56 }
57
58 let limits = StoreLimitsBuilder::new()
59 .memory_size(MAX_MEMORY_BYTES)
60 .instances(1)
61 .memories(1)
62 .tables(1)
63 .build();
64 let mut store = Store::new(&engine, limits);
65 store.limiter(|limits| limits);
66 let instance = Instance::new(&mut store, &module, &[])
67 .map_err(|error| format!("extension/module-invalid: {error}"))?;
68 let memory = instance
69 .get_memory(&mut store, &plan.memory.export)
70 .ok_or_else(|| {
71 format!(
72 "extension/memory-missing: module does not export {}",
73 plan.memory.export
74 )
75 })?;
76 validate_instance(&plan, &mut store, &instance)?;
77
78 Ok(Self {
79 plan,
80 session: RefCell::new(Session {
81 store,
82 instance,
83 memory,
84 }),
85 })
86 }
87
88 pub fn plan(&self) -> &MemoryBindingPlan {
89 &self.plan
90 }
91
92 pub fn invoke(&self, export: &str, arguments: &[Value]) -> Result<Value, String> {
93 let function = self
94 .plan
95 .functions
96 .iter()
97 .find(|function| function.name == export)
98 .cloned()
99 .ok_or_else(|| format!("extension/export-missing: {export}"))?;
100 if arguments.len() != function.arguments.len() {
101 return Err(format!(
102 "extension/arity: {export} expects {} arguments, got {}",
103 function.arguments.len(),
104 arguments.len()
105 ));
106 }
107
108 let mut session = self.session.borrow_mut();
109 session
110 .store
111 .set_fuel(INVOCATION_FUEL)
112 .map_err(|error| format!("extension/execution-limit: {error}"))?;
113
114 let mut release_always = BTreeSet::new();
115 let mut release_on_failure = BTreeSet::new();
116 let mut call_completed = false;
117 let outcome = invoke_inner(
118 &self.plan.memory,
119 &function,
120 arguments,
121 &mut session,
122 &mut release_always,
123 &mut release_on_failure,
124 &mut call_completed,
125 );
126
127 if !call_completed {
128 release_always.extend(release_on_failure);
129 }
130 let cleanup = release_pointers(&self.plan.memory, &mut session, &release_always);
131 combine_outcome(outcome, cleanup)
132 }
133}
134
135fn validate_instance(
136 plan: &MemoryBindingPlan,
137 store: &mut Store<StoreLimits>,
138 instance: &Instance,
139) -> Result<(), String> {
140 for function in &plan.functions {
141 instance
142 .get_func(&mut *store, &function.wasm_export)
143 .ok_or_else(|| {
144 format!(
145 "extension/export-missing: {} -> {}",
146 function.name, function.wasm_export
147 )
148 })?;
149 }
150 if let Some(name) = plan.memory.allocate.as_deref() {
151 instance
152 .get_typed_func::<i32, i32>(&mut *store, name)
153 .map_err(|error| format!("extension/allocator-invalid: {name} ({error})"))?;
154 }
155 if let Some(name) = plan.memory.release.as_deref() {
156 instance
157 .get_typed_func::<i32, ()>(&mut *store, name)
158 .map_err(|error| format!("extension/release-invalid: {name} ({error})"))?;
159 }
160 Ok(())
161}
162
163#[allow(clippy::too_many_arguments)]
164fn invoke_inner(
165 memory_contract: &MemoryContract,
166 function_plan: &MemoryFunctionPlan,
167 arguments: &[Value],
168 session: &mut Session,
169 release_always: &mut BTreeSet<i32>,
170 release_on_failure: &mut BTreeSet<i32>,
171 call_completed: &mut bool,
172) -> Result<Value, String> {
173 let mut raw_arguments = Vec::with_capacity(function_plan.raw_arguments.len());
174 let mut total_input_bytes = 0usize;
175 let mut total_copy_bytes = 0usize;
176
177 for (argument_plan, value) in function_plan.arguments.iter().zip(arguments) {
178 if argument_plan.lowering.is_none() {
179 raw_arguments.push(scalar_argument(
180 &function_plan.name,
181 &argument_plan.hara_type,
182 value,
183 )?);
184 continue;
185 }
186 let bytes = memory_argument_bytes(&argument_plan.hara_type, value, &function_plan.name)?;
187 total_input_bytes = total_input_bytes
188 .checked_add(bytes.len())
189 .ok_or_else(|| "extension/resource-limit: input byte count overflow".to_owned())?;
190 if bytes.len() > MAX_VALUE_BYTES || total_input_bytes > MAX_TOTAL_INPUT_BYTES {
191 return Err(format!(
192 "extension/resource-limit: {} input exceeds the memory.v1 byte limit",
193 function_plan.name
194 ));
195 }
196 let (pointer, length) = lower_pointer_length(
197 memory_contract,
198 session,
199 bytes,
200 &function_plan.name,
201 argument_plan.ownership,
202 release_on_failure,
203 )?;
204 total_copy_bytes = total_copy_bytes
205 .checked_add(bytes.len())
206 .ok_or_else(|| "extension/resource-limit: copy byte count overflow".to_owned())?;
207 if total_copy_bytes > MAX_TOTAL_COPY_BYTES {
208 return Err(format!(
209 "extension/resource-limit: {} exceeds the memory.v1 aggregate copy limit",
210 function_plan.name
211 ));
212 }
213 raw_arguments.push(Val::I32(pointer));
214 raw_arguments.push(Val::I32(length));
215 }
216
217 let function = session
218 .instance
219 .get_func(&mut session.store, &function_plan.wasm_export)
220 .ok_or_else(|| {
221 format!(
222 "extension/export-missing: {} -> {}",
223 function_plan.name, function_plan.wasm_export
224 )
225 })?;
226 let mut raw_results = if function_plan.raw_returns == WasmValueType::Void {
227 Vec::new()
228 } else {
229 vec![default_result(function_plan.raw_returns)]
230 };
231 function
232 .call(&mut session.store, &raw_arguments, &mut raw_results)
233 .map_err(|error| format!("extension/invoke-failed: {} ({error})", function_plan.name))?;
234 *call_completed = true;
235
236 lift_result(
237 &function_plan.name,
238 &function_plan.returns,
239 raw_results.into_iter().next(),
240 session,
241 release_always,
242 &mut total_copy_bytes,
243 )
244}
245
246fn memory_argument_bytes<'a>(
247 expected: &HaraValueType,
248 value: &'a Value,
249 export: &str,
250) -> Result<&'a [u8], String> {
251 match (expected, value) {
252 (HaraValueType::Bytes, Value::Bytes(bytes)) => Ok(bytes),
253 (HaraValueType::String, Value::String(value)) => Ok(value.as_bytes()),
254 _ => Err(format!(
255 "extension/type-error: {export} expects :{}",
256 hara_type_name(expected)
257 )),
258 }
259}
260
261fn lower_pointer_length(
262 contract: &MemoryContract,
263 session: &mut Session,
264 bytes: &[u8],
265 export: &str,
266 ownership: Option<Ownership>,
267 release_on_failure: &mut BTreeSet<i32>,
268) -> Result<(i32, i32), String> {
269 let length = i32::try_from(bytes.len())
270 .map_err(|_| format!("extension/resource-limit: {export} input is too large"))?;
271 if length == 0 {
272 return Ok((0, 0));
273 }
274 let allocator_name = contract
275 .allocate
276 .as_deref()
277 .ok_or_else(|| format!("extension/allocator-missing: {export} requires an allocator"))?;
278 let allocator = session
279 .instance
280 .get_typed_func::<i32, i32>(&mut session.store, allocator_name)
281 .map_err(|error| format!("extension/allocator-invalid: {allocator_name} ({error})"))?;
282 let pointer = allocator
283 .call(&mut session.store, length)
284 .map_err(|error| format!("extension/allocator-failed: {export} ({error})"))?;
285 if pointer != 0 && ownership == Some(Ownership::Transferred) {
286 release_on_failure.insert(pointer);
287 }
288 let start = checked_range(&session.memory, &session.store, pointer, length, export)?;
289 session
290 .memory
291 .write(&mut session.store, start, bytes)
292 .map_err(|error| format!("extension/memory-write-failed: {export} ({error})"))?;
293 Ok((pointer, length))
294}
295
296fn lift_result(
297 export: &str,
298 plan: &MemoryResultPlan,
299 raw: Option<Val>,
300 session: &mut Session,
301 release_always: &mut BTreeSet<i32>,
302 total_copy_bytes: &mut usize,
303) -> Result<Value, String> {
304 if plan.lifting.is_none() {
305 return scalar_result(export, &plan.hara_type, raw);
306 }
307 if plan.lifting != Some(Lifting::PackedI64) {
308 return Err(format!(
309 "extension/abi-type-unsupported: {export} result lifting"
310 ));
311 }
312 let Some(Val::I64(raw)) = raw else {
313 return Err(format!(
314 "extension/abi-type-unsupported: {export} expected packed i64"
315 ));
316 };
317
318 let packed = raw as u64;
321 let pointer_u32 = packed as u32;
322 let length_u32 = (packed >> 32) as u32;
323 let pointer = i32::try_from(pointer_u32)
324 .map_err(|_| format!("extension/memory-range: {export} pointer is out of range"))?;
325 let length = i32::try_from(length_u32)
326 .map_err(|_| format!("extension/resource-limit: {export} result is too large"))?;
327 let length_usize = usize::try_from(length)
328 .map_err(|_| format!("extension/memory-range: {export} result length is negative"))?;
329 if length_usize > MAX_VALUE_BYTES {
330 return Err(format!(
331 "extension/resource-limit: {export} result exceeds the memory.v1 byte limit"
332 ));
333 }
334 *total_copy_bytes = total_copy_bytes
335 .checked_add(length_usize)
336 .ok_or_else(|| format!("extension/resource-limit: {export} copy byte count overflow"))?;
337 if *total_copy_bytes > MAX_TOTAL_COPY_BYTES {
338 return Err(format!(
339 "extension/resource-limit: {export} exceeds the memory.v1 aggregate copy limit"
340 ));
341 }
342 if plan.ownership == Some(Ownership::Caller) && pointer != 0 {
343 release_always.insert(pointer);
344 }
345 let start = checked_range(&session.memory, &session.store, pointer, length, export)?;
346 let mut bytes = vec![0u8; length_usize];
347 session
348 .memory
349 .read(&session.store, start, &mut bytes)
350 .map_err(|error| format!("extension/memory-read-failed: {export} ({error})"))?;
351 match plan.hara_type {
352 HaraValueType::Bytes => Ok(Value::Bytes(bytes)),
353 HaraValueType::String => String::from_utf8(bytes)
354 .map(Value::String)
355 .map_err(|error| format!("extension/utf8-invalid: {export} ({error})")),
356 _ => Err(format!(
357 "extension/abi-type-unsupported: {export} cannot lift :{}",
358 hara_type_name(&plan.hara_type)
359 )),
360 }
361}
362
363fn checked_range(
364 memory: &Memory,
365 store: &Store<StoreLimits>,
366 pointer: i32,
367 length: i32,
368 export: &str,
369) -> Result<usize, String> {
370 let start = usize::try_from(pointer)
371 .map_err(|_| format!("extension/memory-range: {export} pointer is negative"))?;
372 let length = usize::try_from(length)
373 .map_err(|_| format!("extension/memory-range: {export} length is negative"))?;
374 let end = start
375 .checked_add(length)
376 .ok_or_else(|| format!("extension/memory-range: {export} range overflow"))?;
377 if end > memory.data_size(store) {
378 return Err(format!(
379 "extension/memory-range: {export} range {start}..{end} exceeds linear memory"
380 ));
381 }
382 Ok(start)
383}
384
385fn release_pointers(
386 contract: &MemoryContract,
387 session: &mut Session,
388 pointers: &BTreeSet<i32>,
389) -> Result<(), String> {
390 if pointers.is_empty() {
391 return Ok(());
392 }
393 let release_name = contract
394 .release
395 .as_deref()
396 .ok_or_else(|| "extension/release-missing: cleanup requires a release export".to_owned())?;
397 let release = session
398 .instance
399 .get_typed_func::<i32, ()>(&mut session.store, release_name)
400 .map_err(|error| format!("extension/release-invalid: {release_name} ({error})"))?;
401 session
402 .store
403 .set_fuel(CLEANUP_FUEL)
404 .map_err(|error| format!("extension/release-failed: fuel ({error})"))?;
405
406 let mut failures = Vec::new();
407 for pointer in pointers {
408 if let Err(error) = release.call(&mut session.store, *pointer) {
409 failures.push(format!("{pointer}: {error}"));
410 }
411 }
412 if failures.is_empty() {
413 Ok(())
414 } else {
415 Err(format!("extension/release-failed: {}", failures.join("; ")))
416 }
417}
418
419fn combine_outcome(
420 outcome: Result<Value, String>,
421 cleanup: Result<(), String>,
422) -> Result<Value, String> {
423 match (outcome, cleanup) {
424 (Ok(value), Ok(())) => Ok(value),
425 (Err(error), Ok(())) => Err(error),
426 (Ok(_), Err(cleanup)) => Err(cleanup),
427 (Err(error), Err(cleanup)) => Err(format!("{error}; cleanup: {cleanup}")),
428 }
429}
430
431fn scalar_argument(export: &str, expected: &HaraValueType, value: &Value) -> Result<Val, String> {
432 fn finite_f32(value: f64) -> Result<f32, String> {
433 let value = value as f32;
434 if value.is_finite() {
435 Ok(value)
436 } else {
437 Err("non-finite number".into())
438 }
439 }
440 let type_error = || {
441 format!(
442 "extension/type-error: {export} expects :{}",
443 hara_type_name(expected)
444 )
445 };
446 match (expected, value) {
447 (HaraValueType::I32, Value::Number(value)) => i32::try_from(*value)
448 .map(Val::I32)
449 .map_err(|_| type_error()),
450 (HaraValueType::Boolean, Value::Bool(value)) => Ok(Val::I32(i32::from(*value))),
451 (HaraValueType::I64, Value::Number(value)) => Ok(Val::I64(*value)),
452 (HaraValueType::F32, Value::Float(value)) => {
453 Ok(Val::F32(finite_f32(*value)?.to_bits()))
454 }
455 (HaraValueType::F32, Value::Number(value)) => {
456 Ok(Val::F32(finite_f32(*value as f64)?.to_bits()))
457 }
458 (HaraValueType::F64, Value::Float(value)) => {
459 Ok(Val::F64(crate::numeric::finite_float(*value)?.to_bits()))
460 }
461 (HaraValueType::F64, Value::Number(value)) => Ok(Val::F64((*value as f64).to_bits())),
462 _ => Err(type_error()),
463 }
464}
465
466fn scalar_result(
467 export: &str,
468 expected: &HaraValueType,
469 raw: Option<Val>,
470) -> Result<Value, String> {
471 match (expected, raw) {
472 (HaraValueType::Void, None) => Ok(Value::Nil),
473 (HaraValueType::I32, Some(Val::I32(value))) => Ok(Value::Number(i64::from(value))),
474 (HaraValueType::Boolean, Some(Val::I32(value))) => Ok(Value::Bool(value != 0)),
475 (HaraValueType::I64, Some(Val::I64(value))) => Ok(Value::Number(value)),
476 (HaraValueType::F32, Some(Val::F32(value))) => {
477 Ok(Value::Float(crate::numeric::finite_float(
478 f32::from_bits(value) as f64,
479 )?))
480 }
481 (HaraValueType::F64, Some(Val::F64(value))) => {
482 Ok(Value::Float(crate::numeric::finite_float(f64::from_bits(value))?))
483 }
484 _ => Err(format!(
485 "extension/abi-type-unsupported: {export} -> :{}",
486 hara_type_name(expected)
487 )),
488 }
489}
490
491fn default_result(raw: WasmValueType) -> Val {
492 match raw {
493 WasmValueType::I32 => Val::I32(0),
494 WasmValueType::I64 => Val::I64(0),
495 WasmValueType::F32 => Val::F32(0),
496 WasmValueType::F64 => Val::F64(0),
497 WasmValueType::Void => unreachable!("void functions do not allocate a result slot"),
498 }
499}
500
501fn hara_type_name(value: &HaraValueType) -> &str {
502 match value {
503 HaraValueType::I32 => "i32",
504 HaraValueType::I64 => "i64",
505 HaraValueType::F32 => "f32",
506 HaraValueType::F64 => "f64",
507 HaraValueType::Boolean => "boolean",
508 HaraValueType::String => "string",
509 HaraValueType::Bytes => "bytes",
510 HaraValueType::Record(_) => "record",
511 HaraValueType::Variant(_) => "variant",
512 HaraValueType::Handle(_) => "handle",
513 HaraValueType::Callback(_) => "callback",
514 HaraValueType::Void => "void",
515 }
516}