-
Notifications
You must be signed in to change notification settings - Fork 824
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
Fastest way to transfer data between WASM and Rust? #1249
Comments
The problem is that you need some amount of interior mutability to represent the wasm memory, as the wasm code could randomly change any of these bytes without properly communicating that back to Rust. So if the wasm is single threaded, the best way to represent that interior mutability is via |
I'm not 100% on how to avoid undefined behavior, but Unfortunately from my own tests, some uses of Perhaps we should have a memcpy API on memory though for large data transfers 🤔 |
I don't think that's true. Raw pointers are defined to not follow the aliasing model, otherwise any C FFI wouldn't work. So the stacked borrows model explicitly opts out of disallowing aliasing on raw pointers while they are in use. So if you run this with miri, which mutably aliases the same slice twice, it won't complain about it: |
That's good to know! Yeah, I think the line is between mutable and immutable data. It's fine to cast Hmm that implies that we can even have a It wasn't super clear in my comment but by "reserves the right to break this in the future" I was referring to the cast between
|
Yeah that's mostly because UnsafeCell is the one and only way to obtain mutable data from immutably borrowed memory. So you can't replicate this without involving UnsafeCell somewhere within your type to get "interior mutability". So yeah what could likely happen here is that Memory is internally an UnsafeCell<[u8]> or so, to obtain *mut [u8] and then any kind of modifications should be good to go. Though you may need to be careful to when getting "normal" &[u8] and &mut [u8] slices from that. I'm not entirely sure how the stacked borrow model treats that (I believe some temporary ones are fine). This would probably require lots of experimentation against miri. |
On somewhat related note, the documentation for
However, AFAIK, data races are UB in Rust and fwiw in C/C++. |
@pepyakin I agree, that doc comment seems wrong. There are complications about where the data races happen though: if 2 Wasm modules are run concurrently and in parallel and they both access the same shared memory without atomic memory operations, then data races are the "correct" result. The Wasm spec allows this situation to happen, though I believe we can force all uses of shared memory to be atomic, too, to mitigate some of it and still be in compliance with the Wasm spec. Data races involving the host are no good though and I'm not sure what a safe API for that looks like. If a Wasm module is accessing the same memory at the same time without atomic memory accesses, it seems like we're still in a bad place even if the host is using atomic memory operations. It probably depends on the level of security guarantees that we want to make. We can probably, for example, create global locks on memory or something if we really needed it but that doesn't seem like a good solution in general. Or perhaps we can expose an API to configure/implement memory to allow users to do whatever they need for their specific use case. @CryZe I recently read that it's the source mutability that matters for actually mutating... I can't remember exactly where, but I became convinced in the past week or two that as long as it originally came from a mutable location, then it can be cast to immutable and back to mutable and nothing bad will happen when mutating it. If I can remember where I read that I'll search for it and post a link here!
That's true, any mapping of high level language types with guarantees like |
I don’t want to interrupt the discussion, just wanted to say- From a user perspective, the confusing thing to me is that it looks like it’s locking/synchronized for each byte individually, which means that the conceptual data structure I’m accessing (a data buffer eg 16KiB+ for streaming chunks of data) is still subject to data races. I saw that the view function is generic so I can specify a different type, but then I don’t know how I would be able to index it if the address allocated for the buffer ends up not being a multiple of 16Ki. I’ve never heard of something being aligned based on the size of the entire buffer, usually it’s 4, 8, or 16 bytes. What I thought maybe I was supposed to do then was specify a byte offset somewhere and then access the view using the type, but I didn’t see an option for that. In practice for multiple instances I’d expect that you’d want a small amount of memory to be atomic (eg for a mutex) and then the rest of the data buffer would be unsynchronized, with the exception of some fence to make sure everything has been written through to RAM before the mutex is unlocked and the next wasm instance has a go. Even for data structures I’d expect this would be the most common model, I’d expect it’d mostly be counters or flags where people would want integer-sized atomic accesses. I’m not sure how you would do this interoperability though since I’d expect transmuting some memory to a Mutex type on the x86_64 side would not map exactly to a wasm mutex type. In this case it would be nice if wasmer did provide an API, but synchronization primitives that can be used on both the wasm and x86_64 sides would make sense to me as well. In my case right now, I’m only running wasm on a single thread so entirely unsynchronized should be ok. |
Yeah, the array-style indexing has been around for a while, it'd be good to have an API for accessing properly aligned types that don't fit that more easily, Rust doesn't like unaligned memory accesses but we can probably provide a copying unaligned access function too (perhaps the aligned access should copy too... It's possible to hold references to always valid Rust data types in Wasm memory but not in shared memory and in unshared memory the lifetime of the reference must end before the next time the Wasm can execute: this should work today with the current API).
There's no locking when memory is accessed currently, it's all direct access. With the exception of things like Traditionally a data race is when, through the lack of use atomics in a shared setting, data is partially updated or observable in an invalid state. When dealing with types that are larger than the atomics your system supports, you'd normally have to use a higher level synchronization object like a mutex. The problem is that if a participant in the memory access is untrustable (like Wasm modules are in general), then there is no way to prevent logical race conditions at this level. However because we can control the Wasm execution, we can force a lock on its memory/execution on the host level and get safe access to its memory if that matters, but it shouldn't be an issue in general to be honest: the host should treat memory from the guest as malicious and possibly invalid (it may be corrupt even if you trust the Wasm module). To put it more directly: it's the responsibility of the host to handle bad input, so given proper atomic access to memory it shouldn't matter if the guest is attempting to maliciously edit that same memory (with the one caveat that I don't fully understand the interaction between atomic and non-atomic memory accesses, so there could be an issue there). However even if the guest could partially corrupt its own data, the host shouldn't be trusting any types that aren't valid in all possible bit patterns from the guest, this is what the unsafe
In general, hosts should not communicate through Wasm memory. Due to malicious Wasm modules and possible internal corruption. Doing so would be in the realm of using |
Just to be clear, this is the host streaming data into the guest or the guest streaming data into the host. Is there a better way to do this than Ctx.memory()? |
@spease I was referring to getting data from the guest as the host, mostly. So it depends, if you look at the design of something like Abstractly that's a pretty nice design for most types of bulk data transfer unless the data will exist on the guest as a linear array, in which case it can be transferred to the host in one go. I'd recommend not worrying about memory in general right now and write for the single-threaded use case. I forgot to mention One thing to note: I just realized the Also apologies about the poor documentation on the |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
In #2442, new methods have been added to wasmer/lib/types/src/memory_view.rs Lines 98 to 113 in c7f4bd2
@spease Do you need more API, or is your problem solved? |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
I’ve moved on from the company I was working with when I ticketed this, but copy_from does look like it would have solved my use case of moving more than one byte of data at a time. Thanks for implementing it, I’m sure it will be useful for someone (I was surprised there didn’t seem to be something like it given how ubiquitous I expected it to be for people to want to move data in and out of a wasm sandbox). |
From Wasmer 3 onwards, WasmSlice seems to be available to do this directly. |
Currently I'm using memory.view() to transfer bytedata between WASM and Rust. This data is generally arbitrarily placed and sized. It may be possible for the size to be a compile-time constant, but it is currently set at runtime.
I've been disappointed that to get or set the data I have to call cell.get() or cell.set() for every single byte. This seems like it will likely needlessly slow things down, unless significant optimizations are made. In addition, it prevents me from using WASM memory directly eg with read as a buffer.
I've noticed there's an atomically() function. This changes the type of each byte in AtomicU8 rather than Cell.
Which of these is closer to the native representation?
Which is expected to generally be faster?
Why isn't there a way to directly access the bytes of the WASM memory space?
Thanks.
The text was updated successfully, but these errors were encountered: