Skip to content

Commit 85cf4d5

Browse files
authored
Merge pull request #149 from WebAssembly/fix-reentrancy
Catch destructor reentrance
2 parents 4c0ce2b + 80510d4 commit 85cf4d5

File tree

3 files changed

+52
-43
lines changed

3 files changed

+52
-43
lines changed

design/mvp/CanonicalABI.md

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ class Context:
219219
opts: CanonicalOptions
220220
inst: ComponentInstance
221221
call: Call
222-
called_as_export: bool
223222
```
224223

225224
The `opts` field represents the [`canonopt`] values supplied to
@@ -250,20 +249,17 @@ class ComponentInstance:
250249
self.handles = HandleTable()
251250
```
252251

253-
Lastly, the `called_as_export` field of `Context` indicates whether the lifted
254-
function is being called through a component export or whether this is an
255-
internal call (for example, when a child component calls an import that is
256-
defined by its parent component).
257-
258252
The `HandleTable` class is defined in terms of a collection of supporting
259253
runtime bookkeeping classes that we'll go through first.
260254

261255
The `Resource` class represents a runtime instance of a resource type, storing
262-
the core representation value (which is currently fixed to `i32`):
256+
the core representation value (which is currently fixed to `i32`) and the
257+
component instance that is implementing this resource.
263258
```python
264259
@dataclass
265260
class Resource:
266261
rep: int
262+
impl: ComponentInstance
267263
```
268264

269265
The `OwnHandle` and `BorrowHandle` classes represent runtime handle values of
@@ -1422,11 +1418,7 @@ component*.
14221418
Given the above closure arguments, `canon_lift` is defined:
14231419
```python
14241420
def canon_lift(cx, callee, ft, call, args):
1425-
if cx.called_as_export:
1426-
trap_if(not cx.inst.may_enter)
1427-
cx.inst.may_enter = False
1428-
else:
1429-
assert(not cx.inst.may_enter)
1421+
trap_if(not cx.inst.may_enter)
14301422

14311423
outer_call = cx.call
14321424
cx.call = call
@@ -1447,9 +1439,6 @@ def canon_lift(cx, callee, ft, call, args):
14471439
if cx.opts.post_return is not None:
14481440
cx.opts.post_return(flat_results)
14491441

1450-
if cx.called_as_export:
1451-
cx.inst.may_enter = True
1452-
14531442
cx.call.finish_lift()
14541443
cx.call = outer_call
14551444

@@ -1462,14 +1451,6 @@ boundaries. Thus, if a component wishes to signal an error, it must use some
14621451
sort of explicit type such as `result` (whose `error` case particular language
14631452
bindings may choose to map to and from exceptions).
14641453

1465-
By clearing `may_enter` for the duration of `canon_lift` when the function is
1466-
called as an export, the dynamic traps ensure that components cannot be
1467-
reentered, ensuring the non-reentrance [component invariant]. Furthermore,
1468-
because `may_enter` is not cleared on the exceptional exit path taken by
1469-
`trap()`, if there is a trap during Core WebAssembly execution of lifting or
1470-
lowering, the component is left permanently un-enterable, ensuring the
1471-
lockdown-after-trap [component invariant].
1472-
14731454
The `call` parameter is assumed to have been created by the caller (the host or
14741455
`canon lower`) for this one call. Since, in `not cx.called_as_export` scenarios
14751456
a single component instance may be reentered (by its children), `cx.call` must
@@ -1500,9 +1481,13 @@ Thus, from the perspective of Core WebAssembly, `$f` is a [function instance]
15001481
containing a `hostfunc` that closes over `$opts`, `$inst`, `$callee` and `$ft`
15011482
and, when called from Core WebAssembly code, calls `canon_lower`, which is defined as:
15021483
```python
1503-
def canon_lower(cx, callee, ft, flat_args):
1484+
def canon_lower(cx, callee, calling_import, ft, flat_args):
15041485
trap_if(not cx.inst.may_leave)
15051486

1487+
assert(cx.inst.may_enter)
1488+
if calling_import:
1489+
cx.inst.may_enter = False
1490+
15061491
outer_call = cx.call
15071492
cx.call = Call()
15081493

