Skip to content

Commit b9d5c23

Browse files
authored
[Stack Switching] Fix resuming of return calls (#7860)
A return call is a form of control flow, so we need to handle it. Specifically, the original function is unwound before calling the next one, so when we resume, we should just call the next one. To do that, save the call target along with the locals during unwinding.
1 parent 1785bc3 commit b9d5c23

File tree

2 files changed

+108
-2
lines changed

2 files changed

+108
-2
lines changed

src/wasm-interpreter.h

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4664,6 +4664,30 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
46644664

46654665
// We may have to call multiple functions in the event of return calls.
46664666
while (true) {
4667+
if (self()->isResuming()) {
4668+
// See which function to call. Re-winding the stack, we are calling the
4669+
// function that the parent called, but the target that was called may
4670+
// have return-called. In that case, the original target function should
4671+
// not be called, as it was returned from, and we noted the proper
4672+
// target during that return.
4673+
auto entry = self()->popResumeEntry("function-target");
4674+
assert(entry.size() == 1);
4675+
auto func = entry[0];
4676+
auto data = func.getFuncData();
4677+
// We must be in the right module to do the call using that name.
4678+
if (data->self != self()) {
4679+
// Restore the entry to the resume stack, as the other module's
4680+
// callFunction() will read it. Then call into the other module. This
4681+
// sets this up as if we called into the proper module in the first
4682+
// place.
4683+
self()->pushResumeEntry(entry, "function-target");
4684+
return data->doCall(arguments);
4685+
}
4686+
4687+
// We are in the right place, and can just call the given function.
4688+
name = data->name;
4689+
}
4690+
46674691
Function* function = wasm.getFunction(name);
46684692
assert(function);
46694693

@@ -4684,7 +4708,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
46844708
// Restore the local state (see below for the ordering, we push/pop).
46854709
for (Index i = 0; i < scope.locals.size(); i++) {
46864710
auto l = scope.locals.size() - 1 - i;
4687-
scope.locals[l] = self()->popResumeEntry("function");
4711+
scope.locals[l] = self()->popResumeEntry("function-local");
46884712
#ifndef NDEBUG
46894713
// Must have restored valid data. The type must match the local's
46904714
// type, except for the case of a non-nullable local that has not yet
@@ -4718,8 +4742,15 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
47184742
if (flow.suspendTag) {
47194743
// Save the local state.
47204744
for (auto& local : scope.locals) {
4721-
self()->pushResumeEntry(local, "function");
4745+
self()->pushResumeEntry(local, "function-local");
47224746
}
4747+
4748+
// Save the function we called (in the case of a return call, this is
4749+
// not the original function that was called, and the original has been
4750+
// returned from already; we should call the last return_called
4751+
// function).
4752+
auto target = self()->makeFuncData(name, function->type);
4753+
self()->pushResumeEntry({target}, "function-target");
47234754
}
47244755

47254756
if (flow.breakTo != RETURN_CALL_FLOW) {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited.
2+
3+
;; RUN: foreach %s %t wasm-opt -all --fuzz-exec-before -q -o /dev/null 2>&1 | filecheck %s
4+
5+
;; Test suspend/resume through a return call. $cont return_calls $sub, which
6+
;; then suspends. The return_call removes $cont from the stack before that
7+
;; suspend, so we should jump into $sub during resuming, and not hit any errors.
8+
9+
(module
10+
(type $i32 (func (result i32)))
11+
(type $cont (cont $i32))
12+
(type $none (func))
13+
14+
(import "fuzzing-support" "log" (func $log (param i32)))
15+
16+
(tag $tag (type $none))
17+
18+
;; CHECK: [fuzz-exec] calling main
19+
;; CHECK-NEXT: [LoggingExternalInterface logging 50]
20+
;; CHECK-NEXT: [LoggingExternalInterface logging -1]
21+
;; CHECK-NEXT: [LoggingExternalInterface logging 100]
22+
;; CHECK-NEXT: [LoggingExternalInterface logging 200]
23+
;; CHECK-NEXT: [LoggingExternalInterface logging -1]
24+
;; CHECK-NEXT: [LoggingExternalInterface logging 300]
25+
;; CHECK-NEXT: [LoggingExternalInterface logging 400]
26+
;; CHECK-NEXT: [LoggingExternalInterface logging -1]
27+
;; CHECK-NEXT: [LoggingExternalInterface logging 500]
28+
;; CHECK-NEXT: [trap unreachable]
29+
(func $main (export "main")
30+
(local $c (ref $cont))
31+
;; Create a continuation and keep resuming it while it suspends.
32+
(local.set $c
33+
(cont.new $cont
34+
(ref.func $cont)
35+
)
36+
)
37+
(loop $label
38+
(local.set $c
39+
(block $block (result (ref $cont))
40+
(drop
41+
(resume $cont (on $tag $block)
42+
(local.get $c)
43+
)
44+
)
45+
(unreachable)
46+
)
47+
)
48+
(call $log (i32.const -1)) ;; logged each time we suspend
49+
(br $label)
50+
)
51+
)
52+
53+
(func $cont (type $i32) (result i32)
54+
(call $log (i32.const 50)) ;; only reached once
55+
(suspend $tag)
56+
(call $log (i32.const 100)) ;; only reached once
57+
(return_call $sub)
58+
(call $log (i32.const -100)) ;; never reached
59+
)
60+
61+
(func $sub (result i32)
62+
(call $log (i32.const 200))
63+
(suspend $tag)
64+
(call $log (i32.const 300))
65+
(return_call $subsub)
66+
)
67+
68+
(func $subsub (result i32)
69+
(call $log (i32.const 400))
70+
(suspend $tag)
71+
(call $log (i32.const 500))
72+
(i32.const 0)
73+
)
74+
)
75+

0 commit comments

Comments
 (0)