Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wiggle: allow wiggle to use shared memory #5054

Merged
merged 1 commit into from
Nov 15, 2022

Conversation

abrown
Copy link
Contributor

@abrown abrown commented Oct 13, 2022

wiggle looks for an exported Memory named "memory" to use for its guest slices. This change allows it to use a SharedMemory if this is the kind of memory used for the export.

@abrown
Copy link
Contributor Author

abrown commented Oct 13, 2022

This was originally a part of #4949 but I could use this separately for experimenting with wasi-threads.

@github-actions github-actions bot added the wasi Issues pertaining to WASI label Oct 13, 2022
@github-actions
Copy link

Subscribe to Label Action

cc @kubkon

This issue or pull request has been labeled: "wasi"

Thus the following users have been cc'd because of the following labels:

  • kubkon: wasi

To subscribe or unsubscribe from this label, edit the .github/subscribe-to-label.json configuration file.

Learn more.

@alexcrichton
Copy link
Member

Ah thanks for splitting this out, I missed this reading the other patch!

Unfortunately though I don't think this can be done, the unsafe here truly is unsafe and this is exposing a memory-vulnerabilty-in-waiting due to violating Rust's model of ownership. Or well it's not really specific to Rust per-se, but it's easiest to explain through Rust.

Wiggle aggressively takes advantage of an internal borrow checker and handing out slices which point raw into wasm linear memory. This means that when the host gets &mut [u8] or &[u8] or &str those are all pointers into raw wasm memory. While this is valid for single-threaded instances because the contents cannot change none of these Rust-based views are valid for shared memories because the contents can change at any time.

All host-based processing of data stored in linear memory needs to change with shared memories. All loads/stores need to be atomic and additionally nothing can ever reside in the linear memory itself. For example if a wiggle API gives a string to the host then the host needs to copy out the string and then validate utf-8, whereas I think the opposite order happens today.

I believe that solving this would require a fair bit of work and design within wiggle itself. Given the cost of atomic operations we probably can't do them by default. Additionally it may be too expensive to check "is this shared memory" at all interactions with linear memory from wiggle so having a runtime flag to process this may or may not be the best option as well.

Overall I haven't really thought much about how to solve this problem. We've known about this for quite some time now but it's been a moot issue due to the lack of support for the threads proposal in Wasmtime. Also FWIW many of the same issues apply to supporting memory64 in WASI and additionally supporting either threads or memory64 in the component model support within Wasmtime.

@abrown
Copy link
Contributor Author

abrown commented Oct 17, 2022

Overall I haven't really thought much about how to solve this problem. We've known about this for quite some time now but it's been a moot issue due to the lack of support for the threads proposal in Wasmtime.

One thought that I have had about this is to sequentialize host access from the WebAssembly threads, either by locking on every host call. If this were done, then the Rust views to the underlying memory would be protected from concurrent modification. I looked for a way to do this here but am still struggling to figure out how to tell a Linker to "wrap every call with a Mutex::lock" while still passing the expected WASI context type through get_cx without dropping the lock. It seems to me like some refactoring may be needed.

The other idea I had was to lock calls within a WASI proposal, so that one could make concurrent calls to wasi-common and wasi-nn, e.g., but not within wasi-common itself. The motivation for this is that the contexts/states of each WASI proposal are distinct and there is no reason (along those lines) to lock everything. The point you raise above, however, suggests that locking is not just for protecting WASI contexts/states, but also to preserve the Rust expectations of the memory views used by host calls. So maybe I need to abandon this "lock calls within a WASI proposal."

@alexcrichton
Copy link
Member

Unfortunately I don't think either of those strategies would be sufficient. You would need something akin to "stop the world" semantics of GCs because threads not in hostcalls can stomp over memory that one hostcall is working with. This means that a single hostcall is all that's needed for things to go wrong, so locking hostcalls themselves won't be sufficient.

@penzn
Copy link

penzn commented Nov 3, 2022

All host-based processing of data stored in linear memory needs to change with shared memories. All loads/stores need to be atomic and additionally nothing can ever reside in the linear memory itself. For example if a wiggle API gives a string to the host then the host needs to copy out the string and then validate utf-8, whereas I think the opposite order happens today.

