Skip to content

Commit 8992b99

Browse files
authored
trap on blocking call in sync task before return (#12043)
* trap on blocking call in sync task before return This implements a spec change (PR pending) such that tasks created for calls to synchronous exports may not call potentially-blocking imports or return `wait` or `poll` callback codes prior to returning a value. Specifically, the following are prohibited in that scenario: - returning callback-code.{wait,poll} - sync calling an async import - sync calling subtask.cancel - sync calling {stream,future}.{read,write} - sync calling {stream,future}.cancel-{read,write} - calling waitable-set.{wait,poll} - calling thread.suspend This breaks a number of tests, which will be addressed in follow-up commits: - The `{tcp,udp}-socket.bind` implementation in `wasmtime-wasi` is implemented using `Linker::func_wrap_concurrent` and thus assumed to be async, whereas the WIT interface says they're sync, leading to a type mismatch error at runtime. Alex and I have discussed this and have a general plan to address it. - A number of tests in the tests/component-model submodule that points to the spec repo are failing. Those will presumably be fixed as part of the upcoming spec PR (although some could be due to bugs in this implementation, in which case I'll fix them). - A number of tests in tests/misc_testsuite are failing. I'll address those in a follow-up commit. Signed-off-by: Joel Dice <[email protected]> * call `check_may_leave` before `check_blocking` `check_blocking` needs access to the current task, but that's not set for post-return functions since those should not be calling _any_ imports at all, so first check for that. Signed-off-by: Joel Dice <[email protected]> * fix `misc_testsuite` test regressions This amounts to adding `async` to any exported component functions that might need to block. Signed-off-by: Joel Dice <[email protected]> * simplify code in `ConcurrentState::check_blocking` Signed-off-by: Joel Dice <[email protected]> * make `thread.yield` a no-op in non-blocking contexts Per the proposed spec changes, `thread.yield` should return control to the guest immediately without allowing any other thread to run. Similarly, when an async-lifted export or callback returns `CALLBACK_CODE_YIELD`, we should call the callback again immediately without allowing another thread to run. Signed-off-by: Joel Dice <[email protected]> * fix build when `component-model-async` feature disabled Signed-off-by: Joel Dice <[email protected]> * fix more test regressions Signed-off-by: Joel Dice <[email protected]> * fix more test regressions Note that this temporarily updates the `tests/component-model` submodule to the branch for WebAssembly/component-model#577 until that PR is merged. Signed-off-by: Joel Dice <[email protected]> * tweak `Trap::CannotBlockSyncTask` message This clarifies that such a task cannot block prior to returning. Signed-off-by: Joel Dice <[email protected]> * fix cancel_host_future test Signed-off-by: Joel Dice <[email protected]> * trap sync-lowered, guest->guest async calls in sync tasks I somehow forgot to address this earlier. Thanks to Luke for catching this. Note that this commit doesn't include test coverage, but Luke's forthecoming tests in the `component-model` repo will cover it, and we'll pull that in with the next submodule update. Signed-off-by: Joel Dice <[email protected]> * switch back to `main` branch of `component-model` repo ...and skip or `should_fail` the tests that won't pass until WebAssembly/component-model#578 is merged. Signed-off-by: Joel Dice <[email protected]> * add `trap-if-block-and-sync.wast` We'll remove this again in favor of the upstream version once WebAssembly/component-model#578 has been merged. Signed-off-by: Joel Dice <[email protected]> * address review feedback - Assert that `StoreOpaque::suspend` is not called in a non-blocking context except in specific circumstances - Typecheck async-ness for dynamic host functions - Use type parameter instead of value parameter in `call_host[_dynamic]` Signed-off-by: Joel Dice <[email protected]> * add explanation comments to `check_blocking` calls Signed-off-by: Joel Dice <[email protected]> * fix fuzz test oracle for async functions Signed-off-by: Joel Dice <[email protected]> --------- Signed-off-by: Joel Dice <[email protected]>
1 parent 9fd594a commit 8992b99

36 files changed

+876
-156
lines changed

crates/cranelift/src/compiler/component.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,14 @@ impl<'a> TrampolineCompiler<'a> {
742742
|_, _| {},
743743
);
744744
}
745+
Trampoline::CheckBlocking => {
746+
self.translate_libcall(
747+
host::check_blocking,
748+
TrapSentinel::Falsy,
749+
WasmArgs::InRegisters,
750+
|_, _| {},
751+
);
752+
}
745753
Trampoline::ContextGet { instance, slot } => {
746754
self.translate_libcall(
747755
host::context_get,

crates/environ/src/component.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ macro_rules! foreach_builtin_component_function {
132132
caller_instance: u32,
133133
callee_instance: u32,
134134
task_return_type: u32,
135+
callee_async: u32,
135136
string_encoding: u32,
136137
result_count_or_max_if_async: u32,
137138
storage: ptr_u8,
@@ -186,6 +187,8 @@ macro_rules! foreach_builtin_component_function {
186187
#[cfg(feature = "component-model-async")]
187188
error_context_transfer(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64;
188189
#[cfg(feature = "component-model-async")]
190+
check_blocking(vmctx: vmctx) -> bool;
191+
#[cfg(feature = "component-model-async")]
189192
context_get(vmctx: vmctx, caller_instance: u32, slot: u32) -> u64;
190193
#[cfg(feature = "component-model-async")]
191194
context_set(vmctx: vmctx, caller_instance: u32, slot: u32, val: u32) -> bool;

crates/environ/src/component/dfg.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ pub enum Trampoline {
478478
FutureTransfer,
479479
StreamTransfer,
480480
ErrorContextTransfer,
481+
CheckBlocking,
481482
ContextGet {
482483
instance: RuntimeComponentInstanceIndex,
483484
slot: u32,
@@ -1160,6 +1161,7 @@ impl LinearizeDfg<'_> {
11601161
Trampoline::FutureTransfer => info::Trampoline::FutureTransfer,
11611162
Trampoline::StreamTransfer => info::Trampoline::StreamTransfer,
11621163
Trampoline::ErrorContextTransfer => info::Trampoline::ErrorContextTransfer,
1164+
Trampoline::CheckBlocking => info::Trampoline::CheckBlocking,
11631165
Trampoline::ContextGet { instance, slot } => info::Trampoline::ContextGet {
11641166
instance: *instance,
11651167
slot: *slot,

crates/environ/src/component/info.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,10 @@ pub enum Trampoline {
11121112
/// component does not invalidate the handle in the original component.
11131113
ErrorContextTransfer,
11141114

1115+
/// An intrinsic used by FACT-generated modules to check whether an
1116+
/// async-typed function may be called via a sync lower.
1117+
CheckBlocking,
1118+
11151119
/// Intrinsic used to implement the `context.get` component model builtin.
11161120
///
11171121
/// The payload here represents that this is accessing the Nth slot of local
@@ -1242,6 +1246,7 @@ impl Trampoline {
12421246
FutureTransfer => format!("future-transfer"),
12431247
StreamTransfer => format!("stream-transfer"),
12441248
ErrorContextTransfer => format!("error-context-transfer"),
1249+
CheckBlocking => format!("check-blocking"),
12451250
ContextGet { .. } => format!("context-get"),
12461251
ContextSet { .. } => format!("context-set"),
12471252
ThreadIndex => format!("thread-index"),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ fn fact_import_to_core_def(
345345
fact::Import::ErrorContextTransfer => {
346346
simple_intrinsic(dfg::Trampoline::ErrorContextTransfer)
347347
}
348+
fact::Import::CheckBlocking => simple_intrinsic(dfg::Trampoline::CheckBlocking),
348349
}
349350
}
350351

crates/environ/src/fact.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub static PREPARE_CALL_FIXED_PARAMS: &[ValType] = &[
4747
ValType::I32, // caller_instance
4848
ValType::I32, // callee_instance
4949
ValType::I32, // task_return_type
50+
ValType::I32, // callee_async
5051
ValType::I32, // string_encoding
5152
ValType::I32, // result_count_or_max_if_async
5253
];
@@ -89,6 +90,8 @@ pub struct Module<'a> {
8990
imported_stream_transfer: Option<FuncIndex>,
9091
imported_error_context_transfer: Option<FuncIndex>,
9192

93+
imported_check_blocking: Option<FuncIndex>,
94+
9295
// Current status of index spaces from the imports generated so far.
9396
imported_funcs: PrimaryMap<FuncIndex, Option<CoreDef>>,
9497
imported_memories: PrimaryMap<MemoryIndex, CoreDef>,
@@ -259,6 +262,7 @@ impl<'a> Module<'a> {
259262
imported_future_transfer: None,
260263
imported_stream_transfer: None,
261264
imported_error_context_transfer: None,
265+
imported_check_blocking: None,
262266
exports: Vec::new(),
263267
}
264268
}
@@ -712,6 +716,17 @@ impl<'a> Module<'a> {
712716
)
713717
}
714718

719+
fn import_check_blocking(&mut self) -> FuncIndex {
720+
self.import_simple(
721+
"async",
722+
"check-blocking",
723+
&[],
724+
&[],
725+
Import::CheckBlocking,
726+
|me| &mut me.imported_check_blocking,
727+
)
728+
}
729+
715730
fn translate_helper(&mut self, helper: Helper) -> FunctionId {
716731
*self.helper_funcs.entry(helper).or_insert_with(|| {
717732
// Generate a fresh `Function` with a unique id for what we're about to
@@ -870,6 +885,9 @@ pub enum Import {
870885
/// An intrinisic used by FACT-generated modules to (partially or entirely) transfer
871886
/// ownership of an `error-context`.
872887
ErrorContextTransfer,
888+
/// An intrinsic used by FACT-generated modules to check whether an
889+
/// async-typed function may be called via a sync lower.
890+
CheckBlocking,
873891
}
874892

875893
impl Options {

crates/environ/src/fact/trampoline.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,11 @@ impl<'a, 'b> Compiler<'a, 'b> {
544544
self.instruction(I32Const(
545545
i32::try_from(self.types[adapter.lift.ty].results.as_u32()).unwrap(),
546546
));
547+
self.instruction(I32Const(if self.types[adapter.lift.ty].async_ {
548+
1
549+
} else {
550+
0
551+
}));
547552
self.instruction(I32Const(i32::from(
548553
adapter.lift.options.string_encoding as u8,
549554
)));
@@ -748,6 +753,11 @@ impl<'a, 'b> Compiler<'a, 'b> {
748753
);
749754
}
750755

756+
if self.types[adapter.lift.ty].async_ {
757+
let check_blocking = self.module.import_check_blocking();
758+
self.instruction(Call(check_blocking.as_u32()));
759+
}
760+
751761
if self.emit_resource_call {
752762
let enter = self.module.import_resource_enter_call();
753763
self.instruction(Call(enter.as_u32()));

crates/environ/src/trap_encoding.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ pub enum Trap {
112112
/// scenario where a component instance tried to call an import or intrinsic
113113
/// when it wasn't allowed to, e.g. from a post-return function.
114114
CannotLeaveComponent,
115+
116+
/// A synchronous task attempted to make a potentially blocking call.
117+
CannotBlockSyncTask,
115118
// if adding a variant here be sure to update the `check!` macro below
116119
}
117120

@@ -154,6 +157,7 @@ impl Trap {
154157
DisabledOpcode
155158
AsyncDeadlock
156159
CannotLeaveComponent
160+
CannotBlockSyncTask
157161
}
158162

159163
None
@@ -190,6 +194,7 @@ impl fmt::Display for Trap {
190194
DisabledOpcode => "pulley opcode disabled at compile time was executed",
191195
AsyncDeadlock => "deadlock detected: event loop cannot make further progress",
192196
CannotLeaveComponent => "cannot leave component instance",
197+
CannotBlockSyncTask => "cannot block a synchronous task before returning",
193198
};
194199
write!(f, "wasm trap: {desc}")
195200
}

crates/misc/component-async-tests/wit/test.wit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ interface resource-stream {
122122
foo: func();
123123
}
124124

125-
foo: func(count: u32) -> stream<x>;
125+
foo: async func(count: u32) -> stream<x>;
126126
}
127127

128128
interface closed {
@@ -157,7 +157,7 @@ interface cancel {
157157
leak-task-after-cancel,
158158
}
159159

160-
run: func(mode: mode, cancel-delay-millis: u64);
160+
run: async func(mode: mode, cancel-delay-millis: u64);
161161
}
162162

163163
interface intertask {

crates/test-programs/src/bin/async_read_resource_stream.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct Component;
1515
impl Guest for Component {
1616
async fn run() {
1717
let mut count = 7;
18-
let mut stream = resource_stream::foo(count);
18+
let mut stream = resource_stream::foo(count).await;
1919

2020
while let Some(x) = stream.next().await {
2121
if count > 0 {

0 commit comments

Comments
 (0)