-
Notifications
You must be signed in to change notification settings - Fork 131
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
[move-vm] Removed most RCs from runtime #160
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to take a closer look, but here are some early feedbacks regarding unsafe fn.
I think one thing to point out is that write_safe
is probably not the only thing that's unsafe -- read_ref
and more broadly, anything that dereferences a CheckedRef (a.k,a, raw pointers) are also unsafe. Consider this case:
let locals = Locals::new(4);
locals.store_loc(0, Value::u64(0));
locals.store_loc(1, Value::u64(0));
let r1 = locals.borrow_loc(0).unwrap();
let r2 = locals.borrow_loc(1).unwrap();
std::mem::drop(locals);
r1.equals(r2);
I'm aware this is a terrible way to use the value APIs, but this is exactly the point of marking them as unsafe -- forcing the Move VM to annotate its usages, making it easier for us to review them and make sure they are indeed doing sane things.
Very perceptive :) I saw exactly this after working on #175, seems you just saw it a bit earlier than myself |
Not sure why the hardhat-tests are failing... Could you try to rebase? |
- RCs are no longer necessary due to Move's reference safety checks - Some RCs are still needed to update clean/dirty status or hold onto external memory safely
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, but let's leave some time for @wrwg to have a look, just to make sure he's on board with the general direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concerned about the spread of unsafe
across the code.
@@ -75,7 +75,7 @@ fn native_send( | |||
let ext = context.extensions_mut().get_mut::<AsyncExtension>(); | |||
let mut bcs_args = vec![]; | |||
while args.len() > 2 { | |||
bcs_args.push(pop_arg!(args, Vector).to_vec_u8()?); | |||
bcs_args.push(pop_arg!(args, Vec<u8>)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! That actually works, great. Problem with that kind of magic is that the APIs aren't clearly documented and buried somewhere in all those trait impls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed its a bit hard to discover, not sure if @vgao1996 has any thoughts on that (to be clear the VMValueCast stuff has been around a while, not new to this PR)
.unwrap() | ||
.value_as::<Reference>()? | ||
.read_ref()?; | ||
let key = unsafe { args.pop_back().unwrap().value_as::<Reference>()?.read_ref() }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering: can't we isolate the unsafe
somewhere in the value impl? Really don't like to see this in user code.
//! However, it is **very important** that any instance of `Locals` is not dropped until after | ||
//! all Values are consumed. If this is not followed, reading/writing to Values *will* read from | ||
//! invalid pointers. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is kind of bummer for usage of the VM. We want to use references in adapters (doesn't Sui does so) and custom embeddings of the VM, as well as native functions implementations, avoiding the need for using unsafe
all over the code. Unsafe Rust should be isolated. Can you wrap the unsafe usage somewhere inside the VM?
Anything with references in this implementation will be inherently unsafe. Even reading from them could be unsafe, if an improper write occurs. So there isn't really way to encapsulate the logic, which we already do for adapters, could do some endpoints, and can't really do for native functions: On native functions: On the adapter: For other endpoints (testing infra and such): |
I wonder whether we differentiate here between unsafeness regards the Rust and the Move language? Accessing a dangling Move reference may result in an undefined value, but it should not result in a process crash. In contrast, wherever you use unsafe Rust, a process crash can happen because of programmer mistakes. It is acceptable to do this in some isolated internal contexts, but not Rustonian to require unsafe in public APIs. Perhaps we need a safe wrapper around the Value implementation? If it costs a bit extra, I think this is acceptable. We can also keep the unsafe API open for the implementation of certain critical native functions, but one should not be forced into using it. |
Just a further note, not actionable but related to this PR. If we want to have a more efficient implementation of values, I think we could go a different approach. What I sketch is motivated both by implementing the Move memory model on top of the EVM and for the Prover. (a) Linear Memory Borrow semantics is a model for linear addressable RAM, including pointer indirection and address arithmetic, and I think will be most efficiently implemented by reflecting those aspects. So why not represent any value as a byte-addressable region of memory. You can borrow offsets in this region, read values, and mutate them. An unsafe, untyped value representation would then be The lifetime of the region the value points to is guaranteed to be valid by our own implementation of memory allocation. We can also skip the GC (as Solidity does) assuming that transactions are short running. I.e. we use arena style allocation for As to how we layout memory, this was investigated as part of EVM Move. Assuming a field in a struct has a fixed offset, field borrowing simply amounts to Nested structs can be inlined, or represented via indirections like vectors, which inherently need indirections because they have no fixed size. (b) Generics and Variants We can avoid enums for different value variants all together. A Since There is one exception regards integrity, that is when we have an indirection to a vector (or struct if we choose so) embedded in
|
Motivation
write_ref
unsafe, as it could lead to memory issues if the reference safety checks are wrong or not runTest Plan