Sorry to barge in 😄 Is this always necessary, for example, when host call involves memory area that only the instance initiating the call is using? In JS world accesses from outside are not atomic, unless they are explicitly made atomic.

@alexcrichton
Copy link
Member

Unfortunately, yes, all accesses need to be atomic in Rust. They can be a Relaxed atomic load, for example, but they need to be tagged as "something else can racily write to this location at any time", which Rust only has the ability to do so with atomics.

In JS world accesses from outside are not atomic

I don't think that this is correct because in JS the backing memory is in a SharedArrayBuffer and all reads/writes from that are done with an atomic ordering. If that's wrapped up in a Uint32Array and you read an index then it doesn't look atomic but I believe that under the hood it's implicitly doing an Unordered atomic ordering (at least according to my reading of the spec)

abrown added a commit to abrown/wasmtime that referenced this pull request Nov 8, 2022
This change is the first in a series of changes to support shared memory
in Wiggle. Since Wiggle was written under the assumption of
single-threaded guest-side access, this change introduces a `shared`
field in order to flag when this assumption will not be true. This
change always sets `shared` to `false`; once a few more pieces are in
place, `shared` will be set dynamically when a shared memory is
detected, e.g., in a change llike bytecodealliance#5054.

Using the `shared` knowledge, we can now decide to load Wiggle values
differently. This change  makes the guest `T::read` and `T::write` calls
into `Relaxed` atomic loads and stores in order to maintain
WebAssembly's expected memory consistency guarantees. We choose Rust's
`Relaxed` here to match the `Unordered` memory consistency described in
the [memory model] section of the ECMA spec.

[memory model]: https://tc39.es/ecma262/multipage/memory-model.html#sec-memory-model

Since 128-bit scalar types do not have `Atomic*` equivalents, we remove
their `T::read` and `T::write` implementations here. They are unused by
any WASI implementations in the project.
abrown added a commit to abrown/wasmtime that referenced this pull request Nov 8, 2022
This change is the first in a series of changes to support shared memory
in Wiggle. Since Wiggle was written under the assumption of
single-threaded guest-side access, this change introduces a `shared`
field to guest memories in order to flag when this assumption will not
be the case. This change always sets `shared` to `false`; once a few
more pieces are in place, `shared` will be set dynamically when a shared
memory is detected, e.g., in a change like bytecodealliance#5054.

Using the `shared` field, we can now decide to load Wiggle values
differently under the new assumptions. This change  makes the guest
`T::read` and `T::write` calls into `Relaxed` atomic loads and stores in
order to maintain WebAssembly's expected memory consistency guarantees.
We choose Rust's `Relaxed` here to match the `Unordered` memory
consistency described in the [memory model] section of the ECMA spec.

[memory model]: https://tc39.es/ecma262/multipage/memory-model.html#sec-memory-model

Since 128-bit scalar types do not have `Atomic*` equivalents, we remove
their `T::read` and `T::write` implementations here. They are unused by
any WASI implementations in the project.
abrown added a commit that referenced this pull request Nov 8, 2022
This change is the first in a series of changes to support shared memory
in Wiggle. Since Wiggle was written under the assumption of
single-threaded guest-side access, this change introduces a `shared`
field to guest memories in order to flag when this assumption will not
be the case. This change always sets `shared` to `false`; once a few
more pieces are in place, `shared` will be set dynamically when a shared
memory is detected, e.g., in a change like #5054.

Using the `shared` field, we can now decide to load Wiggle values
differently under the new assumptions. This change  makes the guest
`T::read` and `T::write` calls into `Relaxed` atomic loads and stores in
order to maintain WebAssembly's expected memory consistency guarantees.
We choose Rust's `Relaxed` here to match the `Unordered` memory
consistency described in the [memory model] section of the ECMA spec.
These relaxed accesses are done unconditionally, since we theorize that
the performance benefit of an additional branch vs a relaxed load is
not much.

[memory model]: https://tc39.es/ecma262/multipage/memory-model.html#sec-memory-model

