-
Couldn't load subscription status.
- Fork 118
Adding some more miri tests #503
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
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #503 +/- ##
==========================================
+ Coverage 79.82% 80.73% +0.90%
==========================================
Files 57 61 +4
Lines 12591 13375 +784
Branches 12591 13375 +784
==========================================
+ Hits 10051 10798 +747
- Misses 2006 2027 +21
- Partials 534 550 +16 ☔ View full report in Codecov by Sentry. |
ffi/src/lib.rs
Outdated
| let s: &str = unsafe { TryFromStringSlice::try_from_slice(&kernel_str).unwrap() }; | ||
| let b = Box::new(s.to_string()); |
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.
There's an impl for String as well:
| let s: &str = unsafe { TryFromStringSlice::try_from_slice(&kernel_str).unwrap() }; | |
| let b = Box::new(s.to_string()); | |
| let s = unsafe { String::try_from_slice(&kernel_str).unwrap() }; | |
| let b = s.into_boxed_str(); |
Also, I'm pretty sure that String::into_boxed_str and From<Box<str>> for String are non-allocating and thus preferable to Box<String> that will be a pointer-to-pointer.
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.
Re all of this *mut str is a fat pointer, because slices need to hold their length. To keep the length bundled up I used a String. There's probably a more elegant way to do it, but since it's testing code I wasn't going to spend too long optimizing it.
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.
To expand a bit, you can totally get a c_void pointer from a String
Code
let boxed_str = s.into_boxed_str();
let raw_str_ptr: *mut str = Box::into_raw(boxed_str);
let non_null = NonNull::new(raw_str_ptr).unwrap();
let ptr = non_null.cast::<c_void>();but once you have a NonNull<c_void> I don't think you can recover it, because:
// ptr from above which is a NonNull<c_void>
ptr.as_ptr() as *mut strSo you get into needing to basically recreate KernelStringSlice, since you need to store the pointer and the length separately. We could probably create a slice and leak a pointer to it, and then re-assemble, but just boxing and unboxing the string seems easier.
isn't legal: cannot cast thin pointer `*mut c_void` to wide pointer `*mut str`
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.
Wait... str is actually a slice, not just deref to slice? That indeed poses a problem.
ffi/src/lib.rs
Outdated
| let p = Box::leak(b) as *mut String as *mut c_void; | ||
| let ptr = unsafe { NonNull::new_unchecked(p) }; |
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.
nit:
| let p = Box::leak(b) as *mut String as *mut c_void; | |
| let ptr = unsafe { NonNull::new_unchecked(p) }; | |
| let ptr = Box::into_raw(b).cast(); | |
| let ptr = unsafe { NonNull::new_unchecked(ptr) }; |
| let snapshot = | ||
| unsafe { ok_or_panic(snapshot(kernel_string_slice!(path), engine.clone_ptr())) }; | ||
|
|
||
| let version = unsafe { version(snapshot.clone_ptr()) }; | ||
| assert_eq!(version, 0); | ||
|
|
||
| let table_root = unsafe { snapshot_table_root(snapshot.clone_ptr(), allocate_str) }; | ||
| assert!(table_root.is_some()); | ||
| let s = recover_string(table_root.unwrap()); | ||
| assert_eq!(&s, path); | ||
|
|
||
| unsafe { free_snapshot(snapshot) } |
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.
Interesting...
Functions like snapshot want to take an owned
Handle<SharedExternEngine>, which means we can't "reuse" a handle from rust code. But the semantics are actually that dropping the handle (which will happen after calling snapshot) does not drop the underlying data, so if we call snapshot from "regular" rust code, we actually leak it.
Given the documented semantics of Handle (that it is valid until Handle::drop_handle is called), should we consider making Handle be Copy? Foreign code can anyway copy it freely, and is already responsible to properly enforce borrowed vs. mutable semantics (and Send+Sync) by not calling back into kernel from multiple threads simultaneously. Meanwhile, not being Copy gives no very little (**) protection to rust code because -- as both you and the docs note -- rust dropping the handle does not free the underlying memory.
(**) Not being Copy does grant some protection from double-free, because you can't call Handle::drop_handle twice... in the same function call. But that's only true for rust since handles are anyway effectively Copy from native code perspective. In particular, rust can't actually be sure a Handle has not been copied, if it was received from native code (tho that's an expected hazard of FFI being unsafe, I guess).
A safe alternative for shared handles is to call clone_handle and let the clone be consumed. But no such facility exists for exclusive handles.
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.
yea and considering NonNull implements Copy this seems reasonable? we are just a pointer at that point?
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.
Thinking more about this -- the actual target audience of this FFI API (e.g. C++ code) doesn't have the problem because handles are trivially copyable. So it's only rust code pretending to be FFI that needs the workaround. And for rust code, I suspect it's better to have an obvious function call to alert readers about what's going on, rather than an implicit copy?
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.
yeah. i was working through this a bit, and I think, since we really just want it for testing atm, it makes sense to be a shallow_copy function call to make it really explicit
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 makes sense - definitely useful to mock the C(++) behavior in these tests, just left a couple comments on safety and implementing Copy
ffi/src/handle.rs
Outdated
|
|
||
| /// In testing code we want to simulate what c code can do where a pointer can be used | ||
| /// without consuming it. This creates a "new" handle just by cloning the underlying | ||
| /// pointer. This is unsafe! Do not use outside testing code! |
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.
does this mean we should make it literally unsafe?
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.
no, unsafe as a keyword means it calls unsafe code, this is "safe" in that sense. I will find a better word to use in the comment
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.
"dangerous" or "highly error-prone"?
| let snapshot = | ||
| unsafe { ok_or_panic(snapshot(kernel_string_slice!(path), engine.clone_ptr())) }; | ||
|
|
||
| let version = unsafe { version(snapshot.clone_ptr()) }; | ||
| assert_eq!(version, 0); | ||
|
|
||
| let table_root = unsafe { snapshot_table_root(snapshot.clone_ptr(), allocate_str) }; | ||
| assert!(table_root.is_some()); | ||
| let s = recover_string(table_root.unwrap()); | ||
| assert_eq!(&s, path); | ||
|
|
||
| unsafe { free_snapshot(snapshot) } |
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.
yea and considering NonNull implements Copy this seems reasonable? we are just a pointer at that point?
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #503 +/- ##
==========================================
+ Coverage 81.07% 81.54% +0.46%
==========================================
Files 67 67
Lines 14496 14544 +48
Branches 14496 14544 +48
==========================================
+ Hits 11753 11860 +107
+ Misses 2170 2107 -63
- Partials 573 577 +4 ☔ View full report in Codecov by Sentry. |
|
@nicklan the last semver check came up clean -- you might try removing the label before your next push, to see if it gets re-added? |
ffi/src/handle.rs
Outdated
|
|
||
| /// In testing code we want to simulate what c code can do where a pointer can be used | ||
| /// without consuming it. This creates a "new" handle just by cloning the underlying | ||
| /// pointer. This is unsafe! Do not use outside testing code! |
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.
"dangerous" or "highly error-prone"?
| let snapshot = | ||
| unsafe { ok_or_panic(snapshot(kernel_string_slice!(path), engine.clone_ptr())) }; | ||
|
|
||
| let version = unsafe { version(snapshot.clone_ptr()) }; | ||
| assert_eq!(version, 0); | ||
|
|
||
| let table_root = unsafe { snapshot_table_root(snapshot.clone_ptr(), allocate_str) }; | ||
| assert!(table_root.is_some()); | ||
| let s = recover_string(table_root.unwrap()); | ||
| assert_eq!(&s, path); | ||
|
|
||
| unsafe { free_snapshot(snapshot) } |
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.
Thinking more about this -- the actual target audience of this FFI API (e.g. C++ code) doesn't have the problem because handles are trivially copyable. So it's only rust code pretending to be FFI that needs the workaround. And for rust code, I suspect it's better to have an obvious function call to alert readers about what's going on, rather than an implicit copy?
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
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!
What changes are proposed in this pull request?
Add more tests to ffi so we're using miri to test more things.
Also, add a testing only
shallow_copymethod to handles. This creates a newHandlejust by copying the interior pointer value .Functions like
snapshotwant to take an ownedHandle<SharedExternEngine>, which means we can't "reuse" a handle from rust code. But the semantics are actually that dropping the handle (which will happen after callingsnapshot) does not drop the underlying data, so if we callsnapshotfrom "regular" rust code, we actually leak it. So to "simulate" what happens in c/c++ we canshallow_copywhich just makes another pointer, so you don't have to lose access to the original. This is really not safe, so it's only available in#[cfg(test)]