Skip to content

Commit a7029a6

Browse files
authored
CABI: trap if closing writable future without having written a value (#521)
1 parent e3317f2 commit a7029a6

File tree

4 files changed

+136
-2
lines changed

4 files changed

+136
-2
lines changed

design/mvp/CanonicalABI.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,14 +1485,15 @@ class WritableFutureEnd(FutureEnd):
14851485
def copy(self, inst, src, on_partial_copy, on_copy_done):
14861486
return self.close_after_copy(self.shared.write, inst, src, on_copy_done)
14871487
def drop(self):
1488+
trap_if(not self.shared.closed())
14881489
FutureEnd.drop(self)
14891490
```
14901491
The `future.{read,write}` built-ins fix the buffer length to `1`, ensuring the
14911492
`assert(buffer.remain() == 1)` holds. Because of this, there are no partial
14921493
copies and `on_partial_copy` is never called.
14931494

1494-
The additional `trap_if` in `WritableFutureEnd.drop` ensures that the only
1495-
valid way close a future without writing its value is to close it in error.
1495+
The additional `trap_if` in `WritableFutureEnd.drop` ensures that a future
1496+
must have written a value before closing.
14961497

14971498

14981499
### Despecialization

design/mvp/canonical-abi/definitions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,7 @@ class WritableFutureEnd(FutureEnd):
873873
def copy(self, inst, src, on_partial_copy, on_copy_done):
874874
return self.close_after_copy(self.shared.write, inst, src, on_copy_done)
875875
def drop(self):
876+
trap_if(not self.shared.closed())
876877
FutureEnd.drop(self)
877878

878879
### Despecialization

design/mvp/canonical-abi/run_tests.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1921,6 +1921,15 @@ async def core_func(task, args):
19211921
[] = await canon_future_close_writable(FutureType(U8Type()), task, wfi)
19221922
[] = await canon_future_close_readable(FutureType(U8Type()), task, rfi)
19231923

1924+
[packed] = await canon_future_new(FutureType(U8Type()), task)
1925+
rfi,wfi = unpack_new_ends(packed)
1926+
trapped = False
1927+
try:
1928+
await canon_future_close_writable(FutureType(U8Type()), task, wfi)
1929+
except Trap:
1930+
trapped = True
1931+
assert(trapped)
1932+
19241933
return []
19251934

19261935
await canon_lift(lift_opts, inst, FuncType([],[]), core_func, None, lambda:[], lambda _:(), host_on_block)

test/async/futures-must-write.wast

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
;; This test contains two components $C and $D that test that a trap occurs
2+
;; when closing the writable end of a future (in $C) before having written
3+
;; a value while closing the readable end of a future (in $D) before reading
4+
;; a value is fine.
5+
(component
6+
(component $C
7+
(core module $Memory (memory (export "mem") 1))
8+
(core instance $memory (instantiate $Memory))
9+
(core module $CM
10+
(import "" "mem" (memory 1))
11+
(import "" "future.new" (func $future.new (result i64)))
12+
(import "" "future.write" (func $future.write (param i32 i32) (result i32)))
13+
(import "" "future.close-writable" (func $future.close-writable (param i32)))
14+
15+
(global $fw (mut i32) (i32.const 0))
16+
17+
(func $start-future (export "start-future") (result i32)
18+
;; create a new future, return the readable end to the caller
19+
(local $ret64 i64)
20+
(local.set $ret64 (call $future.new))
21+
(global.set $fw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32))))
22+
(i32.wrap_i64 (local.get $ret64))
23+
)
24+
(func $attempt-write (export "attempt-write") (result i32)
25+
;; because the caller already closed the readable end, this write will eagerly
26+
;; return CLOSED having written no values.
27+
(local $ret i32)
28+
(local.set $ret (call $future.write (global.get $fw) (i32.const 42)))
29+
(if (i32.ne (i32.const 0x01 (; CLOSED=1 | (0<<4) ;)) (local.get $ret))
30+
(then
31+
(i32.load (i32.add (local.get $ret) (i32.const 0x8000_0000)))
32+
unreachable))
33+
34+
;; return without trapping
35+
(i32.const 42)
36+
)
37+
(func $close-writable (export "close-writable")
38+
;; maybe boom
39+
(call $future.close-writable (global.get $fw))
40+
)
41+
)
42+
(type $FT (future u8))
43+
(canon future.new $FT (core func $future.new))
44+
(canon future.write $FT async (memory $memory "mem") (core func $future.write))
45+
(canon future.close-writable $FT (core func $future.close-writable))
46+
(core instance $cm (instantiate $CM (with "" (instance
47+
(export "mem" (memory $memory "mem"))
48+
(export "future.new" (func $future.new))
49+
(export "future.write" (func $future.write))
50+
(export "future.close-writable" (func $future.close-writable))
51+
))))
52+
(func (export "start-future") (result (future u8)) (canon lift (core func $cm "start-future")))
53+
(func (export "attempt-write") (result u32) (canon lift (core func $cm "attempt-write")))
54+
(func (export "close-writable") (canon lift (core func $cm "close-writable")))
55+
)
56+
(component $D
57+
(import "c" (instance $c
58+
(export "start-future" (func (result (future u8))))
59+
(export "attempt-write" (func (result u32)))
60+
(export "close-writable" (func))
61+
))
62+
63+
(core module $Memory (memory (export "mem") 1))
64+
(core instance $memory (instantiate $Memory))
65+
(core module $Core
66+
(import "" "mem" (memory 1))
67+
(import "" "future.read" (func $future.read (param i32 i32) (result i32)))
68+
(import "" "future.close-readable" (func $future.close-readable (param i32)))
69+
(import "" "start-future" (func $start-future (result i32)))
70+
(import "" "attempt-write" (func $attempt-write (result i32)))
71+
(import "" "close-writable" (func $close-writable))
72+
73+
(func $close-readable-future-before-read (export "close-readable-future-before-read") (result i32)
74+
;; call 'start-future' to get the future we'll be working with
75+
(local $fr i32)
76+
(local.set $fr (call $start-future))
77+
(if (i32.ne (i32.const 1) (local.get $fr))
78+
(then unreachable))
79+
80+
;; ok to immediately close the readable end
81+
(call $future.close-readable (local.get $fr))
82+
83+
;; the callee will see that we closed the readable end when it tries to write
84+
(call $attempt-write)
85+
)
86+
(func $close-writable-future-before-write (export "close-writable-future-before-write")
87+
;; call 'start-future' to get the future we'll be working with
88+
(local $fr i32)
89+
(local.set $fr (call $start-future))
90+
(if (i32.ne (i32.const 1) (local.get $fr))
91+
(then unreachable))
92+
93+
;; boom
94+
(call $close-writable)
95+
)
96+
)
97+
(type $FT (future u8))
98+
(canon future.new $FT (core func $future.new))
99+
(canon future.read $FT async (memory $memory "mem") (core func $future.read))
100+
(canon future.close-readable $FT (core func $future.close-readable))
101+
(canon lower (func $c "start-future") (core func $start-future'))
102+
(canon lower (func $c "attempt-write") (core func $attempt-write'))
103+
(canon lower (func $c "close-writable") (core func $close-writable'))
104+
(core instance $core (instantiate $Core (with "" (instance
105+
(export "mem" (memory $memory "mem"))
106+
(export "future.new" (func $future.new))
107+
(export "future.read" (func $future.read))
108+
(export "future.close-readable" (func $future.close-readable))
109+
(export "start-future" (func $start-future'))
110+
(export "attempt-write" (func $attempt-write'))
111+
(export "close-writable" (func $close-writable'))
112+
))))
113+
(func (export "close-readable-future-before-read") (result u32) (canon lift (core func $core "close-readable-future-before-read")))
114+
(func (export "close-writable-future-before-write") (canon lift (core func $core "close-writable-future-before-write")))
115+
)
116+
(instance $c (instantiate $C))
117+
(instance $d (instantiate $D (with "c" (instance $c))))
118+
(func (export "close-writable-future-before-write") (alias export $d "close-writable-future-before-write"))
119+
(func (export "close-readable-future-before-read") (alias export $d "close-readable-future-before-read"))
120+
)
121+
122+
(assert_return (invoke "close-readable-future-before-read") (u32.const 42))
123+
(assert_trap (invoke "close-writable-future-before-write") "cannot close future write end without first writing a value")

0 commit comments

Comments
 (0)