1use std::sync::{Arc, Mutex};
20use std::time::Instant;
21
22use wasmtime::{Engine, Linker, Module, Store};
23use wasmtime_wasi::p1::WasiP1Ctx;
24use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
25use wasmtime_wasi::WasiCtxBuilder;
26
27use crate::runtime::{ExecContext, ExecError, ExecOutput, ExitStatus, OutputFormat, Runtime};
28use crate::wasm::engine::SandboxLimits;
29
30pub struct WasmModule {
35 language: &'static str,
36 engine: Engine,
37 module: Module,
38 limits: SandboxLimits,
39}
40
41impl WasmModule {
42 pub fn from_bytes(
45 language: &'static str,
46 engine: &Engine,
47 wasm: &[u8],
48 ) -> Result<Self, ExecError> {
49 let module = Module::from_binary(engine, wasm)
50 .map_err(|e| ExecError::Sandbox(format!("load wasm: {e}")))?;
51 Ok(Self {
52 language,
53 engine: engine.clone(),
54 module,
55 limits: SandboxLimits::default(),
56 })
57 }
58
59 pub fn with_limits(mut self, limits: SandboxLimits) -> Self {
63 self.limits = limits;
64 self
65 }
66}
67
68impl Runtime for WasmModule {
69 fn language(&self) -> &'static str {
70 self.language
71 }
72
73 fn execute(&self, source: &str, ctx: &ExecContext) -> Result<ExecOutput, ExecError> {
74 let start = Instant::now();
75
76 let stdin_pipe = MemoryInputPipe::new(source.as_bytes().to_vec());
79 let stdout_pipe = MemoryOutputPipe::new(64 * 1024);
80 let stderr_pipe = MemoryOutputPipe::new(64 * 1024);
81
82 let stdout_read = stdout_pipe.clone();
83 let stderr_read = stderr_pipe.clone();
84
85 let wasi = WasiCtxBuilder::new()
86 .stdin(stdin_pipe)
87 .stdout(stdout_pipe)
88 .stderr(stderr_pipe)
89 .build_p1();
92
93 let mut store = Store::new(&self.engine, wasi);
94 store
95 .set_fuel(self.limits.fuel)
96 .map_err(|e| ExecError::Sandbox(format!("set fuel: {e}")))?;
97
98 store.set_epoch_deadline(1);
102 let timeout_engine = self.engine.clone();
103 let cancel_guard = Arc::new(Mutex::new(false));
104 let cancel_for_thread = cancel_guard.clone();
105 let timeout = ctx.timeout;
106 std::thread::Builder::new()
107 .name("outl-wasm-watchdog".into())
108 .spawn(move || {
109 std::thread::sleep(timeout);
110 if !*cancel_for_thread.lock().unwrap() {
111 timeout_engine.increment_epoch();
112 }
113 })
114 .map_err(|e| ExecError::Sandbox(format!("spawn watchdog: {e}")))?;
115
116 let mut linker: Linker<WasiP1Ctx> = Linker::new(&self.engine);
117 wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |s| s)
118 .map_err(|e| ExecError::Sandbox(format!("link wasi: {e}")))?;
119
120 let instance = linker
121 .instantiate(&mut store, &self.module)
122 .map_err(|e| ExecError::Sandbox(format!("instantiate: {e}")))?;
123
124 let start_func = instance
125 .get_typed_func::<(), ()>(&mut store, "_start")
126 .map_err(|e| ExecError::Sandbox(format!("module missing `_start`: {e}")))?;
127 let call_result = start_func.call(&mut store, ());
128
129 *cancel_guard.lock().unwrap() = true;
131
132 let stdout = pipe_to_string(&stdout_read);
133 let stderr = pipe_to_string(&stderr_read);
134 let duration = start.elapsed();
135
136 match call_result {
137 Ok(()) => Ok(ExecOutput {
138 stdout,
139 stderr,
140 duration,
141 exit: ExitStatus::Ok,
142 format: OutputFormat::Text,
143 }),
144 Err(e) => {
145 let msg = format!("{e:?}");
147 if msg.contains("out of fuel") || msg.contains("Interrupt") {
148 return Err(ExecError::Timeout(timeout));
149 }
150 let exit = if let Some(exit_code) =
153 e.downcast_ref::<wasmtime_wasi::I32Exit>().map(|i| i.0)
154 {
155 if exit_code == 0 {
156 ExitStatus::Ok
157 } else {
158 ExitStatus::NonZero(exit_code)
159 }
160 } else {
161 ExitStatus::Trap(format!("{e}"))
162 };
163 Ok(ExecOutput {
164 stdout,
165 stderr,
166 duration,
167 exit,
168 format: OutputFormat::Text,
169 })
170 }
171 }
172 }
173}
174
175fn pipe_to_string(p: &MemoryOutputPipe) -> String {
176 String::from_utf8_lossy(p.contents().as_ref()).into_owned()
177}