Skip to content

Commit 376e970

Browse files
committed
CABI: check ptr bounds for zero-length reads/writes
1 parent d095e2f commit 376e970

4 files changed

Lines changed: 114 additions & 43 deletions

File tree

‎design/mvp/CanonicalABI.md‎

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,7 +1551,7 @@ class GuestBuffer(Buffer):
15511551

15521552
def __init__(self, t, cx, ptr, length):
15531553
trap_if(length > Buffer.MAX_LENGTH)
1554-
if t and length > 0:
1554+
if t is not None:
15551555
trap_if(ptr != align_to(ptr, alignment(t, cx.opts.memory.ptr_type())))
15561556
trap_if(ptr + length * elem_size(t, cx.opts.memory.ptr_type()) > len(cx.opts.memory))
15571557
self.cx = cx
@@ -1584,11 +1584,13 @@ class WritableGuestBuffer(GuestBuffer, WritableBuffer):
15841584
assert(all(v == () for v in vs))
15851585
self.progress += len(vs)
15861586
```
1587-
When `t` is `None` (arising from `stream` and `future` with empty element
1588-
types), the core-wasm-supplied `ptr` is entirely ignored, while the `length`
1589-
and `progress` are still semantically meaningful. Source bindings may represent
1590-
this case with a generic stream/future of [unit] type or a distinct type that
1591-
conveys events without values.
1587+
As with `load_list_from_range` below, the alignment and range checks are
1588+
performed even when `length` is `0`. When `t` is `None` (arising from `stream`
1589+
and `future` with empty element types), the core-wasm-supplied `ptr` is
1590+
entirely ignored, while the `length` and `progress` are still semantically
1591+
meaningful. Source bindings may represent this case with a generic
1592+
stream/future of [unit] type or a distinct type that conveys events without
1593+
values.
15921594

15931595
The `load_list_from_valid_range` and `store_list_into_valid_range` functions
15941596
that do all the heavy lifting are shared with function parameter/result lifting
@@ -1691,30 +1693,33 @@ the code below:
16911693
assert(self.has_pending_event())
16921694
elif self.other.buffer is None:
16931695
self.buffer = buffer
1694-
elif buffer.remain() > 0 and self.other.buffer.remain() > 0:
1696+
else:
16951697
trap_if(self.owner and self.owner is self.other.owner and not none_or_number_type(self.t))
1696-
n = min(buffer.remain(), self.other.buffer.remain())
1697-
if is_read:
1698-
buffer.write(self.other.buffer.read(n))
1699-
else:
1700-
self.other.buffer.write(buffer.read(n))
1701-
self.notify(buffer.progress)
1702-
self.other.notify(self.other.buffer.progress)
1703-
if self.other.buffer.remain() == 0:
1698+
if buffer.remain() > 0 and self.other.buffer.remain() > 0:
1699+
n = min(buffer.remain(), self.other.buffer.remain())
1700+
if is_read:
1701+
buffer.write(self.other.buffer.read(n))
1702+
else:
1703+
self.other.buffer.write(buffer.read(n))
1704+
self.notify(buffer.progress)
1705+
self.other.notify(self.other.buffer.progress)
1706+
if self.other.buffer.remain() == 0:
1707+
self.other.buffer = None
1708+
elif buffer.remain() > 0 or (is_read and self.other.buffer.remain() == 0):
1709+
self.other.notify(progress = 0)
17041710
self.other.buffer = None
1705-
elif buffer.remain() > 0 or (is_read and self.other.buffer.remain() == 0):
1706-
self.other.notify(progress = 0)
1707-
self.other.buffer = None
1708-
self.buffer = buffer
1709-
else:
1710-
self.notify(progress = 0)
1711+
self.buffer = buffer
1712+
else:
1713+
self.notify(progress = 0)
17111714
```
17121715
As a temporary measure (until [lazy lowering] obviates the problem), there is
17131716
also a trap when both the `read` and `write` come from the same component
17141717
instance and there is a non-empty, non-number element type. The reason for this
17151718
trap is that when lifting and lowering can alias the same memory, the eager
17161719
interleaving semantics of copying compound values would otherwise be complex to
1717-
precisely specify and implement.
1720+
precisely specify and implement. The trap is checked whenever both ends have
1721+
provided a buffer (cases 3, 4 and 5), even if no elements end up being copied,
1722+
so that whether a trap occurs doesn't depend on the buffer lengths.
17181723

17191724
The `End.cancel` method is called by `{stream,future}.cancel-{read,write}` to
17201725
transition an end from `COPYING` (set by `End.copy` above) to `CANCELLING_COPY`.