Since 128-bit scalar types do not have `Atomic*` equivalents, we remove
their `T::read` and `T::write` implementations here. They are unused by
any WASI implementations in the project.
abrown added a commit to abrown/wasmtime that referenced this pull request Nov 8, 2022
When multiple threads can concurrently modify a WebAssembly shared
memory, the underlying data for a Wiggle `GuestSlice` and
`GuestSliceMut` could change due to access from other threads. This
breaks Rust guarantees when `&[T]` and `&mut [T]` slices are handed out.
This change modifies `GuestPtr` to make `as_slice` and `as_slice_mut`
return an `Option` which is `None` when the underlying WebAssembly
memory is shared.

But WASI implementations still need access to the underlying WebAssembly
memory, both to read to it and write from it. This change adds new APIs:
- `GuestPtr::to_vec` copies the  bytes from WebAssembly memory (from
  which we can safely take a `&[T]`)
- `GuestPtr::as_unsafe_slice_mut` returns a wrapper `struct` from which
  we can  `unsafe`-ly return a mutable slice (users must accept the
  unsafety of concurrently modifying a `&mut [T]`)

This approach allows us to maintain Wiggle's borrow-checking
infrastructure, which enforces the guarantee that Wiggle will not modify
overlapping regions, e.g. This is important because the underlying
system calls may expect this. Though other threads may modify the same
underlying region, this is impossible to prevent; at least Wiggle will
not be able to do so.

