Skip to main content

submilli_engine/runtime/
finalizers.rs

1//! Invokes a package's reserved `finalizer` for an embedder, with the argument
2//! registered for it, restored and checked by the package itself.
3
4use wasmtime::{Linker, Store, Val};
5
6use crate::backtrace::ThrownError;
7use crate::runtime::host::Denial;
8use crate::runtime::json::JsonUnknownAllocator;
9use crate::runtime::{LinkedPackageModule, StoreData, exec, install_package_modules_async};
10
11/// One registered cleanup: the package that registered it and the key and
12/// value it registered, as the UTF-16 units the session finalizer provider
13/// received.
14#[derive(Clone, Copy, Debug)]
15pub struct FinalizerCall<'a> {
16    pub package: &'a str,
17    pub key: &'a [u16],
18    /// The JSON text registration serialized the value to.
19    pub value: &'a [u16],
20}
21
22#[derive(Debug)]
23pub enum FinalizerError {
24    /// The package is not among those linked, exports no valid `finalizer`, or
25    /// was built by an engine that does not export its argument check.
26    Unavailable(String),
27    /// The stored value is not JSON.
28    MalformedArgument(String),
29    /// The stored value does not fit the finalizer's parameter type, as
30    /// `value as T` would reject it.
31    IncompatibleArgument(String),
32    /// A runtime denial escaped the cleanup.
33    Denied(Denial),
34    /// Fuel, the deadline or memory ran out.
35    BudgetExhausted(wasmtime::Error),
36    /// An initializer or the finalizer threw or trapped.
37    Failed(wasmtime::Error),
38}
39
40impl std::fmt::Display for FinalizerError {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::Unavailable(message) => write!(f, "finalizer unavailable: {message}"),
44            Self::MalformedArgument(message) => {
45                write!(f, "malformed finalizer argument: {message}")
46            }
47            Self::IncompatibleArgument(message) => {
48                write!(f, "incompatible finalizer argument: {message}")
49            }
50            Self::Denied(denial) => write!(
51                f,
52                "finalizer denied `{}` for `{}`",
53                denial.capability, denial.caller
54            ),
55            Self::BudgetExhausted(error) => write!(f, "finalizer budget exhausted: {error}"),
56            Self::Failed(error) => write!(f, "finalizer failed: {error}"),
57        }
58    }
59}
60
61impl std::error::Error for FinalizerError {}
62
63/// Runs `call.package`'s `finalizer(key, value)` in a fresh store.
64///
65/// `store` must be unused: this installs the runtime and `packages`, the
66/// package and its dependencies at the versions the caller chose, then runs
67/// their initializers. The store is in finalizing mode from
68/// the start, so neither initializers nor the finalizer can change finalizer
69/// registrations. Calls are attributed to the package whose code makes them,
70/// as in any run.
71///
72/// The caller keeps what bounds the run: the store's fuel, memory and output,
73/// a deadline ([`RuntimeConfig::arm_timeout`](crate::runtime::RuntimeConfig::arm_timeout)
74/// with a timeout around this future), and the host services and capability
75/// checks in the store's data. Whether this future completes or is dropped,
76/// the caller must then drain `store.data_mut().blocking_work`.
77///
78/// Every thread polling this future must meet
79/// [`RuntimeConfig::native_stack_size`](crate::runtime::RuntimeConfig::native_stack_size)'s
80/// caller contract.
81pub async fn invoke_finalizer_async(
82    store: &mut Store<StoreData>,
83    packages: &[LinkedPackageModule<'_>],
84    call: FinalizerCall<'_>,
85) -> Result<(), FinalizerError> {
86    store.data_mut().finalizing = true;
87    // Fail before any initializer runs when there is nothing to call. A
88    // package only builds with a valid `finalizer` signature.
89    let declaration = packages
90        .iter()
91        .map(|package| package.declaration)
92        .find(|declaration| declaration.package_name == call.package)
93        .ok_or_else(|| {
94            FinalizerError::Unavailable(format!("package `{}` is not linked", call.package))
95        })?;
96    if !declaration.values.contains_key("finalizer") {
97        return Err(FinalizerError::Unavailable(format!(
98            "package `{}` exports no finalizer",
99            call.package
100        )));
101    }
102    let value = crate::runtime::json_text::parse(call.value)
103        .map_err(|error| FinalizerError::MalformedArgument(error.message().to_owned()))?;
104
105    let mut linker = Linker::new(store.engine());
106    crate::runtime::install_runtime_async(&mut linker, store)
107        .await
108        .map_err(classify)?;
109    install_package_modules_async(&mut linker, store, packages)
110        .await
111        .map_err(classify)?;
112
113    let export = |store: &mut Store<StoreData>, name: &str| match linker.get(
114        &mut *store,
115        call.package,
116        name,
117    ) {
118        Ok(wasmtime::Extern::Func(function)) => Ok(function),
119        _ => Err(FinalizerError::Unavailable(format!(
120            "package `{}` does not export `{name}`; rebuild it",
121            call.package
122        ))),
123    };
124    let check = export(
125        store,
126        crate::mangle::finalizer_argument(call.package).as_str(),
127    )?;
128    let finalizer = export(
129        store,
130        crate::mangle::package_symbol(call.package, "finalizer").as_str(),
131    )?;
132
133    let allocator = JsonUnknownAllocator::new(store).map_err(classify)?;
134    let unknown = allocator.allocate(store, &value).map_err(classify)?;
135    let key = allocator
136        .allocate(
137            store,
138            &crate::runtime::json_text::JsonValue::String(call.key.to_vec()),
139        )
140        .map_err(classify)?;
141
142    let mut argument = [Val::null_any_ref()];
143    let checked = check
144        .call_async(&mut *store, &[unknown], &mut argument)
145        .await;
146    checked.map_err(|error| match classify(exec::uncaught_error(store, error)) {
147        FinalizerError::Failed(error) if error.is::<ThrownError>() => {
148            FinalizerError::IncompatibleArgument(error.to_string())
149        }
150        other => other,
151    })?;
152
153    let [argument] = argument;
154    // Package exports use the erased ABI: boxed parameters and one boxed
155    // result, `undefined` for a `void` function.
156    let mut returned = vec![Val::null_any_ref(); finalizer.ty(&*store).results().len()];
157    let ran = finalizer
158        .call_async(&mut *store, &[key, argument], &mut returned)
159        .await;
160    ran.map_err(|error| classify(exec::uncaught_error(store, error)))
161}
162
163fn classify(error: wasmtime::Error) -> FinalizerError {
164    if matches!(
165        error.downcast_ref::<wasmtime::Trap>(),
166        Some(wasmtime::Trap::OutOfFuel | wasmtime::Trap::Interrupt)
167    ) || crate::runtime::is_memory_exhausted(&error)
168    {
169        return FinalizerError::BudgetExhausted(error);
170    }
171    if let Some(denial) = error
172        .downcast_ref::<ThrownError>()
173        .and_then(|thrown| thrown.denial.clone())
174    {
175        return FinalizerError::Denied(denial);
176    }
177    FinalizerError::Failed(error)
178}