1use std::path::Path;
13use std::sync::Arc;
14
15use rich_plugin_api::abi::{PluginAbi, wasm as names};
16use wasmi::{
17 Config, EnforcedLimits, Engine, ExternType, Instance, Linker, Memory, Module, Store,
18 StoreLimits, StoreLimitsBuilder, TrapCode,
19};
20
21use super::{AbiBackend, LoadError, MAX_OUTPUT_BYTES, RuntimeKind, RuntimePlugin, WasmLimits};
22
23const PAGE: u64 = 64 * 1024;
25
26struct WasmBackend {
27 engine: Engine,
28 module: Module,
29 limits: WasmLimits,
30}
31
32enum CallError {
34 OutOfFuel,
35 Memory(String),
36 Other(String),
37}
38
39impl CallError {
40 fn message(self, limits: &WasmLimits) -> String {
41 match self {
42 CallError::OutOfFuel => format!(
43 "the plugin ran out of fuel (the limit is {} units per call)",
44 limits.fuel
45 ),
46 CallError::Memory(message) | CallError::Other(message) => message,
47 }
48 }
49}
50
51fn classify(error: wasmi::Error) -> CallError {
52 match error.as_trap_code() {
53 Some(TrapCode::OutOfFuel) => CallError::OutOfFuel,
54 Some(TrapCode::GrowthOperationLimited) => CallError::Memory(
55 "the plugin tried to use more memory (or table elements) than the limit allows"
56 .to_string(),
57 ),
58 _ => CallError::Other(format!("the plugin failed: {error}")),
59 }
60}
61
62impl WasmBackend {
63 fn instantiate(&self) -> Result<(Store<StoreLimits>, Instance, Memory), CallError> {
65 let limits = StoreLimitsBuilder::new()
66 .memory_size(self.limits.memory_bytes)
67 .memories(1)
68 .tables(1)
69 .table_elements(self.limits.table_elements)
70 .instances(1)
71 .trap_on_grow_failure(true)
72 .build();
73 let mut store = Store::new(&self.engine, limits);
74 store.limiter(|limits| limits);
75 store
76 .set_fuel(self.limits.fuel)
77 .map_err(|e| CallError::Other(e.to_string()))?;
78 let linker = Linker::<StoreLimits>::new(&self.engine);
79 let instance = linker
80 .instantiate_and_start(&mut store, &self.module)
81 .map_err(classify)?;
82 let memory = instance
83 .get_memory(&store, names::MEMORY)
84 .ok_or_else(|| CallError::Other(format!("it does not export `{}`", names::MEMORY)))?;
85 Ok((store, instance, memory))
86 }
87
88 fn read_packed(
90 store: &Store<StoreLimits>,
91 memory: &Memory,
92 packed: u64,
93 ) -> Result<String, CallError> {
94 let ptr = ((packed >> 32) & 0x7fff_ffff) as usize;
95 let len = (packed & 0xffff_ffff) as usize;
96 if len > MAX_OUTPUT_BYTES {
97 return Err(CallError::Other(format!(
98 "the plugin returned {len} bytes; at most {MAX_OUTPUT_BYTES} are accepted"
99 )));
100 }
101 let bytes = memory
102 .data(store)
103 .get(ptr..ptr.saturating_add(len))
104 .ok_or_else(|| {
105 CallError::Other("the plugin returned a range outside its memory".into())
106 })?;
107 String::from_utf8(bytes.to_vec())
108 .map_err(|_| CallError::Other("the plugin returned non-UTF-8".into()))
109 }
110
111 fn manifest(&self) -> Result<String, CallError> {
112 let (mut store, instance, memory) = self.instantiate()?;
113 let manifest = instance
114 .get_typed_func::<(), i64>(&store, names::MANIFEST)
115 .map_err(|_| missing(names::MANIFEST, "() -> i64"))?;
116 let packed = manifest.call(&mut store, ()).map_err(classify)? as u64;
117 Self::read_packed(&store, &memory, packed & !names::ERROR_BIT)
118 }
119
120 fn run(&self, capability: usize, input: &str, width: u32) -> Result<String, CallError> {
121 let (mut store, instance, memory) = self.instantiate()?;
122 let alloc = instance
123 .get_typed_func::<i32, i32>(&store, names::ALLOC)
124 .map_err(|_| missing(names::ALLOC, "(i32) -> i32"))?;
125 let call = instance
126 .get_typed_func::<(i32, i32, i32, i32), i64>(&store, names::CALL)
127 .map_err(|_| missing(names::CALL, "(i32, i32, i32, i32) -> i64"))?;
128 let too_big = || CallError::Memory("the input is larger than the plugin can accept".into());
129 let len = i32::try_from(input.len()).map_err(|_| too_big())?;
130 let ptr = alloc.call(&mut store, len).map_err(classify)?;
131 let start = usize::try_from(ptr).map_err(|_| too_big())?;
132 memory
133 .write(&mut store, start, input.as_bytes())
134 .map_err(|_| {
135 CallError::Other("the plugin's allocation is outside its memory".into())
136 })?;
137 let capability = i32::try_from(capability).unwrap_or(i32::MAX);
138 let width = i32::try_from(width).unwrap_or(i32::MAX);
139 let packed = call
140 .call(&mut store, (capability, ptr, len, width))
141 .map_err(classify)? as u64;
142 let text = Self::read_packed(&store, &memory, packed & !names::ERROR_BIT)?;
143 if packed & names::ERROR_BIT != 0 {
144 Err(CallError::Other(text))
145 } else {
146 Ok(text)
147 }
148 }
149}
150
151fn missing(name: &str, signature: &str) -> CallError {
152 CallError::Other(format!("it does not export `{name}` as {signature}"))
153}
154
155impl AbiBackend for WasmBackend {
156 fn call(&self, capability: usize, input: &str, width: u32) -> Result<String, String> {
157 self.run(capability, input, width)
158 .map_err(|error| error.message(&self.limits))
159 }
160}
161
162pub fn load_wasm(path: &Path, limits: &WasmLimits) -> Result<RuntimePlugin, LoadError> {
169 let path = &std::fs::canonicalize(path).map_err(|error| LoadError::Io {
172 path: path.to_path_buf(),
173 message: error.to_string(),
174 })?;
175 let io = |message: String| LoadError::Io {
176 path: path.to_path_buf(),
177 message,
178 };
179 let limit = |message: String| LoadError::Limit {
180 path: path.to_path_buf(),
181 message,
182 };
183 let size = std::fs::metadata(path)
184 .map_err(|e| io(e.to_string()))?
185 .len();
186 if size > limits.module_bytes {
187 return Err(limit(format!(
188 "the module is {size} bytes; at most {} are accepted",
189 limits.module_bytes
190 )));
191 }
192 let bytes = std::fs::read(path).map_err(|e| io(e.to_string()))?;
193 load_wasm_bytes(&bytes, path, limits)
194}
195
196pub fn load_wasm_bytes(
201 bytes: &[u8],
202 path: &Path,
203 limits: &WasmLimits,
204) -> Result<RuntimePlugin, LoadError> {
205 let not_a_plugin = |message: String| LoadError::NotAPlugin {
206 path: path.to_path_buf(),
207 message,
208 };
209 let limit = |message: String| LoadError::Limit {
210 path: path.to_path_buf(),
211 message,
212 };
213 if bytes.len() as u64 > limits.module_bytes {
214 return Err(limit(format!(
215 "the module is {} bytes; at most {} are accepted",
216 bytes.len(),
217 limits.module_bytes
218 )));
219 }
220
221 wasmparser::Validator::new()
225 .validate_all(bytes)
226 .map_err(|e| not_a_plugin(format!("invalid WASM: {e}")))?;
227
228 let mut config = Config::default();
229 config
230 .consume_fuel(true)
231 .enforced_limits(EnforcedLimits::strict());
232 let engine = Engine::new(&config);
233 let module =
234 Module::new(&engine, bytes).map_err(|e| not_a_plugin(format!("invalid WASM: {e}")))?;
235 if let Some(import) = module.imports().next() {
236 return Err(not_a_plugin(format!(
237 "it imports `{}.{}`, but the host provides nothing to import (no WASI, no host \
238 functions)",
239 import.module(),
240 import.name()
241 )));
242 }
243 for export in module.exports() {
244 if let ExternType::Memory(memory) = export.ty() {
245 let bytes = memory.minimum().saturating_mul(PAGE);
246 if bytes > limits.memory_bytes as u64 {
247 return Err(limit(format!(
248 "its memory starts at {bytes} bytes, over the limit of {} bytes",
249 limits.memory_bytes
250 )));
251 }
252 }
253 }
254
255 let backend = WasmBackend {
256 engine,
257 module,
258 limits: *limits,
259 };
260 let manifest = backend.manifest().map_err(|error| match error {
261 CallError::OutOfFuel | CallError::Memory(_) => limit(error.message(limits)),
262 CallError::Other(message) => not_a_plugin(message),
263 })?;
264 let abi = PluginAbi::parse_manifest(&manifest).map_err(|error| LoadError::Abi {
265 path: path.to_path_buf(),
266 error,
267 })?;
268 RuntimePlugin::new(abi, RuntimeKind::Wasm, path, Arc::new(backend))
269}