Skip to content

Commit 4137f4f

Browse files
authored
relax intra-component stream/future read/write rules (#12181)
Per WebAssembly/component-model#580, we now allow intra-component reads and writes for all numeric payloads. While I was implementing this, Alex and I spotted a couple of bugs, which I've fixed here: - We can't treat `bool` and `char` payloads as "flat" (i.e. trivially copy-able) since not all bit patterns are valid for those types. - We weren't enforcing the rules (old or new) for streams with non-"flat" payloads (i.e. the ones that most needed them enforced 🤦) Also, now that WebAssembly/component-model#578 has been merged, I've updated the `tests/component-model` submodule and reduced the list of "expect fail" tests for that directory. Finally, note that the `p3_http_middleware_host_to_host` test is still expected to trap, but now for a different reason. Previously, it trapped because it tried to do an intra-component read/write on a `stream<u8>`. Now that that's allowed, it instead traps when doing an intra-component read/write on a `future<result<option<trailers>, error-code>>`, which is still prohibited. Signed-off-by: Joel Dice <[email protected]>
1 parent 4898322 commit 4137f4f

File tree

7 files changed

+87
-48
lines changed

7 files changed

+87
-48
lines changed

crates/cranelift/src/compiler/component.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -856,17 +856,19 @@ impl<'a> TrampolineCompiler<'a> {
856856
/// Determine whether the specified type can be optimized as a stream
857857
/// payload by lifting and lowering with a simple `memcpy`.
858858
///
859-
/// Any type containing only "flat", primitive data (i.e. no pointers or
860-
/// handles) should qualify for this optimization, but it's also okay to
861-
/// conservatively return `None` here; the fallback slow path will always
862-
/// work -- it just won't be as efficient.
859+
/// Any type containing only "flat", primitive data for which all bit
860+
/// patterns are valid (i.e. no pointers, handles, bools, or chars) should
861+
/// qualify for this optimization, but it's also okay to conservatively
862+
/// return `None` here; the fallback slow path will always work -- it just
863+
/// won't be as efficient.
863864
fn flat_stream_element_info(&self, ty: TypeStreamTableIndex) -> Option<&CanonicalAbiInfo> {
864865
let payload = self.types[self.types[ty].ty].payload;
865866
match payload {
866867
None => Some(&CanonicalAbiInfo::ZERO),
867868
Some(
868-
payload @ (InterfaceType::Bool
869-
| InterfaceType::S8
869+
// Note that we exclude `Bool` and `Char` from this list because
870+
// not all bit patterns are valid for those types.
871+
payload @ (InterfaceType::S8
870872
| InterfaceType::U8
871873
| InterfaceType::S16
872874
| InterfaceType::U16
@@ -875,8 +877,7 @@ impl<'a> TrampolineCompiler<'a> {
875877
| InterfaceType::S64
876878
| InterfaceType::U64
877879
| InterfaceType::Float32
878-
| InterfaceType::Float64
879-
| InterfaceType::Char),
880+
| InterfaceType::Float64),
880881
) => Some(self.types.canonical_abi(&payload)),
881882
// TODO: Recursively check for other "flat" types (i.e. those without pointers or handles),
882883
// e.g. `record`s, `variant`s, etc. which contain only flat types.

crates/test-util/src/wast.rs

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ fn component_test_config(test: &Path) -> TestConfig {
186186
if let Some(parent) = test.parent() {
187187
if parent.ends_with("async") {
188188
ret.component_model_async = Some(true);
189+
ret.component_model_async_stackful = Some(true);
189190
ret.component_model_async_builtins = Some(true);
191+
ret.component_model_threading = Some(true);
190192
}
191193
if parent.ends_with("wasm-tools") {
192194
ret.memory64 = Some(true);
@@ -699,26 +701,12 @@ impl WastTest {
699701
"component-model/test/values/trap-in-post-return.wast",
700702
"component-model/test/wasmtime/resources.wast",
701703
"component-model/test/wasm-tools/naming.wast",
702-
// TODO: remove these once
703-
// https://github.com/WebAssembly/component-model/pull/578 has been
704-
// merged:
705-
"component-model/test/async/async-calls-sync.wast",
706-
"component-model/test/async/backpressure-deadlock.wast",
707-
"component-model/test/async/cancel-stream.wast",
708-
"component-model/test/async/cancel-subtask.wast",
709-
"component-model/test/async/deadlock.wast",
710-
"component-model/test/async/drop-subtask.wast",
711-
"component-model/test/async/drop-waitable-set.wast",
712-
"component-model/test/async/empty-wait.wast",
713-
"component-model/test/async/fused.wast",
714-
"component-model/test/async/future-read.wast",
715-
"component-model/test/async/partial-stream-copies.wast",
716-
"component-model/test/async/passing-resources.wast",
717-
"component-model/test/async/stackful.wast",
704+
// FIXME(#12129)
718705
"component-model/test/async/trap-if-block-and-sync.wast",
719-
"component-model/test/async/trap-if-done.wast",
720-
"component-model/test/async/wait-during-callback.wast",
721-
"component-model/test/async/zero-length.wast",
706+
// TODO: Remove this once
707+
// https://github.com/bytecodealliance/wasm-tools/pull/2406 is
708+
// merged and released, and Wasmtime has been updated to use it:
709+
"component-model/test/async/same-component-stream-future.wast",
722710
];
723711
if failing_component_model_tests
724712
.iter()

crates/wasi-http/tests/all/p3/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ async fn p3_http_middleware() -> Result<()> {
333333
async fn p3_http_middleware_host_to_host() {
334334
let error = format!("{:?}", test_http_middleware(true).await.unwrap_err());
335335

336-
let expected = "cannot read from and write to intra-component stream with non-unit payload";
336+
let expected = "cannot read from and write to intra-component future with non-numeric payload";
337337

338338
assert!(
339339
error.contains(expected),

crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3042,9 +3042,9 @@ impl Instance {
30423042

30433043
let payload = types[types[write_ty].ty].payload;
30443044

3045-
if write_caller == read_caller && payload.is_some() {
3045+
if write_caller == read_caller && !allow_intra_component_read_write(payload) {
30463046
bail!(
3047-
"cannot read from and write to intra-component future with non-unit payload"
3047+
"cannot read from and write to intra-component future with non-numeric payload"
30483048
)
30493049
}
30503050

@@ -3084,13 +3084,15 @@ impl Instance {
30843084
}
30853085
}
30863086
(TransmitIndex::Stream(write_ty), TransmitIndex::Stream(read_ty)) => {
3087-
if let Some(flat_abi) = flat_abi {
3088-
if write_caller == read_caller && types[types[write_ty].ty].payload.is_some() {
3089-
bail!(
3090-
"cannot read from and write to intra-component stream with non-unit payload"
3091-
)
3092-
}
3087+
if write_caller == read_caller
3088+
&& !allow_intra_component_read_write(types[types[write_ty].ty].payload)
3089+
{
3090+
bail!(
3091+
"cannot read from and write to intra-component stream with non-numeric payload"
3092+
)
3093+
}
30933094

3095+
if let Some(flat_abi) = flat_abi {
30943096
// Fast path memcpy for "flat" (i.e. no pointers or handles) payloads:
30953097
let length_in_bytes = usize::try_from(flat_abi.size).unwrap() * count;
30963098
if length_in_bytes > 0 {
@@ -3122,7 +3124,20 @@ impl Instance {
31223124
.as_mut_ptr();
31233125
// SAFETY: Both `src` and `dst` have been validated
31243126
// above.
3125-
unsafe { src.copy_to(dst, length_in_bytes) };
3127+
unsafe {
3128+
if write_caller == read_caller {
3129+
// If the same instance owns both ends of
3130+
// the stream, the source and destination
3131+
// buffers might overlap.
3132+
src.copy_to(dst, length_in_bytes)
3133+
} else {
3134+
// Since the read and write ends of the
3135+
// stream are owned by distinct instances,
3136+
// the buffers cannot possibly belong to the
3137+
// same memory and thus cannot overlap.
3138+
src.copy_to_nonoverlapping(dst, length_in_bytes)
3139+
}
3140+
}
31263141
}
31273142
}
31283143
} else {
@@ -4533,6 +4548,27 @@ impl Waitable {
45334548
}
45344549
}
45354550

4551+
/// Determine whether an intra-component read/write is allowed for the specified
4552+
/// `stream` or `future` payload type according to the component model
4553+
/// specification.
4554+
fn allow_intra_component_read_write(ty: Option<InterfaceType>) -> bool {
4555+
matches!(
4556+
ty,
4557+
None | Some(
4558+
InterfaceType::S8
4559+
| InterfaceType::U8
4560+
| InterfaceType::S16
4561+
| InterfaceType::U16
4562+
| InterfaceType::S32
4563+
| InterfaceType::U32
4564+
| InterfaceType::S64
4565+
| InterfaceType::U64
4566+
| InterfaceType::Float32
4567+
| InterfaceType::Float64
4568+
)
4569+
)
4570+
}
4571+
45364572
#[cfg(test)]
45374573
mod tests {
45384574
use super::*;

tests/misc_testsuite/component-model/async/intra-futures.wast

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
;;! component_model_async_builtins = true
33

44
(component
5-
(core module $libc (memory (export "m") 1))
5+
(core module $libc
6+
(func (export "realloc") (param i32 i32 i32 i32) (result i32)
7+
unreachable
8+
)
9+
(memory (export "m") 1)
10+
)
611
(core instance $libc (instantiate $libc))
712

8-
(type $s (future u32))
13+
(type $s (future string))
914
(core func $future.new (canon future.new $s))
10-
(core func $future.read (canon future.read $s async (memory $libc "m")))
15+
(core func $future.read (canon future.read $s async (memory $libc "m") (realloc (func $libc "realloc"))))
1116
(core func $future.write (canon future.write $s async (memory $libc "m")))
1217

1318
(core module $m
19+
(import "" "m" (memory 1))
1420
(import "" "future.new" (func $future.new (result i64)))
1521
(import "" "future.read" (func $future.read (param i32 i32) (result i32)))
1622
(import "" "future.write" (func $future.write (param i32 i32) (result i32)))
@@ -36,6 +42,7 @@
3642

3743
(core instance $i (instantiate $m
3844
(with "" (instance
45+
(export "m" (memory $libc "m"))
3946
(export "future.new" (func $future.new))
4047
(export "future.read" (func $future.read))
4148
(export "future.write" (func $future.write))
@@ -45,4 +52,4 @@
4552
(func (export "run") (canon lift (core func $i "run")))
4653
)
4754

48-
(assert_trap (invoke "run") "cannot read from and write to intra-component future with non-unit payload")
55+
(assert_trap (invoke "run") "cannot read from and write to intra-component future with non-numeric payload")

tests/misc_testsuite/component-model/async/intra-streams.wast

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
;;! component_model_async_builtins = true
33

44
(component
5-
(core module $libc (memory (export "m") 1))
5+
(core module $libc
6+
(func (export "realloc") (param i32 i32 i32 i32) (result i32)
7+
(i32.const 8)
8+
)
9+
(memory (export "m") 1)
10+
)
611
(core instance $libc (instantiate $libc))
712

8-
(type $s (stream u32))
13+
(type $s (stream string))
914
(core func $stream.new (canon stream.new $s))
10-
(core func $stream.read (canon stream.read $s async (memory $libc "m")))
15+
(core func $stream.read (canon stream.read $s async (memory $libc "m") (realloc (func $libc "realloc"))))
1116
(core func $stream.write (canon stream.write $s async (memory $libc "m")))
1217

1318
(core module $m
19+
(import "" "m" (memory 1))
1420
(import "" "stream.new" (func $stream.new (result i64)))
1521
(import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32)))
1622
(import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32)))
@@ -24,18 +30,19 @@
2430
(local.set $r (i32.wrap_i64 (local.get $tmp)))
2531
(local.set $w (i32.wrap_i64 (i64.shr_u (local.get $tmp) (i64.const 32))))
2632

27-
(call $stream.read (local.get $r) (i32.const 0) (i32.const 4))
33+
(call $stream.read (local.get $r) (i32.const 0) (i32.const 1))
2834
i32.const -1 ;; BLOCKED
2935
i32.ne
3036
if unreachable end
3137

32-
(call $stream.write (local.get $w) (i32.const 0) (i32.const 4))
38+
(call $stream.write (local.get $w) (i32.const 0) (i32.const 1))
3339
drop
3440
)
3541
)
3642

3743
(core instance $i (instantiate $m
3844
(with "" (instance
45+
(export "m" (memory $libc "m"))
3946
(export "stream.new" (func $stream.new))
4047
(export "stream.read" (func $stream.read))
4148
(export "stream.write" (func $stream.write))
@@ -45,4 +52,4 @@
4552
(func (export "run") (canon lift (core func $i "run")))
4653
)
4754

48-
(assert_trap (invoke "run") "cannot read from and write to intra-component stream with non-unit payload")
55+
(assert_trap (invoke "run") "cannot read from and write to intra-component stream with non-numeric payload")

0 commit comments

Comments
 (0)