@@ -1520,6 +1505,9 @@ def canon_lower(cx, callee, ft, flat_args):
15201505
cx.call.finish_lower()
15211506
cx.call = outer_call
15221507

1508+
if calling_import:
1509+
cx.inst.may_enter = True
1510+
15231511
return flat_results
15241512
```
15251513
The definitions of `canon_lift` and `canon_lower` are mostly symmetric (swapping
@@ -1538,6 +1526,20 @@ compilation of the permissive [subtyping](Subtyping.md) allowed between
15381526
components (including the elimination of string operations on the labels of
15391527
records and variants) as well as post-MVP [adapter functions].
15401528

1529+
By clearing `may_enter` for the duration of calls to imports, the `may_enter`
1530+
guard in `canon_lift` ensures that components cannot be externally reentered,
1531+
which is part of the [component invariants]. The `calling_import` condition
1532+
allows a parent component to call into a child component (which is, by
1533+
definition, not a call to an import) and for the child to then reenter the
1534+
parent through a function the parent explicitly supplied to the child's
1535+
`instantiate`. This form of internal reentrance allows the parent to fully
1536+
virtualize the child's imports.
1537+
1538+
Because `may_enter` is not cleared on the exceptional exit path taken by
1539+
`trap()`, if there is a trap during Core WebAssembly execution of lifting or
1540+
lowering, the component is left permanently un-enterable, ensuring the
1541+
lockdown-after-trap [component invariant].
1542+
15411543
The `may_leave` flag set during lowering in `canon_lift` and `canon_lower`
15421544
ensures that the relative ordering of the side effects of `lift` and `lower`
15431545
cannot be observed via import calls and thus an implementation may reliably
@@ -1567,7 +1569,7 @@ Calling `$f` invokes the following function, which creates a resource object
15671569
and inserts it into the current instance's handle table:
15681570
```python
15691571
def canon_resource_new(cx, rt, rep):
1570-
h = OwnHandle(Resource(rep), rt)
1572+
h = OwnHandle(Resource(rep, cx.inst), rt)
15711573
return cx.inst.handles.insert(cx, h)
15721574
```
15731575

@@ -1588,8 +1590,11 @@ optional destructor.
15881590
def canon_resource_drop(cx, t, i):
15891591
h = cx.inst.handles.remove(cx, i, t)
15901592
if isinstance(t, Own) and t.rt.dtor:
1593+
trap_if(not h.resource.impl.may_enter)
15911594
t.rt.dtor(h.resource.rep)
15921595
```
1596+
The `may_enter` guard ensures the non-reentrance [component invariant], since
1597+
a destructor call is analogous to a call to an export.
15931598

15941599
### `canon resource.rep`
15951600

@@ -1618,6 +1623,7 @@ def canon_resource_rep(cx, rt, i):
16181623
[`canonopt`]: Explainer.md#canonical-definitions
16191624
[`canon`]: Explainer.md#canonical-definitions
16201625
[Type Definitions]: Explainer.md#type-definitions
1626+
[Component Invariant]: Explainer.md#component-invariants
16211627
[Component Invariants]: Explainer.md#component-invariants
16221628
[JavaScript Embedding]: Explainer.md#JavaScript-embedding
16231629
[Adapter Functions]: FutureFeatures.md#custom-abis-via-adapter-functions

design/mvp/canonical-abi/definitions.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,6 @@ class Context:
291291
opts: CanonicalOptions
292292
inst: ComponentInstance
293293
call: Call
294-
called_as_export: bool
295294

296295
#
297296

@@ -318,6 +317,7 @@ def __init__(self):
318317
@dataclass
319318
class Resource:
320319
rep: int
320+
impl: ComponentInstance
321321

322322
#
323323

@@ -1151,11 +1151,7 @@ def lower_values(cx, max_flat, vs, ts, out_param = None):
11511151
### `lift`
11521152

11531153
def canon_lift(cx, callee, ft, call, args):
1154-
if cx.called_as_export:
1155-
trap_if(not cx.inst.may_enter)
1156-
cx.inst.may_enter = False
1157-
else:
1158-
assert(not cx.inst.may_enter)
1154+
trap_if(not cx.inst.may_enter)
11591155