Finally, the changes to Wiggle's API are propagated to all WASI
implementations in Wasmtime. For now, code locations that attempt to get
a guest slice will panic if the underlying memory is shared. Note that
Wiggle is not enabled for shared memory (that will come later in
something like bytecodealliance#5054), but when it is, these panics will be clear
indicators of locations that must be re-implemented in a thread-safe
way.
abrown added a commit to abrown/wasmtime that referenced this pull request Nov 8, 2022
When multiple threads can concurrently modify a WebAssembly shared
memory, the underlying data for a Wiggle `GuestSlice` and
`GuestSliceMut` could change due to access from other threads. This
breaks Rust guarantees when `&[T]` and `&mut [T]` slices are handed out.
This change modifies `GuestPtr` to make `as_slice` and `as_slice_mut`
return an `Option` which is `None` when the underlying WebAssembly
memory is shared.

But WASI implementations still need access to the underlying WebAssembly
memory, both to read to it and write from it. This change adds new APIs:
- `GuestPtr::to_vec` copies the  bytes from WebAssembly memory (from
  which we can safely take a `&[T]`)
- `GuestPtr::as_unsafe_slice_mut` returns a wrapper `struct` from which
  we can  `unsafe`-ly return a mutable slice (users must accept the
  unsafety of concurrently modifying a `&mut [T]`)

This approach allows us to maintain Wiggle's borrow-checking
infrastructure, which enforces the guarantee that Wiggle will not modify
overlapping regions, e.g. This is important because the underlying
system calls may expect this. Though other threads may modify the same
underlying region, this is impossible to prevent; at least Wiggle will
not be able to do so.

Finally, the changes to Wiggle's API are propagated to all WASI
implementations in Wasmtime. For now, code locations that attempt to get
a guest slice will panic if the underlying memory is shared. Note that
Wiggle is not enabled for shared memory (that will come later in
something like bytecodealliance#5054), but when it is, these panics will be clear
indicators of locations that must be re-implemented in a thread-safe
way.
abrown added a commit to abrown/wasmtime that referenced this pull request Nov 8, 2022
When multiple threads can concurrently modify a WebAssembly shared
memory, the underlying data for a Wiggle `GuestSlice` and
`GuestSliceMut` could change due to access from other threads. This
breaks Rust guarantees when `&[T]` and `&mut [T]` slices are handed out.
This change modifies `GuestPtr` to make `as_slice` and `as_slice_mut`
return an `Option` which is `None` when the underlying WebAssembly
memory is shared.

But WASI implementations still need access to the underlying WebAssembly
memory, both to read to it and write from it. This change adds new APIs:
- `GuestPtr::to_vec` copies the  bytes from WebAssembly memory (from
  which we can safely take a `&[T]`)
- `GuestPtr::as_unsafe_slice_mut` returns a wrapper `struct` from which
  we can  `unsafe`-ly return a mutable slice (users must accept the
  unsafety of concurrently modifying a `&mut [T]`)

This approach allows us to maintain Wiggle's borrow-checking
infrastructure, which enforces the guarantee that Wiggle will not modify
overlapping regions, e.g. This is important because the underlying
system calls may expect this. Though other threads may modify the same
underlying region, this is impossible to prevent; at least Wiggle will
not be able to do so.

Finally, the changes to Wiggle's API are propagated to all WASI
implementations in Wasmtime. For now, code locations that attempt to get
a guest slice will panic if the underlying memory is shared. Note that
Wiggle is not enabled for shared memory (that will come later in
something like bytecodealliance#5054), but when it is, these panics will be clear
indicators of locations that must be re-implemented in a thread-safe
way.
abrown added a commit to abrown/wasmtime that referenced this pull request Nov 10, 2022
When multiple threads can concurrently modify a WebAssembly shared
memory, the underlying data for a Wiggle `GuestSlice` and
`GuestSliceMut` could change due to access from other threads. This
breaks Rust guarantees when `&[T]` and `&mut [T]` slices are handed out.
This change modifies `GuestPtr` to make `as_slice` and `as_slice_mut`
return an `Option` which is `None` when the underlying WebAssembly
memory is shared.

But WASI implementations still need access to the underlying WebAssembly
memory, both to read to it and write from it. This change adds new APIs:
- `GuestPtr::to_vec` copies the  bytes from WebAssembly memory (from
  which we can safely take a `&[T]`)
- `GuestPtr::as_unsafe_slice_mut` returns a wrapper `struct` from which
  we can  `unsafe`-ly return a mutable slice (users must accept the
  unsafety of concurrently modifying a `&mut [T]`)

This approach allows us to maintain Wiggle's borrow-checking
infrastructure, which enforces the guarantee that Wiggle will not modify
overlapping regions, e.g. This is important because the underlying
system calls may expect this. Though other threads may modify the same
underlying region, this is impossible to prevent; at least Wiggle will
not be able to do so.

Finally, the changes to Wiggle's API are propagated to all WASI
implementations in Wasmtime. For now, code locations that attempt to get
a guest slice will panic if the underlying memory is shared. Note that
Wiggle is not enabled for shared memory (that will come later in
something like bytecodealliance#5054), but when it is, these panics will be clear
indicators of locations that must be re-implemented in a thread-safe
way.
alexcrichton pushed a commit that referenced this pull request Nov 10, 2022
* wiggle: adapt Wiggle guest slices for `unsafe` shared use

When multiple threads can concurrently modify a WebAssembly shared
memory, the underlying data for a Wiggle `GuestSlice` and
`GuestSliceMut` could change due to access from other threads. This
breaks Rust guarantees when `&[T]` and `&mut [T]` slices are handed out.
This change modifies `GuestPtr` to make `as_slice` and `as_slice_mut`
return an `Option` which is `None` when the underlying WebAssembly
memory is shared.

But WASI implementations still need access to the underlying WebAssembly
memory, both to read to it and write from it. This change adds new APIs:
- `GuestPtr::to_vec` copies the  bytes from WebAssembly memory (from
  which we can safely take a `&[T]`)
- `GuestPtr::as_unsafe_slice_mut` returns a wrapper `struct` from which
  we can  `unsafe`-ly return a mutable slice (users must accept the
  unsafety of concurrently modifying a `&mut [T]`)

This approach allows us to maintain Wiggle's borrow-checking
infrastructure, which enforces the guarantee that Wiggle will not modify
overlapping regions, e.g. This is important because the underlying
system calls may expect this. Though other threads may modify the same
underlying region, this is impossible to prevent; at least Wiggle will
not be able to do so.

Finally, the changes to Wiggle's API are propagated to all WASI
implementations in Wasmtime. For now, code locations that attempt to get
a guest slice will panic if the underlying memory is shared. Note that
Wiggle is not enabled for shared memory (that will come later in
something like #5054), but when it is, these panics will be clear
indicators of locations that must be re-implemented in a thread-safe
way.

* review: remove double cast

* review: refactor to include more logic in 'UnsafeGuestSlice'

* review: add reference to #4203

* review: link all thread-safe WASI fixups to #5235

* fix: consume 'UnsafeGuestSlice' during conversion to safe versions

* review: remove 'as_slice' and 'as_slice_mut'

* review: use 'as_unsafe_slice_mut' in 'to_vec'

* review: add `UnsafeBorrowResult`
@abrown
Copy link
Contributor Author

abrown commented Nov 15, 2022

@alexcrichton, I rebased this PR on top of all of the previous Wiggle work (#5225, #5229, #5264) since Wiggle seems safe enough at this point. Optionally, we could merge this once the "thread safety of WASI contexts" story is figured out, but I don't know if that is necessary: if this were merged, users who attempted to use shared memory without taking into account it's "shared-ness" would see a panic error pointing them to #5235.

A side note: WasmtimeGuestMemory expects to have a &mut [u8]; to get there, this latest change uses a transmute. Since the only external access to this slice is from GuestMemory::base(&self) -> (*mut u8, u32), perhaps we just store those types instead? E.g.:

pub struct WasmtimeGuestMemory<'a> {
    ptr: *mut u8,
    len: u32,
    bc: BorrowChecker,
    shared: bool,
}

@alexcrichton
Copy link
Member

Reading over this I had some lingering reservations, even with using a *mut u8/usize combo. Rebasing this on #5268 would make me comfortable landing this, however.

@penzn
Copy link

penzn commented Nov 15, 2022

Sorry, slipped my mind.

I don't think that this is correct because in JS the backing memory is in a SharedArrayBuffer and all reads/writes from that are done with an atomic ordering. If that's wrapped up in a Uint32Array and you read an index then it doesn't look atomic but I believe that under the hood it's implicitly doing an Unordered atomic ordering (at least according to my reading of the spec)

I don't think JavaScript requires all accesses to SAB to be atomic, only the ones that are done via Atomics object. Outside of that it allows for race conditions.

@alexcrichton
Copy link
Member

Well in any case I'm no JS expert and it doesn't seem like either of us are itching to become one digging into JS implementations here. What I can say is that for Rust all accesses need to be atomic. Otherwise this is a data race, or UB, which the purpose of Wasmtime is to prevent.

`wiggle` looks for an exported `Memory` named `"memory"` to use for its
guest slices. This change allows it to use a `SharedMemory` if this is
the kind of memory used for the export.

It is `unsafe` to use shared memory in Wiggle because of broken Rust
guarantees: previously, Wiggle could hand out slices to WebAssembly
linear memory that could be concurrently modified by some other thread.
With the introduction of Wiggle's new `UnsafeGuestSlice` (bytecodealliance#5225, bytecodealliance#5229,
 bytecodealliance#5264), Wiggle should now correctly communicate its guarantees through
its API.
@alexcrichton alexcrichton enabled auto-merge (squash) November 15, 2022 18:06
@alexcrichton alexcrichton merged commit df1d679 into bytecodealliance:main Nov 15, 2022
@penzn
Copy link

penzn commented Nov 15, 2022

Well in any case I'm no JS expert and it doesn't seem like either of us are itching to become one digging into JS implementations here. What I can say is that for Rust all accesses need to be atomic. Otherwise this is a data race, or UB, which the purpose of Wasmtime is to prevent.

I'd say what JS engines exactly do is relevant only to a degree, the question is what can be implemented here both in terms of Rust semantics and what the project aims to achieve. I am slightly concerned about performance if every access to shared memory becomes atomic.

Just as an aside, JS spec mandates three things: (a) atomics to be honored in all circumstances, (b) writes to be observed exactly once, and (c) reads to be observed exactly once. It does not require a specific ordering of non-atomic accesses or that all bits are written or read for those.

@abrown abrown deleted the shmem-in-wiggle branch November 15, 2022 20:34
@alexcrichton
Copy link
Member

Sorry I don't really have anything to add over that this is the absolute bare minimum required to be safe in Rust. There's simply no other option. If you're concerned about performance then the only "fix" I know of would be to make an RFC in upstream rust-lang/rust to add LLVM's "unordered" memory ordering to Rust's Ordering enum for atomic operations.

@abrown abrown added the wasm-proposal:threads Issues related to the WebAssembly threads proposal label Dec 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wasi Issues pertaining to WASI wasm-proposal:threads Issues related to the WebAssembly threads proposal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants