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        | StartCall { .. }
309        | FutureTransfer
310        | StreamTransfer
311        | ErrorContextTransfer
312        | Trap(_)
313        | EnterSyncCall
314        | ExitSyncCall => {
315            unreachable!("these trampolines do not exist yet")
316        }
317    }
318}
319
320/// Visit every `CoreDef` in `dfg` together with the component instance that
321/// references it.
322fn for_each_core_def(
323    dfg: &ComponentDfg,
324    mut f: impl FnMut(RuntimeComponentInstanceIndex, &CoreDef),
325) {
326    for effect in dfg.side_effects.iter() {
327        let SideEffect::Instance(id, instance) = effect else {
328            continue;
329        };
330        match &dfg.instances[*id] {
331            Instance::Static(_, args) => {
332                for def in args.iter() {
333                    f(*instance, def);
334                }
335            }
336            Instance::Import(_, args) => {
337                for (_, defs) in args {
338                    for (_, def) in defs {
339                        f(*instance, def);
340                    }
341                }
342            }
343        }
344    }
345
346    for (_, adapter) in dfg.adapters.iter() {
347        f(adapter.lift_options.instance, &adapter.func);
348        for options in [&adapter.lift_options, &adapter.lower_options] {
349            for def in options
350                .callback
351                .iter()
352                .chain(options.post_return.iter())
353                .chain(match &options.data_model {
354                    DataModel::LinearMemory { realloc, .. } => realloc.iter(),
355                    DataModel::Gc {} => None.iter(),
356                })
357            {
358                f(options.instance, def);
359            }
360        }
361    }
362
363    for (_, options) in dfg.options.iter() {
364        if let Some(callback) = options.callback {
365            f(options.instance, &dfg.callbacks[callback]);
366        }
367        if let Some(post_return) = options.post_return {
368            f(options.instance, &dfg.post_returns[post_return]);
369        }
370        if let CanonicalOptionsDataModel::LinearMemory {
371            realloc: Some(realloc),
372            ..
373        } = &options.data_model
374        {
375            f(options.instance, &dfg.reallocs[*realloc]);
376        }
377    }
378
379    for (_, resource) in dfg.resources.iter() {
380        if let Some(dtor) = &resource.dtor {
381            f(resource.instance, dtor);
382        }
383    }
384
385    for (_, (export, _)) in dfg.exports.iter() {
386        for_each_export_core_def(dfg, export, &mut f);
387    }
388}
389
390fn for_each_export_core_def(
391    dfg: &ComponentDfg,
392    export: &Export,
393    f: &mut impl FnMut(RuntimeComponentInstanceIndex, &CoreDef),
394) {
395    match export {
396        Export::LiftedFunction { func, options, .. } => {
397            f(dfg.options[*options].instance, func);
398        }
399        Export::Instance { exports, .. } => {
400            for (_, (export, _)) in exports.iter() {
401                for_each_export_core_def(dfg, export, f);
402            }
403        }
404        Export::ModuleStatic { .. } | Export::ModuleImport { .. } | Export::Type(_) => {}
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    fn instance(i: u32) -> RuntimeComponentInstanceIndex {
413        RuntimeComponentInstanceIndex::from_u32(i)
414    }
415
416    /// Run the build phase over `observations`, given as `(instance, callable)`
417    /// pairs, and finish it into queryable results.
418    fn analyze(observations: &[(u32, CoreCallable)]) -> ThreadTransparency {
419        let mut builder = ThreadTransparencyBuilder::default();
420        for (i, callable) in observations {
421            builder.observe(instance(*i), *callable);
422        }
423        builder.finish()
424    }
425
426    /// An adapter calling into instance `callee` with a plain, handle-free,
427    /// sync signature.
428    fn facts(callee: u32) -> AdapterFacts {
429        AdapterFacts {
430            callee: instance(callee),
431            any_async: false,
432            any_handle: false,
433        }
434    }
435
436    #[test]
437    fn clean_instance_is_transparent() {
438        let t = analyze(&[]);
439        assert!(t.adapter_is_transparent(facts(0)));
440    }
441
442    #[test]
443    fn context_access_makes_instance_opaque() {
444        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
445        assert!(!t.adapter_is_transparent(facts(0)));
446    }
447
448    #[test]
449    fn thread_state_builtin_makes_instance_opaque() {
450        let t = analyze(&[(0, CoreCallable::ThreadStateBuiltin)]);
451        assert!(!t.adapter_is_transparent(facts(0)));
452    }
453
454    #[test]
455    fn host_import_lowering_makes_instance_opaque() {
456        let t = analyze(&[(0, CoreCallable::HostImport)]);
457        assert!(!t.adapter_is_transparent(facts(0)));
458    }
459
460    #[test]
461    fn inert_callable_leaves_instance_transparent() {
462        let t = analyze(&[(0, CoreCallable::Inert), (0, CoreCallable::Inert)]);
463        assert!(t.adapter_is_transparent(facts(0)));
464    }
465
466    /// Lowering a sync-lifted guest function is just another adapter, and that
467    /// adapter does its own save/restore, so it does not taint the instance
468    /// doing the lowering.
469    #[test]
470    fn sync_adapter_lowering_leaves_instance_transparent() {
471        let t = analyze(&[(0, CoreCallable::Adapter { async_lift: false })]);
472        assert!(t.adapter_is_transparent(facts(0)));
473    }
474
475    /// Lowering an async-lifted guest function does taint the lowering
476    /// instance: the callee may block, and then the scheduler needs to find
477    /// this instance's sync call in progress.
478    #[test]
479    fn async_lift_makes_the_lowering_instance_opaque() {
480        let t = analyze(&[(0, CoreCallable::Adapter { async_lift: true })]);
481        assert!(!t.adapter_is_transparent(facts(0)));
482    }
483
484    /// Only the callee side is judged: an opaque caller calling into a clean
485    /// callee still gets a transparent adapter.
486    #[test]
487    fn only_the_callee_side_is_judged() {
488        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
489        assert!(t.adapter_is_transparent(facts(1)));
490    }
491
492    /// Opacity does not propagate outward: in a chain `outer -> mid -> inner`
493    /// where only `inner` is opaque, the `outer -> mid` adapter is still
494    /// transparent because the `mid -> inner` call has its own adapter.
495    #[test]
496    fn opacity_does_not_propagate_outward() {
497        let t = analyze(&[(2, CoreCallable::ContextAccess)]);
498        assert!(t.adapter_is_transparent(facts(1)), "outer -> mid");
499        assert!(!t.adapter_is_transparent(facts(2)), "mid -> inner");
500    }
501
502    /// Nor inward: an opaque `outer` calling a clean `mid` calling a clean
503    /// `inner` leaves both adapters transparent.
504    #[test]
505    fn opacity_does_not_propagate_inward() {
506        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
507        assert!(t.adapter_is_transparent(facts(1)), "outer -> mid");
508        assert!(t.adapter_is_transparent(facts(2)), "mid -> inner");
509    }
510
511    /// An unrelated sibling instance being opaque taints nobody else.
512    #[test]
513    fn siblings_are_judged_independently() {
514        let t = analyze(&[(0, CoreCallable::ContextAccess)]);
515        assert!(!t.adapter_is_transparent(facts(0)));
516        assert!(t.adapter_is_transparent(facts(1)));
517    }
518
519    #[test]
520    fn async_signature_is_never_transparent() {
521        let t = analyze(&[]);
522        assert!(!t.adapter_is_transparent(AdapterFacts {
523            any_async: true,
524            ..facts(0)
525        }));
526    }
527
528    #[test]
529    fn handle_in_signature_is_never_transparent() {
530        let t = analyze(&[]);
531        assert!(!t.adapter_is_transparent(AdapterFacts {
532            any_handle: true,
533            ..facts(0)
534        }));
535    }
536
537    /// The build phase is monotone, so observations may arrive in any order and
538    /// a clean observation can never undo an opaque one.
539    #[test]
540    fn observations_are_monotone_and_order_independent() {
541        let dirty_first = analyze(&[
542            (0, CoreCallable::ContextAccess),
543            (0, CoreCallable::Inert),
544            (0, CoreCallable::Adapter { async_lift: false }),
545        ]);
546        let dirty_last = analyze(&[
547            (0, CoreCallable::Adapter { async_lift: false }),
548            (0, CoreCallable::Inert),
549            (0, CoreCallable::ContextAccess),
550        ]);
551        assert!(!dirty_first.adapter_is_transparent(facts(0)));
552        assert!(!dirty_last.adapter_is_transparent(facts(0)));
553    }
554}