11601156
outer_call = cx.call
11611157
cx.call = call
@@ -1176,19 +1172,20 @@ def post_return():
11761172
if cx.opts.post_return is not None:
11771173
cx.opts.post_return(flat_results)
11781174

1179-
if cx.called_as_export:
1180-
cx.inst.may_enter = True
1181-
11821175
cx.call.finish_lift()
11831176
cx.call = outer_call
11841177

11851178
return (results, post_return)
11861179

11871180
### `lower`
11881181

1189-
def canon_lower(cx, callee, ft, flat_args):
1182+
def canon_lower(cx, callee, calling_import, ft, flat_args):
11901183
trap_if(not cx.inst.may_leave)
11911184

1185+
assert(cx.inst.may_enter)
1186+
if calling_import:
1187+
cx.inst.may_enter = False
1188+
11921189
outer_call = cx.call
11931190
cx.call = Call()
11941191

@@ -1206,19 +1203,23 @@ def canon_lower(cx, callee, ft, flat_args):
12061203
cx.call.finish_lower()
12071204
cx.call = outer_call
12081205

1206+
if calling_import:
1207+
cx.inst.may_enter = True
1208+
12091209
return flat_results
12101210

12111211
### `resource.new`
12121212

12131213
def canon_resource_new(cx, rt, rep):
1214-
h = OwnHandle(Resource(rep), rt)
1214+
h = OwnHandle(Resource(rep, cx.inst), rt)
12151215
return cx.inst.handles.insert(cx, h)
12161216

12171217
### `resource.drop`
12181218

12191219
def canon_resource_drop(cx, t, i):
12201220
h = cx.inst.handles.remove(cx, i, t)
12211221
if isinstance(t, Own) and t.rt.dtor:
1222+
trap_if(not h.resource.impl.may_enter)
12221223
t.rt.dtor(h.resource.rep)
12231224

12241225
### `resource.rep`

design/mvp/canonical-abi/run_tests.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ def test_roundtrip(t, v):
356356
caller_cx = mk_cx(caller_heap.memory, 'utf8', caller_heap.realloc)
357357

358358
flat_args = lower_flat(caller_cx, v, t)
359-
flat_results = canon_lower(caller_cx, lifted_callee, ft, flat_args)
359+
flat_results = canon_lower(caller_cx, lifted_callee, True, ft, flat_args)
360360
got = lift_flat(caller_cx, ValueIter(flat_results), t)
361361

362362
if got != v:
@@ -381,17 +381,19 @@ def dtor(x):
381381
nonlocal dtor_value
382382
dtor_value = x
383383
rt = ResourceType(dtor)
384-
r1 = Resource(42)
385-
r2 = Resource(43)
386-
r3 = Resource(44)
384+
385+
cx = mk_cx()
386+
r1 = Resource(42, cx.inst)
387+
r2 = Resource(43, cx.inst)
388+
r3 = Resource(44, cx.inst)
387389

388390
def host_import(act, args):
391+
nonlocal cx
389392
assert(len(args) == 2)
390393
assert(args[0] is r1)
391394
assert(args[1] is r3)
392-
return ([Resource(45)], lambda:())
395+
return ([Resource(45, cx.inst)], lambda:())
393396

394-
cx = mk_cx()
395397
def core_wasm(args):
396398
nonlocal dtor_value
397399

@@ -404,7 +406,7 @@ def core_wasm(args):
404406
assert(canon_resource_rep(cx, rt, 2) == 44)
405407

406408
host_ft = FuncType([Borrow(rt),Borrow(rt)],[Own(rt)])
407-
results = canon_lower(cx, host_import, host_ft, [Value('i32',0),Value('i32',2)])
409+
results = canon_lower(cx, host_import, True, host_ft, [Value('i32',0),Value('i32',2)])
408410
assert(len(results) == 1)
409411
assert(results[0].t == 'i32' and results[0].v == 3)
410412
assert(canon_resource_rep(cx, rt, 3) == 45)

0 commit comments

Comments
 (0)