Skip to content

Commit 1c14842

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. Signed-off-by: Joel Dice <[email protected]>
1 parent cb97ae8 commit 1c14842

File tree

26 files changed

+714
-279
lines changed

26 files changed

+714
-279
lines changed

crates/cranelift/src/compiler/component.rs

Lines changed: 49 additions & 18 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 anyhow::{Result, bail};
55
use cranelift_codegen::ir::condcodes::IntCC;
66
use cranelift_codegen::ir::{self, InstBuilder, MemFlags, Value};
@@ -740,6 +740,22 @@ impl<'a> TrampolineCompiler<'a> {
740740
|_, _| {},
741741
);
742742
}
743+
Trampoline::EnterSyncCall => {
744+
self.translate_libcall(
745+
host::enter_sync_call,
746+
TrapSentinel::Falsy,
747+
WasmArgs::InRegisters,
748+
|_, _| {},
749+
);
750+
}
751+
Trampoline::ExitSyncCall => {
752+
self.translate_libcall(
753+
host::exit_sync_call,
754+
TrapSentinel::Falsy,
755+
WasmArgs::InRegisters,
756+
|_, _| {},
757+
);
758+
}
743759
Trampoline::ContextGet { instance, slot } => {
744760
self.translate_libcall(
745761
host::context_get,
@@ -1129,11 +1145,9 @@ impl<'a> TrampolineCompiler<'a> {
11291145
// brif should_run_destructor, run_destructor_block, return_block
11301146
//
11311147
// run_destructor_block:
1132-
// ;; test may_enter, but only if the component instances
1148+
// ;; call `enter_sync_call`, but only if the component instances
11331149
// ;; differ
1134-
// flags = load.i32 vmctx+$offset
1135-
// masked = band flags, $FLAG_MAY_ENTER
1136-
// trapz masked, CANNOT_ENTER_CODE
1150+
// ...
11371151
//
11381152
// ;; ============================================================
11391153
// ;; this is conditionally emitted based on whether the resource
@@ -1147,6 +1161,9 @@ impl<'a> TrampolineCompiler<'a> {
11471161
// call_indirect func_addr, callee_vmctx, vmctx, rep
11481162
// ;; ============================================================
11491163
//
1164+
// ;; call `exit_sync_call` if the component instances differ
1165+
// ...
1166+
//
11501167
// jump return_block
11511168
//
11521169
// return_block:
@@ -1177,25 +1194,32 @@ impl<'a> TrampolineCompiler<'a> {
11771194
self.builder.switch_to_block(run_destructor_block);
11781195

11791196
// If this is a defined resource within the component itself then a
1180-
// check needs to be emitted for the `may_enter` flag. Note though
1197+
// check needs to be emitted for recursive reentrance. Note though
11811198
// that this check can be elided if the resource table resides in
11821199
// the same component instance that defined the resource as the
11831200
// component is calling itself.
1184-
if let Some(def) = resource_def {
1201+
let emit_exit = if let Some(def) = resource_def {
11851202
if self.types[resource].unwrap_concrete_instance() != def.instance {
1186-
let flags = self.builder.ins().load(
1187-
ir::types::I32,
1188-
trusted,
1203+
let host_args = vec![
11891204
vmctx,
1190-
i32::try_from(self.offsets.instance_flags(def.instance)).unwrap(),
1191-
);
1192-
let masked = self
1193-
.builder
1194-
.ins()
1195-
.band_imm(flags, i64::from(FLAG_MAY_ENTER));
1196-
self.builder.ins().trapz(masked, TRAP_CANNOT_ENTER);
1205+
self.builder
1206+
.ins()
1207+
.iconst(ir::types::I32, i64::from(instance.as_u32())),
1208+
self.builder.ins().iconst(ir::types::I32, i64::from(0)),
1209+
self.builder
1210+
.ins()
1211+
.iconst(ir::types::I32, i64::from(def.instance.as_u32())),
1212+
];
1213+
let call = self.call_libcall(vmctx, host::enter_sync_call, &host_args);
1214+
let result = self.builder.func.dfg.inst_results(call).get(0).copied();
1215+
self.raise_if_host_trapped(result.unwrap());
1216+
true
1217+
} else {
1218+
false
11971219
}
1198-
}
1220+
} else {
1221+
false
1222+
};
11991223

12001224
// Conditionally emit destructor-execution code based on whether we
12011225
// statically know that a destructor exists or not.
@@ -1244,6 +1268,13 @@ impl<'a> TrampolineCompiler<'a> {
12441268
&[callee_vmctx, caller_vmctx, rep],
12451269
);
12461270
}
1271+
1272+
if emit_exit {
1273+
let call = self.call_libcall(vmctx, host::exit_sync_call, &[vmctx]);
1274+
let result = self.builder.func.dfg.inst_results(call).get(0).copied();
1275+
self.raise_if_host_trapped(result.unwrap());
1276+
}
1277+
12471278
self.builder.ins().jump(return_block, &[]);
12481279
self.builder.seal_block(run_destructor_block);
12491280

crates/environ/src/component.rs

Lines changed: 3 additions & 0 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")]

crates/environ/src/component/dfg.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,8 @@ pub enum Trampoline {
476476
StreamTransfer,
477477
ErrorContextTransfer,
478478
CheckBlocking,
479+
EnterSyncCall,
480+
ExitSyncCall,
479481
ContextGet {
480482
instance: RuntimeComponentInstanceIndex,
481483
slot: u32,
@@ -1156,6 +1158,8 @@ impl LinearizeDfg<'_> {
11561158
Trampoline::StreamTransfer => info::Trampoline::StreamTransfer,
11571159
Trampoline::ErrorContextTransfer => info::Trampoline::ErrorContextTransfer,
11581160
Trampoline::CheckBlocking => info::Trampoline::CheckBlocking,
1161+
Trampoline::EnterSyncCall => info::Trampoline::EnterSyncCall,
1162+
Trampoline::ExitSyncCall => info::Trampoline::ExitSyncCall,
11591163
Trampoline::ContextGet { instance, slot } => info::Trampoline::ContextGet {
11601164
instance: *instance,
11611165
slot: *slot,

crates/environ/src/component/info.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,14 @@ pub enum Trampoline {
11091109
/// async-typed function may be called via a sync lower.
11101110
CheckBlocking,
11111111

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,
1119+
11121120
/// Intrinsic used to implement the `context.get` component model builtin.
11131121
///
11141122
/// The payload here represents that this is accessing the Nth slot of local
@@ -1239,6 +1247,8 @@ impl Trampoline {
12391247
StreamTransfer => format!("stream-transfer"),
12401248
ErrorContextTransfer => format!("error-context-transfer"),
12411249
CheckBlocking => format!("check-blocking"),
1250+
EnterSyncCall => format!("enter-sync-call"),
1251+
ExitSyncCall => format!("exit-sync-call"),
12421252
ContextGet { .. } => format!("context-get"),
12431253
ContextSet { .. } => format!("context-set"),
12441254
ThreadIndex => format!("thread-index"),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ fn fact_import_to_core_def(
346346
simple_intrinsic(dfg::Trampoline::ErrorContextTransfer)
347347
}
348348
fact::Import::CheckBlocking => simple_intrinsic(dfg::Trampoline::CheckBlocking),
349+
fact::Import::EnterSyncCall => simple_intrinsic(dfg::Trampoline::EnterSyncCall),
350+
fact::Import::ExitSyncCall => simple_intrinsic(dfg::Trampoline::ExitSyncCall),
349351
}
350352
}
351353

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

Lines changed: 3 additions & 5 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

crates/environ/src/component/vmcomponent_offsets.rs

Lines changed: 1 addition & 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)]

crates/environ/src/fact.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ pub struct Module<'a> {
9191
imported_error_context_transfer: Option<FuncIndex>,
9292

9393
imported_check_blocking: Option<FuncIndex>,
94+
imported_enter_sync_call: Option<FuncIndex>,
95+
imported_exit_sync_call: Option<FuncIndex>,
9496

9597
// Current status of index spaces from the imports generated so far.
9698
imported_funcs: PrimaryMap<FuncIndex, Option<CoreDef>>,
@@ -263,6 +265,8 @@ impl<'a> Module<'a> {
263265
imported_stream_transfer: None,
264266
imported_error_context_transfer: None,
265267
imported_check_blocking: None,
268+
imported_enter_sync_call: None,
269+
imported_exit_sync_call: None,
266270
exports: Vec::new(),
267271
}
268272
}
@@ -727,6 +731,28 @@ impl<'a> Module<'a> {
727731
)
728732
}
729733

734+
fn import_enter_sync_call(&mut self) -> FuncIndex {
735+
self.import_simple(
736+
"async",
737+
"enter-sync-call",
738+
&[ValType::I32; 3],
739+
&[],
740+
Import::EnterSyncCall,
741+
|me| &mut me.imported_enter_sync_call,
742+
)
743+
}
744+
745+
fn import_exit_sync_call(&mut self) -> FuncIndex {
746+
self.import_simple(
747+
"async",
748+
"exit-sync-call",
749+
&[],
750+
&[],
751+
Import::ExitSyncCall,
752+
|me| &mut me.imported_exit_sync_call,
753+
)
754+
}
755+
730756
fn translate_helper(&mut self, helper: Helper) -> FunctionId {
731757
*self.helper_funcs.entry(helper).or_insert_with(|| {
732758
// Generate a fresh `Function` with a unique id for what we're about to
@@ -888,6 +914,13 @@ pub enum Import {
888914
/// An intrinsic used by FACT-generated modules to check whether an
889915
/// async-typed function may be called via a sync lower.
890916
CheckBlocking,
917+
/// An intrinsic used by FACT-generated modules to check whether an instance
918+
/// may be entered for a sync-to-sync call and push a task onto the stack if
919+
/// so.
920+
EnterSyncCall,
921+
/// An intrinsic used by FACT-generated modules to pop the task previously
922+
/// pushed by `EnterSyncCall`.
923+
ExitSyncCall,
891924
}
892925

893926
impl Options {

crates/environ/src/fact/trampoline.rs

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
//! can be somewhat arbitrary, an intentional decision.
1717
1818
use crate::component::{
19-
CanonicalAbiInfo, ComponentTypesBuilder, FLAG_MAY_ENTER, FLAG_MAY_LEAVE, FixedEncoding as FE,
20-
FlatType, InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, PREPARE_ASYNC_NO_RESULT,
19+
CanonicalAbiInfo, ComponentTypesBuilder, FLAG_MAY_LEAVE, FixedEncoding as FE, FlatType,
20+
InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, PREPARE_ASYNC_NO_RESULT,
2121
PREPARE_ASYNC_WITH_RESULT, START_FLAG_ASYNC_CALLEE, StringEncoding, Transcode,
2222
TypeComponentLocalErrorContextTableIndex, TypeEnumIndex, TypeFlagsIndex, TypeFutureTableIndex,
2323
TypeListIndex, TypeOptionIndex, TypeRecordIndex, TypeResourceTableIndex, TypeResultIndex,
@@ -743,17 +743,42 @@ impl<'a, 'b> Compiler<'a, 'b> {
743743
// flags on the callee if necessary whether it can be entered.
744744
self.trap_if_not_flag(adapter.lower.flags, FLAG_MAY_LEAVE, Trap::CannotLeave);
745745
if adapter.called_as_export {
746-
self.trap_if_not_flag(adapter.lift.flags, FLAG_MAY_ENTER, Trap::CannotEnter);
747-
self.set_flag(adapter.lift.flags, FLAG_MAY_ENTER, false);
748-
} else if self.module.debug {
749-
self.assert_not_flag(
750-
adapter.lift.flags,
751-
FLAG_MAY_ENTER,
752-
"may_enter should be unset",
753-
);
754-
}
755-
756-
if self.types[adapter.lift.ty].async_ {
746+
// Here we call a runtime intrinsic to check whether the instance
747+
// we're about to enter has already been entered by this task
748+
// (i.e. whether this is a recursive reentrance). If so, we'll trap
749+
// per the component model specification. Otherwise, we'll push a
750+
// task onto the stack and then later pop it off after the call.
751+
//
752+
// Note that, prior to the introduction of concurrency to the
753+
// component model, we were able to handle this via a per-instance
754+
// variable, allowing us to check and set that variable without
755+
// leaving Wasm code. However, now that there may be more than one
756+
// concurrent task running in a given instance, a per-instance
757+
// variable is not sufficient.
758+
//
759+
// Fortunately, we still have a number of opportunities to optimize
760+
// common cases (e.g. components which contain only sync functions,
761+
// and compositions of fewer than 64 transitive subcomponents, for
762+
// which a per-task `uint64` bitset would suffice) and avoid the
763+
// host call. See the discussion of `trap_if_on_the_stack` in
764+
// https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md
765+
// for details.
766+
//
767+
// TODO: Implement one or more of those optimizations.
768+
self.instruction(I32Const(
769+
i32::try_from(adapter.lower.instance.as_u32()).unwrap(),
770+
));
771+
self.instruction(I32Const(if self.types[adapter.lift.ty].async_ {
772+
1
773+
} else {
774+
0
775+
}));
776+
self.instruction(I32Const(
777+
i32::try_from(adapter.lift.instance.as_u32()).unwrap(),
778+
));
779+
let enter_sync_call = self.module.import_enter_sync_call();
780+
self.instruction(Call(enter_sync_call.as_u32()));
781+
} else if self.types[adapter.lift.ty].async_ {
757782
let check_blocking = self.module.import_check_blocking();
758783
self.instruction(Call(check_blocking.as_u32()));
759784
}
@@ -818,9 +843,6 @@ impl<'a, 'b> Compiler<'a, 'b> {
818843
}
819844
self.instruction(Call(func.as_u32()));
820845
}
821-
if adapter.called_as_export {
822-
self.set_flag(adapter.lift.flags, FLAG_MAY_ENTER, true);
823-
}
824846

825847
for tmp in temps {
826848
self.free_temp_local(tmp);
@@ -831,6 +853,11 @@ impl<'a, 'b> Compiler<'a, 'b> {
831853
self.instruction(Call(exit.as_u32()));
832854
}
833855

856+
if adapter.called_as_export {
857+
let exit_sync_call = self.module.import_exit_sync_call();
858+
self.instruction(Call(exit_sync_call.as_u32()));
859+
}
860+
834861
self.finish()
835862
}
836863

@@ -3268,15 +3295,6 @@ impl<'a, 'b> Compiler<'a, 'b> {
32683295
self.instruction(End);
32693296
}
32703297

3271-
fn assert_not_flag(&mut self, flags_global: GlobalIndex, flag_to_test: i32, msg: &'static str) {
3272-
self.instruction(GlobalGet(flags_global.as_u32()));
3273-
self.instruction(I32Const(flag_to_test));
3274-
self.instruction(I32And);
3275-
self.instruction(If(BlockType::Empty));
3276-
self.trap(Trap::AssertFailed(msg));
3277-
self.instruction(End);
3278-
}
3279-
32803298
fn set_flag(&mut self, flags_global: GlobalIndex, flag_to_set: i32, value: bool) {
32813299
self.instruction(GlobalGet(flags_global.as_u32()));
32823300
if value {

crates/environ/src/fact/traps.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use wasm_encoder::Encode;
2626
#[derive(Hash, PartialEq, Eq, Copy, Clone)]
2727
pub enum Trap {
2828
CannotLeave,
29-
CannotEnter,
3029
UnalignedPointer,
3130
InvalidDiscriminant,
3231
InvalidChar,
@@ -103,7 +102,6 @@ impl fmt::Display for Trap {
103102
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104103
match self {
105104
Trap::CannotLeave => "cannot leave instance".fmt(f),
106-
Trap::CannotEnter => "cannot enter instance".fmt(f),
107105
Trap::UnalignedPointer => "pointer not aligned correctly".fmt(f),
108106
Trap::InvalidDiscriminant => "invalid variant discriminant".fmt(f),
109107
Trap::InvalidChar => "invalid char value specified".fmt(f),

0 commit comments

Comments
 (0)