Skip to content

Commit e32036a

Browse files
committed
re-implement recursive reentrance checks
Now that the component model supports multiple tasks and cooperative threads running concurrently in the same instance, the old model of using a per-instance flag to track whether a component may be entered no longer works. Instead of keeping track of whether an instance has been entered at all, we must now keep track of whether an instance has been entered for each in-progress task. This commit removes `FLAG_MAY_ENTER` from `InstanceFlags`, relying instead on a per-task call stack maintained at runtime. When the `component-model-async` feature is enabled, we reuse the `GuestTask` stack for this purpose. When the `component-model-async` feature is disabled, there's just a single stack used for the entire store. Note that these stacks are only updated when crossing component boundaries -- not at every internal call within an instance. Each time we're about to enter a instance from either the host or the guest, we check the call stack, and if the instance is already present, we trap. Otherwise, we push a new element onto the stack and later pop it back off once the callee returns a value. In addition to trapping on recursive reentrance, we do a couple of additional checks for host-to-guest calls, for which we previously relied on `FLAG_MAY_ENTER`: - If _any_ instance owned by the store has previously trapped, we disallow entering that or any other instance owned by the store. - If a post-return call is needed for an instance, we disallow entering it until that call has been made. Note that, for sync-to-sync, guest-to-guest calls, the above process entails significantly more overhead than the prior code, which only involved checking and setting a flag and did not require calling into the host at all. I intend to follow this PR up with one or more optimizations to reduce that overhead. See the discussion of `trap_if_on_the_stack` in https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md for examples of such optimizations. In addition to the above refactoring, this commit does a couple of related things: - When a host function recursively calls back into guest code, we now chain the old call stack to the new one. This allows us to catch recursive reentrance which may span multiple top-level instances. - Previously, we did not push a new task on the stack for sync-to-sync, guest-to-guest calls, which meant we missed catching some violations of the sync-task-must-not-block-before-returning rule. Now that we are pushing a new task in that case, we catch all such violations, which means some of the existing WAST tests needed updating. bless disas/component-model tests optimize sync-to-sync, guest-to-guest calls We now omit recursive reentrance checks entirely when generating fused adapters for components which cannot possibly recursively reenter themselves, noting that components which only contain modules in leaf (sub)components fall into that category. However, we must still include a runtime global variable check-and-set to enforce the may-block rules for sync-typed tasks. With some more effort, we could also eliminate those for components statically known to never make blocking calls to intrinsics or imports. The implementation of the may-block check centers around a per-top-level-instance global variable called `task_may_block`, which we update each time we switch threads and tasks, as well as whenever `task.return` or `task.cancel` is called. This required shuffling some code around and creating a new `StoreOpaque::set_thread` function which encapsulates switching threads and updating `task_may_block` for the old and new instances. tweak rules to determine whether recursive reentrance is possible This accounts for components which instantiate imported modules and/or components.
1 parent 967f460 commit e32036a

39 files changed

+1441
-760
lines changed