‎design/mvp/Explainer.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2006,7 +2006,8 @@ enum copy-result {
20062006
The `stream.read` and `stream.write` built-ins take the matching [readable or
20072007
writable end] of a stream as the first parameter and, if `T` is present, a
20082008
buffer for the `T` values to be read from or written to. If `T` is not present,
2009-
the buffer parameter is ignored.
2009+
the buffer parameter is ignored. Otherwise, the buffer's alignment and bounds
2010+
are checked even when its length is `0`.
20102011

20112012
If the return value is a `stream-result`, then the `progress` field indicates how
20122013
many `T` elements were read or written from the given buffer before the

‎design/mvp/canonical-abi/definitions.py‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,7 @@ class GuestBuffer(Buffer):
895895

896896
def __init__(self, t, cx, ptr, length):
897897
trap_if(length > Buffer.MAX_LENGTH)
898-
if t and length > 0:
898+
if t is not None:
899899
trap_if(ptr != align_to(ptr, alignment(t, cx.opts.memory.ptr_type())))
900900
trap_if(ptr + length * elem_size(t, cx.opts.memory.ptr_type()) > len(cx.opts.memory))
901901
self.cx = cx
@@ -962,23 +962,24 @@ def copy(self, buffer: Buffer, is_read: bool):
962962
assert(self.has_pending_event())
963963
elif self.other.buffer is None:
964964
self.buffer = buffer
965-
elif buffer.remain() > 0 and self.other.buffer.remain() > 0:
965+
else:
966966
trap_if(self.owner and self.owner is self.other.owner and not none_or_number_type(self.t))
967-
n = min(buffer.remain(), self.other.buffer.remain())
968-
if is_read:
969-
buffer.write(self.other.buffer.read(n))
970-
else:
971-
self.other.buffer.write(buffer.read(n))
972-
self.notify(buffer.progress)
973-
self.other.notify(self.other.buffer.progress)
974-
if self.other.buffer.remain() == 0:
967+
if buffer.remain() > 0 and self.other.buffer.remain() > 0:
968+
n = min(buffer.remain(), self.other.buffer.remain())
969+
if is_read:
970+
buffer.write(self.other.buffer.read(n))
971+
else:
972+
self.other.buffer.write(buffer.read(n))
973+
self.notify(buffer.progress)
974+
self.other.notify(self.other.buffer.progress)
975+
if self.other.buffer.remain() == 0:
976+
self.other.buffer = None
977+
elif buffer.remain() > 0 or (is_read and self.other.buffer.remain() == 0):
978+
self.other.notify(progress = 0)
975979
self.other.buffer = None
976-
elif buffer.remain() > 0 or (is_read and self.other.buffer.remain() == 0):
977-
self.other.notify(progress = 0)
978-
self.other.buffer = None
979-
self.buffer = buffer
980-
else:
981-
self.notify(progress = 0)
980+
self.buffer = buffer
981+
else:
982+
self.notify(progress = 0)
982983

983984
def cancel(self):
984985
assert(self.state == End.State.COPYING)

‎design/mvp/canonical-abi/run_tests.py‎

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,7 +1978,7 @@ def core_func1(args):
19781978
result,n = unpack_result(mem1[retp+4])
19791979
assert(n == 4 and result == CopyResult.COMPLETED)
19801980

1981-
[ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 12345, 0)
1981+
[ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 0, 0)
19821982
assert(ret == definitions.BLOCKED)
19831983

19841984
fut4.set()
@@ -1988,7 +1988,7 @@ def core_func1(args):
19881988
assert(mem1[retp+0] == wsi)
19891989
assert(mem1[retp+4] == 0)
19901990

1991-
[ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 12345, 0)
1991+
[ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 0, 0)
19921992
assert(ret == 0)
19931993

19941994
[errctxi] = canon_error_context_new(opts1, 0, 0)
@@ -2032,7 +2032,7 @@ def core_func2(args):
20322032
fut2.set()
20332033
thread.wait_until(fut3.is_set)
20342034

2035-
[ret] = canon_stream_read(StreamType(U8Type()), opts2, rsi, 12345, 0)
2035+
[ret] = canon_stream_read(StreamType(U8Type()), opts2, rsi, 0, 0)
20362036
assert(ret == 0)
20372037

20382038
mem2[0:8] = bytes(8)
@@ -2047,7 +2047,7 @@ def core_func2(args):
20472047

20482048
thread.wait_until(fut4.is_set)
20492049

2050-
[ret] = canon_stream_read(StreamType(U8Type()), opts2, rsi, 12345, 0)
2050+
[ret] = canon_stream_read(StreamType(U8Type()), opts2, rsi, 0, 0)
20512051
assert(ret == definitions.BLOCKED)
20522052

20532053
[event] = canon_waitable_set_wait(MemInst(mem2, 'i32'), seti, retp)
@@ -2851,12 +2851,74 @@ def core_func(args):
28512851
[] = canon_stream_drop_readable(StreamType(elemt), rsi)
28522852
[] = canon_stream_drop_writable(StreamType(elemt), wsi)
28532853

2854+
# A zero-length buffer's pointer is still checked for alignment and bounds
2855+
# (though it may point at the very end of memory), unless the element type
2856+
# is empty, in which case the pointer is ignored.
2857+
[packed] = canon_stream_new(StreamType(elemt))
2858+
rsi,wsi = unpack_new_ends(packed)
2859+
if elemt is None:
2860+
bad_ptrs = []
2861+
ok_ptr = 0xdeadbeef
2862+
else:
2863+
align = alignment(elemt, 'i32')
2864+
bad_ptrs = [len(mem) + align] + ([1] if align > 1 else [])
2865+
ok_ptr = len(mem)
2866+
for ptr in bad_ptrs:
2867+
expect_fail(lambda: canon_stream_read(StreamType(elemt), async_opts, rsi, ptr, 0))
2868+
expect_fail(lambda: canon_stream_write(StreamType(elemt), async_opts, wsi, ptr, 0))
2869+
[ret] = canon_stream_read(StreamType(elemt), async_opts, rsi, ok_ptr, 0)
2870+
assert(ret == definitions.BLOCKED)
2871+
[ret] = canon_stream_write(StreamType(elemt), async_opts, wsi, ok_ptr, 0)
2872+
result,n = unpack_result(ret)
2873+
assert(n == 0 and result == CopyResult.COMPLETED)
2874+
[ret] = canon_stream_cancel_read(StreamType(elemt), True, rsi)
2875+
result,n = unpack_result(ret)
2876+
assert(n == 0 and result == CopyResult.CANCELLED)
2877+
[] = canon_stream_drop_readable(StreamType(elemt), rsi)
2878+
[] = canon_stream_drop_writable(StreamType(elemt), wsi)
2879+
28542880
[] = canon_waitable_set_drop(seti)
28552881
return []
28562882

28572883
lift_and_run(sync_opts, inst, ft, core_func, lambda:[], lambda _:())
28582884

28592885

2886+
def test_self_copy_trap(elemt):
2887+
store = Store()
2888+
inst = ComponentInstance(store)
2889+
mem = bytearray(40)
2890+
sync_opts = mk_opts(memory=MemInst(mem, 'i32'), async_=False)
2891+
async_opts = mk_opts(memory=MemInst(mem, 'i32'), async_=True)
2892+
future_t = FutureType(elemt)
2893+
stream_t = StreamType(elemt)
2894+
2895+
# Reading and writing the same future or stream from within the same
2896+
# component instance traps for non-number element types as soon as both ends
2897+
# have provided a buffer, even if no elements would be copied.
2898+
ft = FuncType([], [], async_ = True)
2899+
def core_func(args):
2900+
[packed] = canon_future_new(future_t)
2901+
rfi,wfi = unpack_new_ends(packed)
2902+
[ret] = canon_future_write(future_t, async_opts, wfi, 0)
2903+
assert(ret == definitions.BLOCKED)
2904+
expect_fail(lambda: canon_future_read(future_t, async_opts, rfi, 0))
2905+
2906+
for write_first in [True, False]:
2907+
for write_len in [0, 1]:
2908+
for read_len in [0, 1]:
2909+
[packed] = canon_stream_new(stream_t)
2910+
rsi,wsi = unpack_new_ends(packed)
2911+
write = lambda: canon_stream_write(stream_t, async_opts, wsi, 0, write_len)
2912+
read = lambda: canon_stream_read(stream_t, async_opts, rsi, 0, read_len)
2913+
first, second = (write, read) if write_first else (read, write)
2914+
[ret] = first()
2915+
assert(ret == definitions.BLOCKED)
2916+
expect_fail(second)
2917+
return []
2918+
2919+
lift_and_run(sync_opts, inst, ft, core_func, lambda:[], lambda _:())
2920+
2921+
28602922
def test_async_flat_params():
28612923
store = Store()
28622924
heap = Heap(1000)
@@ -3148,6 +3210,8 @@ def on_resolve(v):
31483210
test_self_copy(None)
31493211
test_self_copy(U8Type())
31503212
test_self_copy(F64Type())
3213+
test_self_copy_trap(BoolType())
3214+
test_self_copy_trap(StringType())
31513215
test_async_flat_params()
31523216
test_threads()
31533217
test_sync_threads()

0 commit comments

Comments
 (0)