Skip to main content

wasmtime_environ/component/
thread_transparency.rs

1//! Static analysis of "thread transparency" for fused adapters.
2//!
3//! Every fused sync adapter normally brackets its callee with
4//! `enter-sync-call`/`exit-sync-call`. That pair saves the caller's
5//! component-model thread state (`current_thread`, the context slots, the "may
6//! block" flag, etc...) and installs fresh state for the callee, restoring the
7//! caller's state on the way back out.
8//!
9//! However, that bracketing is only necessary if the callee can observe that
10//! thread state. If it cannot, the callee may simply run on top of whatever
11//! thread state the caller left in place, and the adapter can skip the
12//! save/restore entirely. We call such an adapter "thread transparent".
13//!
14//! Core Wasm state cannot be shared between components, so the only way core
15//! Wasm can touch component-model thread state is by calling a `canon lower`ed
16//! function or a canonical built-in. All of those are declared by a particular
17//! component instance and are only reachable from core Wasm belonging to that
18//! same component instance.
19//!
20//! Therefore this analysis works per component instance: an adapter whose
21//! callee instance declares nothing capable of touching thread state is
22//! transparent.
23//!
24//! Note that (lack of) transparency is not transitive across component
25//! instances because a call into an opaque instance will itself save/restore
26//! the thread state, even if none of its transparent callers needed to.
27
28use crate::component::dfg::{
29    AdapterId, CanonicalOptionsDataModel, ComponentDfg, CoreDef, Export, Instance, SideEffect,
30    Trampoline,
31};
32use crate::component::{
33    ComponentTypesBuilder, DataModel, RuntimeComponentInstanceIndex, UnsafeIntrinsic,
34};
35use cranelift_entity::EntitySet;
36
37/// Something a component instance makes callable by its core Wasm instances.
38#[derive(Clone, Copy, PartialEq, Eq, Debug)]
39enum CoreCallable {
40    /// A `canon lower` of a function imported from the host.
41    ///
42    /// The host can do anything at all, including reading and writing the
43    /// current thread's context slots.
44    HostImport,
45
46    /// A `canon lower` of a guest function that was `canon lift`ed elsewhere.
47    Adapter {
48        /// Whether the function on the lifting side uses the `async` ABI.
49        async_lift: bool,
50    },
51
52    /// A `canon context.get` or `canon context.set`.
53    ContextAccess,
54
55    /// Any other canonical built-in that reads or writes thread state:
56    /// `resource.*`, `backpressure.*`, `task.*`, `waitable*.*`, `subtask.*`,
57    /// `stream.*`, `future.*`, `error-context.*`, or `thread.*`.
58    ThreadStateBuiltin,
59
60    /// Something that cannot touch thread state at all: a plain core function,
61    /// a string transcoder, a non-context Wasmtime intrinsic, and so on.
62    Inert,
63}
64
65impl CoreCallable {
66    /// Can calling this reach code that reads or writes component-model thread
67    /// state?
68    fn may_touch_thread_state(self) -> bool {
69        match self {
70            // The host is unconstrained.
71            CoreCallable::HostImport => true,
72
73            // A sync-lifted callee is reached through its own fused adapter,
74            // which saves and restores whatever it needs; nothing about that
75            // call is visible in this instance's thread state.
76            //
77            // An async-lifted callee is different: it may block, and if it
78            // does the scheduler has to find the sync-typed call in progress
79            // on this instance, which means this instance needs real thread
80            // state of its own.
81            CoreCallable::Adapter { async_lift } => async_lift,
82
83            CoreCallable::ContextAccess | CoreCallable::ThreadStateBuiltin => true,
84
85            CoreCallable::Inert => false,
86        }
87    }
88}
89
90/// Everything about one fused adapter that bears on whether it can skip its
91/// `{enter,exit}-sync-call` window.
92#[derive(Clone, Copy, PartialEq, Eq, Debug)]
93struct AdapterFacts {
94    /// The component instance whose core Wasm this adapter calls into, i.e.
95    /// the instance that did the `canon lift`.
96    callee: RuntimeComponentInstanceIndex,
97
98    /// Whether either side of this adapter is `async` in either its canonical
99    /// options or function signature.
100    any_async: bool,
101
102    /// Whether either function signature mentions a handle.
103    any_handle: bool,
104}
105
106/// A builder for a `ThreadTransparency`.
107///
108/// Accumulates the observations that ultimately produce the
109/// `ThreadTransparency` analysis.
110#[derive(Default)]
111struct ThreadTransparencyBuilder {
112    /// The component instances observed to be able to touch thread state.
113    opaque: EntitySet<RuntimeComponentInstanceIndex>,
114}
115
116impl ThreadTransparencyBuilder {
117    /// Record that `instance` makes `callable` available to its core Wasm.
118    fn observe(&mut self, instance: RuntimeComponentInstanceIndex, callable: CoreCallable) {
119        if callable.may_touch_thread_state() {
120            self.opaque.insert(instance);
121        }
122    }
123
124    /// Finish observing and produce the queryable analysis results.
125    fn finish(self) -> ThreadTransparency {
126        ThreadTransparency {
127            opaque: self.opaque,
128        }
129    }
130}
131
132/// A completed thread-transparency analysis, ready to be queried.
133struct ThreadTransparency {
134    /// The component instances that can touch thread state.
135    opaque: EntitySet<RuntimeComponentInstanceIndex>,
136}
137
138impl ThreadTransparency {
139    /// May the adapter described by `facts` omit its `{enter,exit}-sync-call`
140    /// window?
141    fn adapter_is_transparent(&self, facts: AdapterFacts) -> bool {
142        // Async adapters genuinely need thread state of their own.
143        if facts.any_async {
144            return false;
145        }
146
147        // Transferring handles across an instance boundary unconditionally
148        // requires thread state for now.
149        if facts.any_handle {
150            return false;
151        }
152
153        !self.opaque.contains(facts.callee)
154    }
155}
156
157/// Run the thread-transparency analysis over `dfg`, returning the set of
158/// adapters that may skip their `{enter,exit}-sync-call` window.
159pub fn transparent_adapters(
160    dfg: &ComponentDfg,
161    types: &ComponentTypesBuilder,
162) -> EntitySet<AdapterId> {
163    let mut builder = ThreadTransparencyBuilder::default();
164
165    // Observe canonical built-ins.
166    for (_, (_, trampoline)) in dfg.trampolines.iter() {
167        let (instance, callable) = trampoline_callable(dfg, trampoline);
168        builder.observe(instance, callable);
169    }
170
171    // Observe fused adapters.
172    for (_, adapter) in dfg.adapters.iter() {
173        let async_lift = types[adapter.lift_ty].async_;
174        builder.observe(
175            adapter.lower_options.instance,
176            CoreCallable::Adapter { async_lift },
177        );
178    }
179
180    // Observe `CoreDef`s.
181    for_each_core_def(dfg, |instance, def| {
182        builder.observe(instance, core_def_callable(def));
183    });
184
185    // Finish the analysis and query its results to determine the set of
186    // adapters that are transparent.
187    let analysis = builder.finish();
188    let mut transparent = EntitySet::new();
189    for (id, adapter) in dfg.adapters.iter() {
190        if analysis.adapter_is_transparent(facts(types, adapter)) {
191            transparent.insert(id);
192        }
193    }
194    transparent
195}
196
197fn facts(types: &ComponentTypesBuilder, adapter: &super::Adapter) -> AdapterFacts {
198    AdapterFacts {
199        callee: adapter.lift_options.instance,
200        any_async: adapter.lift_options.async_
201            || adapter.lower_options.async_
202            || types[adapter.lift_ty].async_
203            || types[adapter.lower_ty].async_,
204        any_handle: types.func_contains_any_handle(adapter.lift_ty)
205            || types.func_contains_any_handle(adapter.lower_ty),
206    }
207}
208
209/// Classify a `CoreDef` that some component instance references.
210fn core_def_callable(def: &CoreDef) -> CoreCallable {
211    // NB: deliberately exhaustive so that new variants must be classified here.
212    match def {
213        CoreDef::UnsafeIntrinsic(_, intrinsic) => match intrinsic {
214            UnsafeIntrinsic::ContextGetI32_0
215            | UnsafeIntrinsic::ContextSetI32_0
216            | UnsafeIntrinsic::ContextGetI32_1
217            | UnsafeIntrinsic::ContextSetI32_1 => CoreCallable::ContextAccess,
218
219            // Unsafe intrinsics can't access thread state.
220            UnsafeIntrinsic::StoreDataAddress
221            | UnsafeIntrinsic::U8NativeLoad
222            | UnsafeIntrinsic::U8NativeStore
223            | UnsafeIntrinsic::U16NativeLoad
224            | UnsafeIntrinsic::U16NativeStore
225            | UnsafeIntrinsic::U32NativeLoad
226            | UnsafeIntrinsic::U32NativeStore
227            | UnsafeIntrinsic::U64NativeLoad
228            | UnsafeIntrinsic::U64NativeStore
229            | UnsafeIntrinsic::U8CheckedNativeLoad
230            | UnsafeIntrinsic::U8CheckedNativeStore
231            | UnsafeIntrinsic::U16CheckedNativeLoad
232            | UnsafeIntrinsic::U16CheckedNativeStore
233            | UnsafeIntrinsic::U32CheckedNativeLoad
234            | UnsafeIntrinsic::U32CheckedNativeStore
235            | UnsafeIntrinsic::U64CheckedNativeLoad
236            | UnsafeIntrinsic::U64CheckedNativeStore => CoreCallable::Inert,
237        },
238
239        // Trampolines and adapters are observed directly in
240        // `transparent_adapters`, so we don't need to worry about them here.
241        CoreDef::Trampoline(_) | CoreDef::Adapter(_) => CoreCallable::Inert,
242
243        // Plain core Wasm; can't introduce any state-access capability that
244        // isn't already there.
245        CoreDef::Export(_) | CoreDef::InstanceFlags(_) => CoreCallable::Inert,
246    }
247}
248
249/// Classify a trampoline, and identify the component instance that declared it.
250fn trampoline_callable(
251    dfg: &ComponentDfg,
252    trampoline: &Trampoline,
253) -> (RuntimeComponentInstanceIndex, CoreCallable) {
254    use Trampoline::*;
255
256    // NB: deliberately exhaustive so that new variants must be classified here.
257    match trampoline {
258        LowerImport { options, .. } => (dfg.options[*options].instance, CoreCallable::HostImport),
259
260        ResourceNew { instance, .. }
261        | ResourceRep { instance, .. }
262        | ResourceDrop { instance, .. }
263        | BackpressureInc { instance }
264        | BackpressureDec { instance }
265        | TaskReturn { instance, .. }
266        | TaskCancel { instance }
267        | WaitableSetNew { instance }
268        | WaitableSetWait { instance, .. }
269        | WaitableSetPoll { instance, .. }
270        | WaitableSetDrop { instance }
271        | WaitableJoin { instance }
272        | SubtaskDrop { instance }
273        | SubtaskCancel { instance, .. }
274        | StreamNew { instance, .. }
275        | StreamRead { instance, .. }
276        | StreamWrite { instance, .. }
277        | StreamCancelRead { instance, .. }
278        | StreamCancelWrite { instance, .. }
279        | StreamDropReadable { instance, .. }
280        | StreamDropWritable { instance, .. }
281        | FutureNew { instance, .. }
282        | FutureRead { instance, .. }
283        | FutureWrite { instance, .. }
284        | FutureCancelRead { instance, .. }
285        | FutureCancelWrite { instance, .. }
286        | FutureDropReadable { instance, .. }
287        | FutureDropWritable { instance, .. }
288        | ErrorContextNew { instance, .. }
289        | ErrorContextDebugMessage { instance, .. }
290        | ErrorContextDrop { instance, .. }
291        | ThreadIndex { instance }
292        | ThreadNewIndirect { instance, .. }
293        | ThreadResumeLater { instance }
294        | ThreadSuspend { instance, .. }
295        | ThreadYield { instance, .. }
296        | ThreadSuspendThenResume { instance, .. }
297        | ThreadYieldThenResume { instance, .. }
298        | ThreadSuspendThenPromote { instance, .. }
299        | ThreadYieldThenPromote { instance, .. } => (*instance, CoreCallable::ThreadStateBuiltin),
300
301        // These trampolines are only ever created in `translate::adapt`, which
302        // happens strictly after this analysis has run. Therefore none of them
303        // can exist yet.
304        Transcoder { .. }
305        | ResourceTransferOwn
306        | ResourceTransferBorrow
307        | PrepareCall { .. }
308        | SyncStartCall { .. }
309        | AsyncStartCall { .. }
310        | FutureTransfer
311        | StreamTransfer
312        | ErrorContextTransfer
313        | Trap(_)
314        | EnterSyncCall
315        | ExitSyncCall => {
316            unreachable!("these trampolines do not exist yet")
317        }
318    }
319}
320
321/// Visit every `CoreDef` in `dfg` together with the component instance that
322/// references it.
323fn for_each_core_def(
324    dfg: &ComponentDfg,
325    mut f: impl FnMut(RuntimeComponentInstanceIndex, &CoreDef),
326) {
327    for effect in dfg.side_effects.iter() {
328        let SideEffect::Instance(id, instance) = effect else {
329            continue;
330        };
331        match &dfg.instances[*id] {
332            Instance::Static(_, args) => {
333                for def in args.iter() {
334                    f(*instance, def);
335                }
336            }
337            Instance::Import(_, args) => {
338                for (_, defs) in args {
339                    for (_, def) in defs {
340                        f(*instance, def);
341                    }
342                }
343            }
344        }
345    }
346
347    for (_, adapter) in dfg.adapters.iter() {
348        f(adapter.lift_options.instance, &adapter.func);
349        for options in [&adapter.lift_options, &adapter.lower_options] {
350            for def in options
351                .callback
352                .iter()
353                .chain(options.post_return.iter())
354                .chain(match &options.data_model {
355                    DataModel::LinearMemory { realloc, .. } => realloc.iter(),
356                    DataModel::Gc {} => None.iter(),
357                })
358            {
359                f(options.instance, def);
360            }
361        }
362    }
363
364    for (_, options) in dfg.options.iter() {
365        if let Some(callback) = options.callback {
366            f(options.instance, &dfg.callbacks[callback]);
367        }
368        if let Some(post_return) = options.post_return {
369            f(options.instance, &dfg.post_returns[post_return]);
370        }
371        if let CanonicalOptionsDataModel::LinearMemory {
372            realloc: Some(realloc),
373            ..
374        } = &options.data_model
375        {
376            f(options.instance, &dfg.reallocs[*realloc]);
377        }
378    }
379
380    for (_, resource) in dfg.resources.iter() {
381        if let Some(dtor) = &resource.dtor {
382            f(resource.instance, dtor);
383        }
384    }
385
386    for (_, (export, _)) in dfg.exports.iter() {
387        for_each_export_core_def(dfg, export, &mut f);
388    }
389}
390
391fn for_each_export_core_def(
392    dfg: &ComponentDfg,
393    export: &Export,
394    f: &mut impl FnMut(RuntimeComponentInstanceIndex, &CoreDef),
395) {
396    match export {
397        Export::LiftedFunction { func, options, .. } => {
398            f(dfg.options[*options].instance, func);
399        }
400        Export::Instance { exports, .. } => {
401            for (_, (export, _)) in exports.iter() {
402                for_each_export_core_def(dfg, export, f);
403            }
404        }
405        Export::ModuleStatic { .. } | Export::ModuleImport { .. } | Export::Type(_) => {}
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    fn instance(i: u32) -> RuntimeComponentInstanceIndex {
414        RuntimeComponentInstanceIndex::from_u32(i)
415    }
416
417    /// Run the build phase over `observations`, given as `(instance, callable)`
418    /// pairs, and finish it into queryable results.
419    fn analyze(observations: &[(u32, CoreCallable)]) -> ThreadTransparency {
420        let mut builder = ThreadTransparencyBuilder::default();
421        for (i, callable) in observations {
422            builder.observe(instance(*i), *callable);
423        }
424        builder.finish()
425    }
426
427    /// An adapter calling into instance `callee` with a plain, handle-free,
428    /// sync signature.
429    fn facts(callee: u32) -> AdapterFacts {
430        AdapterFacts {
431            callee: instance(callee),
432            any_async: false,
433            any_handle: false,
434        }
435    }
436
437    #[test]
438    fn clean_instance_is_transparent() {
439        let t = analyze(&[]);
440        assert!(t.adapter_is_transparent(facts(0)));
441    }
442
443    #[test]
444    fn context_access_makes_instance_opaque() {
445        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
446        assert!(!t.adapter_is_transparent(facts(0)));
447    }
448
449    #[test]
450    fn thread_state_builtin_makes_instance_opaque() {
451        let t = analyze(&[(0, CoreCallable::ThreadStateBuiltin)]);
452        assert!(!t.adapter_is_transparent(facts(0)));
453    }
454
455    #[test]
456    fn host_import_lowering_makes_instance_opaque() {
457        let t = analyze(&[(0, CoreCallable::HostImport)]);
458        assert!(!t.adapter_is_transparent(facts(0)));
459    }
460
461    #[test]
462    fn inert_callable_leaves_instance_transparent() {
463        let t = analyze(&[(0, CoreCallable::Inert), (0, CoreCallable::Inert)]);
464        assert!(t.adapter_is_transparent(facts(0)));
465    }
466
467    /// Lowering a sync-lifted guest function is just another adapter, and that
468    /// adapter does its own save/restore, so it does not taint the instance
469    /// doing the lowering.
470    #[test]
471    fn sync_adapter_lowering_leaves_instance_transparent() {
472        let t = analyze(&[(0, CoreCallable::Adapter { async_lift: false })]);
473        assert!(t.adapter_is_transparent(facts(0)));
474    }
475
476    /// Lowering an async-lifted guest function does taint the lowering
477    /// instance: the callee may block, and then the scheduler needs to find
478    /// this instance's sync call in progress.
479    #[test]
480    fn async_lift_makes_the_lowering_instance_opaque() {
481        let t = analyze(&[(0, CoreCallable::Adapter { async_lift: true })]);
482        assert!(!t.adapter_is_transparent(facts(0)));
483    }
484
485    /// Only the callee side is judged: an opaque caller calling into a clean
486    /// callee still gets a transparent adapter.
487    #[test]
488    fn only_the_callee_side_is_judged() {
489        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
490        assert!(t.adapter_is_transparent(facts(1)));
491    }
492
493    /// Opacity does not propagate outward: in a chain `outer -> mid -> inner`
494    /// where only `inner` is opaque, the `outer -> mid` adapter is still
495    /// transparent because the `mid -> inner` call has its own adapter.
496    #[test]
497    fn opacity_does_not_propagate_outward() {
498        let t = analyze(&[(2, CoreCallable::ContextAccess)]);
499        assert!(t.adapter_is_transparent(facts(1)), "outer -> mid");
500        assert!(!t.adapter_is_transparent(facts(2)), "mid -> inner");
501    }
502
503    /// Nor inward: an opaque `outer` calling a clean `mid` calling a clean
504    /// `inner` leaves both adapters transparent.
505    #[test]
506    fn opacity_does_not_propagate_inward() {
507        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
508        assert!(t.adapter_is_transparent(facts(1)), "outer -> mid");
509        assert!(t.adapter_is_transparent(facts(2)), "mid -> inner");
510    }
511
512    /// An unrelated sibling instance being opaque taints nobody else.
513    #[test]
514    fn siblings_are_judged_independently() {
515        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
516        assert!(!t.adapter_is_transparent(facts(0)));
517        assert!(t.adapter_is_transparent(facts(1)));
518    }
519
520    #[test]
521    fn async_signature_is_never_transparent() {
522        let t = analyze(&[]);
523        assert!(!t.adapter_is_transparent(AdapterFacts {
524            any_async: true,
525            ..facts(0)
526        }));
527    }
528
529    #[test]
530    fn handle_in_signature_is_never_transparent() {
531        let t = analyze(&[]);
532        assert!(!t.adapter_is_transparent(AdapterFacts {
533            any_handle: true,
534            ..facts(0)
535        }));
536    }
537
538    /// The build phase is monotone, so observations may arrive in any order and
539    /// a clean observation can never undo an opaque one.
540    #[test]
541    fn observations_are_monotone_and_order_independent() {
542        let dirty_first = analyze(&[
543            (0, CoreCallable::ContextAccess),
544            (0, CoreCallable::Inert),
545            (0, CoreCallable::Adapter { async_lift: false }),
546        ]);
547        let dirty_last = analyze(&[
548            (0, CoreCallable::Adapter { async_lift: false }),
549            (0, CoreCallable::Inert),
550            (0, CoreCallable::ContextAccess),
551        ]);
552        assert!(!dirty_first.adapter_is_transparent(facts(0)));
553        assert!(!dirty_last.adapter_is_transparent(facts(0)));
554    }
555}