crates/cranelift/src/compiler/component.rs

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Compilation support for the component model.
22
3-
use crate::{TRAP_ALWAYS, TRAP_CANNOT_ENTER, TRAP_INTERNAL_ASSERT, compiler::Compiler};
3+
use crate::{TRAP_ALWAYS, TRAP_INTERNAL_ASSERT, compiler::Compiler};
44
use cranelift_codegen::ir::condcodes::IntCC;
55
use cranelift_codegen::ir::{self, InstBuilder, MemFlags, Value};
66
use cranelift_codegen::isa::{CallConv, TargetIsa};
@@ -732,9 +732,17 @@ impl<'a> TrampolineCompiler<'a> {
732732
|_, _| {},
733733
);
734734
}
735-
Trampoline::CheckBlocking => {
735+
Trampoline::EnterSyncCall => {
736736
self.translate_libcall(
737-
host::check_blocking,
737+
host::enter_sync_call,
738+
TrapSentinel::Falsy,
739+
WasmArgs::InRegisters,
740+
|_, _| {},
741+
);
742+
}
743+
Trampoline::ExitSyncCall => {
744+
self.translate_libcall(
745+
host::exit_sync_call,
738746
TrapSentinel::Falsy,
739747
WasmArgs::InRegisters,
740748
|_, _| {},
@@ -1138,11 +1146,9 @@ impl<'a> TrampolineCompiler<'a> {
11381146
// brif should_run_destructor, run_destructor_block, return_block
11391147
//
11401148
// run_destructor_block:
1141-
// ;; test may_enter, but only if the component instances
1149+
// ;; call `enter_sync_call`, but only if the component instances
11421150
// ;; differ
1143-
// flags = load.i32 vmctx+$offset
1144-
// masked = band flags, $FLAG_MAY_ENTER
1145-
// trapz masked, CANNOT_ENTER_CODE
1151+
// ...
11461152
//
11471153
// ;; ============================================================
11481154
// ;; this is conditionally emitted based on whether the resource
@@ -1156,6 +1162,9 @@ impl<'a> TrampolineCompiler<'a> {
11561162
// call_indirect func_addr, callee_vmctx, vmctx, rep
11571163
// ;; ============================================================
11581164
//
1165+
// ;; call `exit_sync_call` if the component instances differ
1166+
// ...
1167+
//
11591168
// jump return_block
11601169
//
11611170
// return_block:
@@ -1186,25 +1195,32 @@ impl<'a> TrampolineCompiler<'a> {
11861195
self.builder.switch_to_block(run_destructor_block);
11871196

11881197
// If this is a defined resource within the component itself then a
1189-
// check needs to be emitted for the `may_enter` flag. Note though
1198+
// check needs to be emitted for recursive reentrance. Note though
11901199
// that this check can be elided if the resource table resides in
11911200
// the same component instance that defined the resource as the
11921201
// component is calling itself.
1193-
if let Some(def) = resource_def {
1202+
let emit_exit = if let Some(def) = resource_def {
11941203
if self.types[resource].unwrap_concrete_instance() != def.instance {
1195-
let flags = self.builder.ins().load(
1196-
ir::types::I32,
1197-
trusted,
1204+
let host_args = vec![
11981205
vmctx,
1199-
i32::try_from(self.offsets.instance_flags(def.instance)).unwrap(),
1200-
);
1201-
let masked = self
1202-
.builder
1203-
.ins()
1204-
.band_imm(flags, i64::from(FLAG_MAY_ENTER));
1205-
self.builder.ins().trapz(masked, TRAP_CANNOT_ENTER);
1206+
self.builder
1207+
.ins()
1208+
.iconst(ir::types::I32, i64::from(instance.as_u32())),
1209+
self.builder.ins().iconst(ir::types::I32, i64::from(0)),
1210+
self.builder
1211+
.ins()
1212+
.iconst(ir::types::I32, i64::from(def.instance.as_u32())),
1213+
];
1214+
let call = self.call_libcall(vmctx, host::enter_sync_call, &host_args);
1215+
let result = self.builder.func.dfg.inst_results(call).get(0).copied();
1216+
self.raise_if_host_trapped(result.unwrap());
1217+
true
1218+
} else {
1219+
false
12061220
}
1207-
}
1221+
} else {
1222+
false
1223+
};
12081224

12091225
// Conditionally emit destructor-execution code based on whether we
12101226
// statically know that a destructor exists or not.
@@ -1253,6 +1269,13 @@ impl<'a> TrampolineCompiler<'a> {
12531269
&[callee_vmctx, caller_vmctx, rep],
12541270
);
12551271
}
1272+
1273+
if emit_exit {
1274+
let call = self.call_libcall(vmctx, host::exit_sync_call, &[vmctx]);
1275+
let result = self.builder.func.dfg.inst_results(call).get(0).copied();
1276+
self.raise_if_host_trapped(result.unwrap());
1277+
}
1278+
12561279
self.builder.ins().jump(return_block, &[]);
12571280
self.builder.seal_block(run_destructor_block);
12581281

crates/environ/src/component.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ macro_rules! foreach_builtin_component_function {
9999
resource_enter_call(vmctx: vmctx);
100100
resource_exit_call(vmctx: vmctx) -> bool;
101101

102+
enter_sync_call(vmctx: vmctx, caller_instance: u32, callee_async: u32, callee_instance: u32) -> bool;
103+
exit_sync_call(vmctx: vmctx) -> bool;
104+
102105
#[cfg(feature = "component-model-async")]
103106
backpressure_modify(vmctx: vmctx, caller_instance: u32, increment: u8) -> bool;
104107
#[cfg(feature = "component-model-async")]
@@ -185,8 +188,6 @@ macro_rules! foreach_builtin_component_function {
185188
#[cfg(feature = "component-model-async")]
186189
error_context_transfer(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64;
187190
#[cfg(feature = "component-model-async")]
188-
check_blocking(vmctx: vmctx) -> bool;
189-
#[cfg(feature = "component-model-async")]
190191
context_get(vmctx: vmctx, caller_instance: u32, slot: u32) -> u64;
191192
#[cfg(feature = "component-model-async")]
192193
context_set(vmctx: vmctx, caller_instance: u32, slot: u32, val: u32) -> bool;

crates/environ/src/component/dfg.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ pub enum CoreDef {
267267
InstanceFlags(RuntimeComponentInstanceIndex),
268268
Trampoline(TrampolineIndex),
269269
UnsafeIntrinsic(ModuleInternedTypeIndex, UnsafeIntrinsic),
270+
TaskMayBlock,
270271

271272
/// This is a special variant not present in `info::CoreDef` which
272273
/// represents that this definition refers to a fused adapter function. This
@@ -475,8 +476,9 @@ pub enum Trampoline {
475476
FutureTransfer,
476477
StreamTransfer,
477478
ErrorContextTransfer,
478-
CheckBlocking,
479479
Trap,
480+
EnterSyncCall,
481+
ExitSyncCall,
480482
ContextGet {
481483
instance: RuntimeComponentInstanceIndex,
482484
slot: u32,
@@ -905,6 +907,7 @@ impl LinearizeDfg<'_> {
905907
}
906908
info::CoreDef::UnsafeIntrinsic(*i)
907909
}
910+
CoreDef::TaskMayBlock => info::CoreDef::TaskMayBlock,
908911
}
909912
}
910913

@@ -1156,8 +1159,9 @@ impl LinearizeDfg<'_> {
11561159
Trampoline::FutureTransfer => info::Trampoline::FutureTransfer,
11571160
Trampoline::StreamTransfer => info::Trampoline::StreamTransfer,
11581161
Trampoline::ErrorContextTransfer => info::Trampoline::ErrorContextTransfer,
1159-
Trampoline::CheckBlocking => info::Trampoline::CheckBlocking,
11601162
Trampoline::Trap => info::Trampoline::Trap,
1163+
Trampoline::EnterSyncCall => info::Trampoline::EnterSyncCall,
1164+
Trampoline::ExitSyncCall => info::Trampoline::ExitSyncCall,
11611165
Trampoline::ContextGet { instance, slot } => info::Trampoline::ContextGet {
11621166
instance: *instance,
11631167
slot: *slot,

crates/environ/src/component/info.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ pub enum CoreDef {
389389
Trampoline(TrampolineIndex),
390390
/// An intrinsic for compile-time builtins.
391391
UnsafeIntrinsic(UnsafeIntrinsic),
392+
/// Reference to a wasm global which represents a runtime-managed boolean
393+
/// indicating whether the currently-running task may perform a blocking
394+
/// operation.
395+
TaskMayBlock,
392396
}
393397

394398
impl<T> From<CoreExport<T>> for CoreDef
@@ -1105,9 +1109,13 @@ pub enum Trampoline {
11051109
/// component does not invalidate the handle in the original component.
11061110
ErrorContextTransfer,
11071111

1108-
/// An intrinsic used by FACT-generated modules to check whether an
1109-
/// async-typed function may be called via a sync lower.
1110-
CheckBlocking,
1112+
/// An intrinsic used by FACT-generated modules to check whether an instance
1113+
/// may be entered for a sync-to-sync call and push a task onto the stack if
1114+
/// so.
1115+
EnterSyncCall,
1116+
/// An intrinsic used by FACT-generated modules to pop the task previously
1117+
/// pushed by `EnterSyncCall`.
1118+
ExitSyncCall,
11111119

11121120
/// An intrinsic used by FACT-generated modules to trap with a specified
11131121
/// code.
@@ -1242,8 +1250,9 @@ impl Trampoline {
12421250
FutureTransfer => format!("future-transfer"),
12431251
StreamTransfer => format!("stream-transfer"),
12441252
ErrorContextTransfer => format!("error-context-transfer"),
1245-
CheckBlocking => format!("check-blocking"),
12461253
Trap => format!("trap"),
1254+
EnterSyncCall => format!("enter-sync-call"),
1255+
ExitSyncCall => format!("exit-sync-call"),
12471256
ContextGet { .. } => format!("context-get"),
12481257
ContextSet { .. } => format!("context-set"),
12491258
ThreadIndex => format!("thread-index"),

crates/environ/src/component/translate.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ pub struct Translator<'a, 'data> {
7979

8080
/// The top-level import name for Wasmtime's unsafe intrinsics, if any.
8181
unsafe_intrinsics_import: Option<&'a str>,
82+
83+
/// Whether this component contains any module instantiations outside of
84+
/// leaf components.
85+
///
86+
/// We'll use this when generating fused adapters; if it's false, then we
87+
/// know that no guest-to-guest call can reenter a runtime instance
88+
/// recursively.
89+
has_nonleaf_module_instantiations: bool,
8290
}
8391

8492
/// Representation of the syntactic scope of a component meaning where it is
@@ -171,6 +179,14 @@ struct Translation<'data> {
171179
/// component has finished, e.g. for the `inline` pass, but beforehand this
172180
/// is set to `None`.
173181
types: Option<Types>,
182+
183+
/// Whether we've encountered a module instantiation while parsing this
184+
/// component (disregarding instantiations in subcomponents).
185+
saw_module_instantiation: bool,
186+
187+
/// Whether we've encountered a component instantation while parsing this
188+
/// component (disregarding instantiations in subcomponents).
189+
saw_component_instantiation: bool,
174190
}
175191

176192
// NB: the type information contained in `LocalInitializer` should always point
@@ -455,6 +471,7 @@ impl<'a, 'data> Translator<'a, 'data> {
455471
static_modules: Default::default(),
456472
scope_vec,
457473
unsafe_intrinsics_import: None,
474+
has_nonleaf_module_instantiations: false,
458475
}
459476
}
460477

@@ -606,6 +623,7 @@ impl<'a, 'data> Translator<'a, 'data> {
606623

607624
let known_func = match arg {
608625
CoreDef::InstanceFlags(_) => unreachable!("instance flags are not a function"),
626+
CoreDef::TaskMayBlock => unreachable!("task_may_block is not a function"),
609627

610628
// We could in theory inline these trampolines, so it could
611629
// potentially make sense to record that we know this
@@ -694,6 +712,9 @@ impl<'a, 'data> Translator<'a, 'data> {
694712
assert!(self.result.types.is_none());
695713
self.result.types = Some(self.validator.end(offset)?);
696714

715+
self.has_nonleaf_module_instantiations |=
716+
self.result.saw_module_instantiation && self.result.saw_component_instantiation;
717+
697718
// Exit the current lexical scope. If there is no parent (no
698719
// frame currently on the stack) then translation is finished.
699720
// Otherwise that means that a nested component has been
@@ -1251,6 +1272,7 @@ impl<'a, 'data> Translator<'a, 'data> {
12511272
// largely just records the arguments given from wasmparser into a
12521273
// `HashMap` for processing later during inlining.
12531274
Payload::InstanceSection(s) => {
1275+
self.result.saw_module_instantiation = true;
12541276
self.validator.instance_section(&s)?;
12551277
for instance in s {
12561278
let init = match instance? {
@@ -1266,6 +1288,7 @@ impl<'a, 'data> Translator<'a, 'data> {
12661288
}
12671289
}
12681290
Payload::ComponentInstanceSection(s) => {
1291+
self.result.saw_component_instantiation = true;
12691292
let mut index = self.validator.types(0).unwrap().component_instance_count();
12701293
self.validator.component_instance_section(&s)?;
12711294
for instance in s {

crates/environ/src/component/translate/adapt.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,11 @@ impl<'data> Translator<'_, 'data> {
205205
// the module using standard core wasm translation, and then fills out
206206
// the dfg metadata for each adapter.
207207
for (module_id, adapter_module) in state.adapter_modules.iter() {
208-
let mut module =
209-
fact::Module::new(self.types.types(), self.tunables.debug_adapter_modules);
208+
let mut module = fact::Module::new(
209+
self.types.types(),
210+
self.tunables.debug_adapter_modules,
211+
self.has_nonleaf_module_instantiations,
212+
);
210213
let mut names = Vec::with_capacity(adapter_module.adapters.len());
211214
for adapter in adapter_module.adapters.iter() {
212215
let name = format!("adapter{}", adapter.as_u32());
@@ -345,8 +348,9 @@ fn fact_import_to_core_def(
345348
fact::Import::ErrorContextTransfer => {
346349
simple_intrinsic(dfg::Trampoline::ErrorContextTransfer)
347350
}
348-
fact::Import::CheckBlocking => simple_intrinsic(dfg::Trampoline::CheckBlocking),
349351
fact::Import::Trap => simple_intrinsic(dfg::Trampoline::Trap),
352+
fact::Import::EnterSyncCall => simple_intrinsic(dfg::Trampoline::EnterSyncCall),
353+
fact::Import::ExitSyncCall => simple_intrinsic(dfg::Trampoline::ExitSyncCall),
350354
}
351355
}
352356

@@ -452,7 +456,8 @@ impl PartitionAdapterModules {
452456
// These items can't transitively depend on an adapter
453457
dfg::CoreDef::Trampoline(_)
454458
| dfg::CoreDef::InstanceFlags(_)
455-
| dfg::CoreDef::UnsafeIntrinsic(..) => {}
459+
| dfg::CoreDef::UnsafeIntrinsic(..)
460+
| dfg::CoreDef::TaskMayBlock => {}
456461
}
457462
}
458463

crates/environ/src/component/translate/inline.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -538,11 +538,9 @@ impl<'a> Inliner<'a> {
538538
// happening within the same component instance.
539539
//
540540
// In this situation if the `canon.lower`'d function is
541-
// called then it immediately sets `may_enter` to `false`.
542-
// When calling the callee, however, that's `canon.lift`
543-
// which immediately traps if `may_enter` is `false`. That
544-
// means that this pairing of functions creates a function
545-
// that always traps.
541+
// called then it recursively re-enters itself, which is
542+
// defined to trap by the spec. That means that this pairing
543+
// of functions creates a function that always traps.
546544
//
547545
// When closely reading the spec though the precise trap
548546
// that comes out can be somewhat variable. Technically the
@@ -1594,7 +1592,7 @@ impl<'a> Inliner<'a> {
15941592
}
15951593
}
15961594

1597-
/// Translatees an `AdapterOptions` into a `CanonicalOptions` where
1595+
/// Translates an `AdapterOptions` into a `CanonicalOptions` where
15981596
/// memories/functions are inserted into the global initializer list for
15991597
/// use at runtime. This is only used for lowered host functions and lifted
16001598
/// functions exported to the host.

crates/environ/src/component/vmcomponent_offsets.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,9 @@ pub const VMCOMPONENT_MAGIC: u32 = u32::from_le_bytes(*b"comp");
2828
/// canonical ABI flag `may_leave`
2929
pub const FLAG_MAY_LEAVE: i32 = 1 << 0;
3030

31-
/// Flag for the `VMComponentContext::flags` field which corresponds to the
32-
/// canonical ABI flag `may_enter`
33-
pub const FLAG_MAY_ENTER: i32 = 1 << 1;
34-
3531
/// Flag for the `VMComponentContext::flags` field which is set whenever a
3632
/// function is called to indicate that `post_return` must be called next.
37-
pub const FLAG_NEEDS_POST_RETURN: i32 = 1 << 2;
33+
pub const FLAG_NEEDS_POST_RETURN: i32 = 1 << 1;
3834

3935
/// Runtime offsets within a `VMComponentContext` for a specific component.
4036
#[derive(Debug, Clone, Copy)]
@@ -70,6 +66,7 @@ pub struct VMComponentOffsets<P> {
7066
builtins: u32,
7167
vm_store_context: u32,
7268
flags: u32,
69+
task_may_block: u32,
7370
trampoline_func_refs: u32,
7471
intrinsic_func_refs: u32,
7572
lowerings: u32,
@@ -129,6 +126,7 @@ impl<P: PtrSize> VMComponentOffsets<P> {
129126
builtins: 0,
130127
vm_store_context: 0,
131128
flags: 0,
129+
task_may_block: 0,
132130
trampoline_func_refs: 0,
133131
intrinsic_func_refs: 0,
134132
lowerings: 0,
@@ -172,6 +170,7 @@ impl<P: PtrSize> VMComponentOffsets<P> {
172170
size(vm_store_context) = ret.ptr.size(),
173171
align(16),
174172
size(flags) = cmul(ret.num_runtime_component_instances, ret.ptr.size_of_vmglobal_definition()),
173+
size(task_may_block) = ret.ptr.size(),
175174
align(u32::from(ret.ptr.size())),
176175
size(trampoline_func_refs) = cmul(ret.num_trampolines, ret.ptr.size_of_vm_func_ref()),
177176
size(intrinsic_func_refs) = cmul(ret.num_unsafe_intrinsics, ret.ptr.size_of_vm_func_ref()),
@@ -219,6 +218,11 @@ impl<P: PtrSize> VMComponentOffsets<P> {
219218
self.flags + index.as_u32() * u32::from(self.ptr.size_of_vmglobal_definition())
220219
}
221220

221+
/// The offset of the `task_may_block` field.
222+
pub fn task_may_block(&self) -> u32 {
223+
self.task_may_block
224+
}
225+
222226
/// The offset of the `vm_store_context` field.
223227
#[inline]
224228
pub fn vm_store_context(&self) -> u32 {

0 commit comments

Comments
 (0)