Skip to content

Commit 4a33837

Browse files
committed
CABI: assert basic reentrancy rules the host must obey (clarification)
1 parent f4a4f07 commit 4a33837

4 files changed

Lines changed: 354 additions & 271 deletions

File tree

design/mvp/Concurrency.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ emojis. For an even higher-level introduction, see [these][wasmio-2024]
1616
* [Thread Built-ins](#thread-built-ins)
1717
* [Thread-Local Storage](#thread-local-storage)
1818
* [Blocking](#blocking)
19+
* [Reentrance](#reentrance)
1920
* [Waitables and Waitable Sets](#waitables-and-waitable-sets)
2021
* [Streams and Futures](#streams-and-futures)
2122
* [Stream Readiness](#stream-readiness)
@@ -313,6 +314,7 @@ of the new subtask created for the import call. Thus, one reason for
313314
associating every thread with a "containing task" is to ensure that there is
314315
always a well-defined async call stack.
315316

317+
TODO: maybe move this to 'Reentrance' section
316318
A semantically-observable use of the async call stack is to distinguish between
317319
hazardous **recursive reentrance**, in which a component instance is reentered
318320
when one of its tasks is already on the callstack, from business-as-usual
@@ -503,6 +505,10 @@ once the reason for blocking is addressed.
503505
The [Canonical ABI explainer] defines the above behavior more precisely; search
504506
for `may_block` to see all the relevant points.
505507

508+
### Reentrance
509+
510+
TODO
511+
506512
### Waitables and Waitable Sets
507513

508514
When an `async`-typed function is called with the async ABI and the call
@@ -1278,7 +1284,7 @@ comes after:
12781284
* add an `async` effect on `resource` type definitions allowing a resource
12791285
type to block during its destructor
12801286
* `recursive` function type attribute: allow a function to opt in to
1281-
recursive [reentrance], extending the ABI to link the inner and
1287+
recursive [#reentrance], extending the ABI to link the inner and
12821288
outer activations
12831289
* add a `strict-callback` option that adds extra trapping conditions to
12841290
provide the semantic guarantees needed for engines to statically avoid
@@ -1356,7 +1362,6 @@ comes after:
13561362
[Binary Format]: Binary.md
13571363
[WIT]: WIT.md
13581364
[Blast Zone]: FutureFeatures.md#blast-zones
1359-
[Reentrance]: Explainer.md#component-invariants
13601365
[`start`]: Explainer.md#start-definitions
13611366

13621367
[Store]: https://webassembly.github.io/spec/core/exec/runtime.html#syntax-store

design/mvp/Explainer.md

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,10 +1735,10 @@ propagated once received.
17351735
If `waitable-set.wait` is called from a synchronous- or `async callback`-lifted
17361736
export, no other threads that were implicitly created by a separate
17371737
synchronous- or `async callback`-lifted export call can start or progress in
1738-
the current component instance until `waitable-set.wait` returns (thereby
1739-
ensuring non-reentrance of the core wasm code). However, explicitly-created
1740-
threads and threads implicitly created by non-`callback` `async`-lifted
1741-
("stackful async") exports may start or progress at any time.
1738+
the current component instance until `waitable-set.wait` returns (preserving
1739+
[component invariant] #2). However, explicitly-created threads and threads
1740+
implicitly created by non-`callback` `async`-lifted ("stackful async") exports
1741+
may start or progress at any time.
17421742

17431743
A `subtask` event notifies the supertask that its subtask is now in the given
17441744
state (the meanings of which are described by the [concurrency explainer]).
@@ -2201,12 +2201,12 @@ thread to be resumed (as with `thread.yield-to`). If `cancellable` is set,
22012201
`thread.yield` returns whether the current task was [cancelled] by the caller;
22022202
otherwise, `thread.yield` always returns `false`.
22032203

2204-
If `thread.yield` is called from a synchronous- or `async callback`-lifted
2205-
export, it returns immediately without blocking (instead of trapping, as with
2206-
other possibly-blocking operations like `waitable-set.wait`). This is because,
2207-
unlike other built-ins, `thread.yield` may be scattered liberally throughout
2208-
code that might show up in the transitive call tree of a synchronous function
2209-
call.
2204+
If `thread.yield` is called from a non-`async`-typed function that has not yet
2205+
returned a value, it returns immediately without blocking (instead of trapping,
2206+
as with other possibly-blocking operations like `waitable-set.wait`). This is
2207+
because, unlike other built-ins, `thread.yield` may be scattered liberally
2208+
throughout code that might show up in the transitive call tree of a synchronous
2209+
function call.
22102210

22112211
For details, see [Thread Built-ins] in the concurrency explainer and
22122212
[`canon_thread_yield`] in the Canonical ABI explainer.
@@ -2908,8 +2908,8 @@ definition. Thus, component functions form a "membrane" around the collection
29082908
of core module instances contained by a component instance, allowing the
29092909
Component Model to establish invariants that increase optimizability and
29102910
composability in ways not otherwise possible in the shared-everything setting
2911-
of Core WebAssembly. The Component Model proposes establishing the following
2912-
two runtime invariants:
2911+
of Core WebAssembly. The Component Model establishes the following runtime
2912+
invariants:
29132913
1. Components define a "lockdown" state that prevents continued execution
29142914
after a trap. This both prevents continued execution with corrupt state and
29152915
also allows more-aggressive compiler optimizations (e.g., store reordering).
@@ -2919,13 +2919,16 @@ two runtime invariants:
29192919
implicitly checked at every execution step by component functions. Thus,
29202920
after a trap, it's no longer possible to observe the internal state of a
29212921
component instance.
2922-
2. The Component Model disallows reentrance by trapping if a callee's
2923-
component-instance is already on the stack when the call starts.
2924-
(For details, see [`call_might_be_recursive`](CanonicalABI.md#component-instance-state)
2925-
in the Canonical ABI explainer.) This default prevents obscure
2926-
composition-time bugs and also enables more-efficient non-reentrant
2927-
runtime glue code. This rule will be relaxed by an opt-in
2928-
function type attribute in the [future](Concurrency.md#todo).
2922+
2. The Component Model's [reentrance] rules allow a producer toolchain to
2923+
implement both synchronous and `async` (🔀) exported functions using a
2924+
single, fixed-size shadow stack in linear memory that is pushed and popped in
2925+
LIFO order. (Internal use of cooperative threads (🧵) or the "stackful" ABI
2926+
option (🚟) may however require the use of multiple shadow stacks.)
2927+
3. Until the Component Model provides a mechanism for guest code to reliably
2928+
avoid recursive deadlocks (in particular, due to async backpressure),
2929+
recursive [reentrance] rules trap in cases which might otherwise lead to
2930+
recursive deadlock. (This limitation is intended to be relaxed in the
2931+
[future](Concurrency.md#todo).)
29292932

29302933

29312934
## JavaScript Embedding
@@ -3327,6 +3330,7 @@ For some use-case-focused, worked examples, see:
33273330
[Resolved]: Concurrency.md#cancellation
33283331
[Cancellation]: Concurrency.md#cancellation
33293332
[Cancelled]: Concurrency.md#cancellation
3333+
[Reentrance]: Concurrency.md#reentrance
33303334

33313335
[Component Model Documentation]: https://component-model.bytecodealliance.org
33323336
[`wizer`]: https://github.com/bytecodealliance/wizer

design/mvp/canonical-abi/definitions.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class ComponentInstance:
197197
num_waiting_to_enter: int
198198
exclusive: Optional[Thread]
199199

200-
def __init__(self, store, parent = None):
200+
def __init__(self, store, parent):
201201
assert(parent is None or parent.store is store)
202202
self.store = store
203203
self.parent = parent
@@ -208,13 +208,11 @@ def __init__(self, store, parent = None):
208208
self.exclusive = None
209209
self.num_waiting_to_enter = 0
210210

211-
def reflexive_ancestors(self) -> set[ComponentInstance]:
212-
s = set()
211+
def root(self) -> RootComponentInstance:
213212
inst = self
214-
while inst is not None:
215-
s.add(inst)
213+
while inst.parent is not None:
216214
inst = inst.parent
217-
return s
215+
return inst
218216

219217
def is_ancestor_of(self, other):
220218
ancestor = other.parent
@@ -224,17 +222,26 @@ def is_ancestor_of(self, other):
224222
ancestor = ancestor.parent
225223
return False
226224

227-
def call_might_be_recursive(self, caller: Supertask):
228-
if caller.inst is None:
229-
while caller is not None:
230-
if caller.inst and caller.inst.reflexive_ancestors() & self.reflexive_ancestors():
231-
return True
232-
caller = caller.supertask
233-
return False
234-
else:
235-
return (caller.inst is self or
236-
caller.inst.is_ancestor_of(self) or
237-
self.is_ancestor_of(caller.inst))
225+
def host_call_is_recursive(self, caller: Supertask):
226+
assert(caller.inst is None or caller.inst.root() is not self.root())
227+
while caller is not None:
228+
if caller.inst is not None and caller.inst.root() is self.root():
229+
return True
230+
caller = caller.supertask
231+
return False
232+
233+
def intra_component_call_might_be_recursive(self, caller: Supertask):
234+
assert(caller.inst.root() is self.root())
235+
return (caller.inst is self or
236+
caller.inst.is_ancestor_of(self) or
237+
self.is_ancestor_of(caller.inst))
238+
239+
class RootComponentInstance(ComponentInstance):
240+
may_enter: bool
241+
242+
def __init__(self, store):
243+
ComponentInstance.__init__(self, store, parent = None)
244+
self.may_enter = True
238245

239246
## Concurrency Primitives
240247

@@ -546,15 +553,27 @@ def current_instance() -> ComponentInstance:
546553

547554
class Store:
548555
waiting: list[Thread]
556+
entered: int
549557

550558
def __init__(self):
551559
self.waiting = []
560+
self.entered = 0
552561

553562
def lift(self, callee, opts: CanonicalOptions, ft: FuncType, inst: ComponentInstance) -> FuncInst:
554563
def func_inst(caller: Supertask, on_start, on_resolve) -> OnCancel:
555-
trap_if(inst.call_might_be_recursive(caller))
564+
self.entered += 1
565+
intra_component = caller.inst is not None and caller.inst.root() is inst.root()
566+
if intra_component:
567+
trap_if(inst.intra_component_call_might_be_recursive(caller))
568+
else:
569+
trap_if(inst.host_call_is_recursive(caller))
570+
trap_if(not inst.root().may_enter)
571+
inst.root().may_enter = False
556572
task = Task(caller, inst, ft, opts, on_start, on_resolve)
557573
Thread(task, lambda: canon_lift(callee)).resume()
574+
if not intra_component:
575+
inst.root().may_enter = True
576+
self.entered -= 1
558577
return task.request_cancellation
559578
return func_inst
560579

@@ -565,10 +584,14 @@ def invoke(self, f: FuncInst, caller: Optional[Supertask], on_start, on_resolve)
565584
return f(host_caller, on_start, on_resolve)
566585

567586
def tick(self):
587+
assert(self.entered == 0)
568588
random.shuffle(self.waiting)
569589
for thread in self.waiting:
570590
if thread.ready():
591+
assert(thread.task.inst.root().may_enter)
592+
thread.task.inst.root().may_enter = False
571593
thread.resume()
594+
thread.task.inst.root().may_enter = True
572595
return
573596

574597
## Lifting and Lowering Context

0 commit comments

Comments
 (0)