Next Generation WASM Microkernel Operating System
at trap_handler 414 lines 15 kB view raw
1// Copyright 2025 Jonas Kruckenberg 2// 3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or 4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or 5// http://opensource.org/licenses/MIT>, at your option. This file may not be 6// copied, modified, or distributed except according to those terms. 7 8mod host; 9mod typed; 10 11use crate::arch; 12use crate::mem::VirtualAddress; 13use crate::util::zip_eq::IteratorExt; 14use crate::wasm::indices::VMSharedTypeIndex; 15use crate::wasm::store::{StoreOpaque, Stored}; 16use crate::wasm::trap_handler::{Trap, TrapReason}; 17use crate::wasm::types::FuncType; 18use crate::wasm::values::Val; 19use crate::wasm::vm::{ 20 ExportedFunction, VMArrayCallHostFuncContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, 21 VMVal, VmPtr, 22}; 23use crate::wasm::{MAX_WASM_STACK, Module, Store}; 24use alloc::boxed::Box; 25use alloc::sync::Arc; 26use anyhow::ensure; 27use core::ffi::c_void; 28use core::mem; 29use core::ptr::NonNull; 30pub use host::{HostFunc, IntoFunc}; 31pub use typed::{TypedFunc, WasmParams, WasmResults, WasmTy}; 32 33#[derive(Clone, Copy, Debug)] 34pub struct Func(pub(super) Stored<FuncData>); 35#[derive(Debug)] 36pub struct FuncData { 37 kind: FuncKind, 38} 39 40#[derive(Debug)] 41 42enum FuncKind { 43 StoreOwned { export: ExportedFunction }, 44 SharedHost(Arc<HostFunc>), 45 Host(Box<HostFunc>), 46} 47 48impl Func { 49 pub fn wrap<T, Params, Results>( 50 store: &mut Store<T>, 51 func: impl IntoFunc<T, Params, Results>, 52 ) -> TypedFunc<Params, Results> 53 where 54 Params: WasmParams, 55 Results: WasmResults, 56 { 57 let (func, ty) = HostFunc::wrap(store.engine(), func); 58 59 let stored = store.add_function(FuncData { 60 kind: FuncKind::Host(Box::new(func)), 61 }); 62 63 // Safety: the Rust generics ensure this is safe 64 unsafe { TypedFunc::new_unchecked(Self(stored), ty) } 65 } 66 67 pub fn typed<Params, Results>( 68 self, 69 store: &StoreOpaque, 70 ) -> crate::Result<TypedFunc<Params, Results>> 71 where 72 Params: WasmParams, 73 Results: WasmResults, 74 { 75 let ty = self.ty(store); 76 Params::typecheck(store.engine(), ty.params())?; 77 Results::typecheck(store.engine(), ty.results())?; 78 79 // Safety: the Rust generics ensure this is safe 80 Ok(unsafe { TypedFunc::new_unchecked(self, ty) }) 81 } 82 83 pub fn call( 84 self, 85 store: &mut StoreOpaque, 86 params: &[Val], 87 results: &mut [Val], 88 ) -> crate::Result<()> { 89 // Do the typechecking. Notice how `TypedFunc::call` is essentially the same function 90 // minus this typechecking? Yeah. That's the benefit of the typed function. 91 let ty = self.ty(store); 92 93 let params_ = ty.params().zip_eq(params); 94 for (expected, param) in params_ { 95 let found = param.ty(store)?; 96 ensure!( 97 expected.matches(&found), 98 "Type mismatch. Expected `{expected:?}`, but found `{found:?}`" 99 ); 100 } 101 102 ensure!( 103 results.len() >= ty.results().len(), 104 "Results slice too small. Need space for at least {}, but got only {}", 105 ty.results().len(), 106 results.len() 107 ); 108 109 // Safety: we have checked the types above, we're safe to proceed 110 unsafe { self.call_unchecked(store, params, results) } 111 } 112 113 /// Calls the given function with the provided arguments and places the results in the provided 114 /// results slice. 115 /// 116 /// # Errors 117 /// 118 /// TODO 119 /// 120 /// # Safety 121 /// 122 /// It is up to the caller to ensure the provided arguments are of the correct types and that 123 /// the `results` slice has enough space to hold the results of the function. 124 pub unsafe fn call_unchecked( 125 self, 126 store: &mut StoreOpaque, 127 params: &[Val], 128 results: &mut [Val], 129 ) -> crate::Result<()> { 130 // This function mainly performs the lowering and lifting of VMVal values from and to Rust. 131 // Because - unlike TypedFunc - we don't have compile-time knowledge about the function type, 132 // we use a heap allocated vec (obtained through `store.take_wasm_vmval_storage()`) to store 133 // our parameters into and read results from. 134 // 135 // This is obviously a little less efficient, but it's not that big of a deal. 136 137 // take out the argument storage from the store 138 let mut values_vec = store.take_wasm_vmval_storage(); 139 debug_assert!(values_vec.is_empty()); 140 141 // resize the vec so we can be sure that params and results will fit. 142 let values_vec_size = params.len().max(results.len()); 143 values_vec.resize_with(values_vec_size, || VMVal::v128(0)); 144 145 // copy the arguments into the storage vec 146 for (arg, slot) in params.iter().zip(&mut values_vec) { 147 // Safety: the store stays alive for the duration of the call 148 unsafe { 149 *slot = arg.to_vmval(store)?; 150 } 151 } 152 153 // Safety: func refs obtained from our store are always usable by us. 154 let func_ref = unsafe { self.vm_func_ref(store).as_ref() }; 155 156 // do the actual call 157 // Safety: at this point we have typechecked, we have allocated enough memory for the params 158 // and results, and obtained a valid func ref to call. 159 unsafe { 160 do_call(store, func_ref, &mut values_vec)?; 161 } 162 163 // copy the results out of the storage 164 let func_ty = self.ty(store); 165 for ((i, slot), vmval) in results.iter_mut().enumerate().zip(&values_vec) { 166 let ty = func_ty.result(i).unwrap(); 167 // Safety: caller has to ensure safety 168 *slot = unsafe { Val::from_vmval(store, *vmval, ty) }; 169 } 170 171 // clean up and return the argument storage 172 values_vec.truncate(0); 173 store.return_wasm_vmval_storage(values_vec); 174 175 Ok(()) 176 } 177 178 pub fn ty(self, store: &StoreOpaque) -> FuncType { 179 FuncType::from_shared_type_index(store.engine(), self.type_index(store)) 180 } 181 182 pub fn matches_ty(self, store: &StoreOpaque, ty: FuncType) -> bool { 183 let actual_ty = self.ty(store); 184 actual_ty.matches(&ty) 185 } 186 187 pub(super) fn type_index(self, store: &StoreOpaque) -> VMSharedTypeIndex { 188 // Safety: TODO 189 unsafe { self.vm_func_ref(store).as_ref().type_index } 190 } 191 192 pub(super) unsafe fn from_exported_function( 193 store: &mut StoreOpaque, 194 export: ExportedFunction, 195 ) -> Self { 196 let stored = store.add_function(FuncData { 197 kind: FuncKind::StoreOwned { export }, 198 }); 199 Self(stored) 200 } 201 202 pub(super) fn as_vmfunction_import( 203 self, 204 store: &mut StoreOpaque, 205 module: &Module, 206 ) -> VMFunctionImport { 207 let f = self.vm_func_ref(store); 208 209 // Safety: TODO 210 unsafe { 211 VMFunctionImport { 212 wasm_call: f.as_ref().wasm_call.unwrap_or_else(|| { 213 // Assert that this is a array-call function, since those 214 // are the only ones that could be missing a `wasm_call` 215 // trampoline. 216 let _ = VMArrayCallHostFuncContext::from_opaque(f.as_ref().vmctx.as_non_null()); 217 218 let sig = self.type_index(store); 219 220 let ptr = module.wasm_to_array_trampoline(sig).expect( 221 "if the wasm is importing a function of a given type, it must have the \ 222 type's trampoline", 223 ); 224 225 VmPtr::from(ptr) 226 }), 227 array_call: f.as_ref().array_call, 228 vmctx: f.as_ref().vmctx, 229 } 230 } 231 } 232 233 pub(super) fn comes_from_same_store(self, store: &StoreOpaque) -> bool { 234 store.has_function(self.0) 235 } 236 237 pub(super) unsafe fn from_vm_func_ref( 238 store: &mut StoreOpaque, 239 func_ref: NonNull<VMFuncRef>, 240 ) -> Self { 241 // Safety: ensured by caller 242 unsafe { 243 debug_assert!(func_ref.as_ref().type_index != VMSharedTypeIndex::default()); 244 Func::from_exported_function(store, ExportedFunction { func_ref }) 245 } 246 } 247 248 pub(super) fn vm_func_ref(self, store: &StoreOpaque) -> NonNull<VMFuncRef> { 249 match &store[self.0].kind { 250 FuncKind::StoreOwned { export } => export.func_ref, 251 FuncKind::SharedHost(func) => func.func_ref(), 252 FuncKind::Host(func) => func.func_ref(), 253 } 254 } 255 256 pub(super) unsafe fn from_vmval(store: &mut StoreOpaque, raw: *mut c_void) -> Option<Self> { 257 // Safety: ensured by caller 258 unsafe { Some(Func::from_vm_func_ref(store, NonNull::new(raw.cast())?)) } 259 } 260 261 /// Extracts the raw value of this `Func`, which is owned by `store`. 262 /// 263 /// This function returns a value that's suitable for writing into the 264 /// `funcref` field of the [`VMVal`] structure. 265 /// 266 /// # Safety 267 /// 268 /// The returned value is only valid for as long as the store is alive and 269 /// this function is properly rooted within it. Additionally this function 270 /// should not be liberally used since it's a very low-level knob. 271 pub(super) unsafe fn to_vmval(self, store: &mut StoreOpaque) -> *mut c_void { 272 self.vm_func_ref(store).as_ptr().cast() 273 } 274} 275 276pub(super) unsafe fn do_call( 277 store: &mut StoreOpaque, 278 func_ref: &VMFuncRef, 279 params_and_results: &mut [VMVal], 280) -> crate::Result<()> { 281 // Safety: TODO 282 unsafe { 283 let span = tracing::trace_span!("WASM"); 284 285 span.in_scope(|| { 286 let exit = enter_wasm(store); 287 let res = crate::wasm::trap_handler::catch_traps(store, |caller| { 288 tracing::trace!("calling VMFuncRef array call"); 289 let success = func_ref.array_call( 290 VMOpaqueContext::from_vmcontext(caller), 291 NonNull::from(params_and_results), 292 ); 293 tracing::trace!(success, "returned from VMFuncRef array call"); 294 }); 295 exit_wasm(store, exit); 296 297 match res { 298 Ok(()) => Ok(()), 299 Err(Trap { reason, backtrace }) => { 300 let error = match reason { 301 TrapReason::User(err) => err, 302 TrapReason::Jit { 303 pc, 304 faulting_addr, 305 trap, 306 } => { 307 let mut err: anyhow::Error = trap.into(); 308 if let Some(fault) = 309 faulting_addr.and_then(|addr| store.wasm_fault(pc, addr)) 310 { 311 err = err.context(fault); 312 } 313 err 314 } 315 TrapReason::Wasm(trap_code) => trap_code.into(), 316 }; 317 318 if let Some(bt) = backtrace { 319 tracing::debug!("TODO properly format wasm backtrace {bt:?}"); 320 321 // let bt = WasmBacktrace::from_captured(store, bt, pc); 322 // if !bt.wasm_trace.is_empty() { 323 // error = error.context(bt); 324 // } 325 } 326 327 Err(error) 328 } 329 } 330 }) 331 } 332} 333 334/// This function is called to register state within `Store` whenever 335/// WebAssembly is entered within the `Store`. 336/// 337/// This function sets up various limits such as: 338/// 339/// * The stack limit. This is what ensures that we limit the stack space 340/// allocated by WebAssembly code and it's relative to the initial stack 341/// pointer that called into wasm. 342/// 343/// This function may fail if the stack limit can't be set because an 344/// interrupt already happened. 345fn enter_wasm(store: &mut StoreOpaque) -> Option<VirtualAddress> { 346 // If this is a recursive call, e.g. our stack limit is already set, then 347 // we may be able to skip this function. 348 // 349 // // For synchronous stores there's nothing else to do because all wasm calls 350 // // happen synchronously and on the same stack. This means that the previous 351 // // stack limit will suffice for the next recursive call. 352 // // 353 // // For asynchronous stores then each call happens on a separate native 354 // // stack. This means that the previous stack limit is no longer relevant 355 // // because we're on a separate stack. 356 // if unsafe { *store.vm_store_context().stack_limit.get() } != VirtualAddress::MAX 357 // && !store.async_support() 358 // { 359 // return None; 360 // } 361 362 // Ignore this stack pointer business on miri since we can't execute wasm 363 // anyway and the concept of a stack pointer on miri is a bit nebulous 364 // regardless. 365 if cfg!(miri) { 366 return None; 367 } 368 369 // When Cranelift has support for the host then we might be running native 370 // compiled code meaning we need to read the actual stack pointer. If 371 // Cranelift can't be used though then we're guaranteed to be running pulley 372 // in which case this stack pointer isn't actually used as Pulley has custom 373 // mechanisms for stack overflow. 374 let stack_pointer = arch::get_stack_pointer(); 375 376 // Determine the stack pointer where, after which, any wasm code will 377 // immediately trap. This is checked on the entry to all wasm functions. 378 // 379 // Note that this isn't 100% precise. We are requested to give wasm 380 // `max_wasm_stack` bytes, but what we're actually doing is giving wasm 381 // probably a little less than `max_wasm_stack` because we're 382 // calculating the limit relative to this function's approximate stack 383 // pointer. Wasm will be executed on a frame beneath this one (or next 384 // to it). In any case it's expected to be at most a few hundred bytes 385 // of slop one way or another. When wasm is typically given a MB or so 386 // (a million bytes) the slop shouldn't matter too much. 387 // 388 // After we've got the stack limit then we store it into the `stack_limit` 389 // variable. 390 let wasm_stack_limit = VirtualAddress::new(stack_pointer - MAX_WASM_STACK).unwrap(); 391 392 // Safety: the VMStoreContext is always properly initialized 393 let prev_stack = unsafe { 394 mem::replace( 395 &mut *store.vm_store_context().stack_limit.get(), 396 wasm_stack_limit, 397 ) 398 }; 399 400 Some(prev_stack) 401} 402 403fn exit_wasm(store: &mut StoreOpaque, prev_stack: Option<VirtualAddress>) { 404 // If we don't have a previous stack pointer to restore, then there's no 405 // cleanup we need to perform here. 406 let Some(prev_stack) = prev_stack else { 407 return; 408 }; 409 410 // Safety: the VMStoreContext is always properly initialized 411 unsafe { 412 *store.vm_store_context().stack_limit.get() = prev_stack; 413 } 414}