-
Notifications
You must be signed in to change notification settings - Fork 11
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
safety: introduce pointer types and their restrictions #208
base: master
Are you sure you want to change the base?
Conversation
f433610
to
9dadad8
Compare
9dadad8
to
7b8bb5a
Compare
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.
The approach is very impressive.
I couldn't check all the changes, there were too much of them; I just peeked through them. I trust your meticulousness :)
7b8bb5a
to
e8d2d93
Compare
Rebased on master |
98fbac9
to
aa90f5a
Compare
v1.1: Addressed @wprzytula comments |
Waiting with review for a rebase on #207. |
Everything is up to date |
aa90f5a
to
6c2a061
Compare
v2: Improved pointer API based on @wprzytula suggestions. Thank you!
I still need to think how to address the issue in tests. It's not possible to reuse the pointer, which is not |
You could have an |
6c2a061
to
3686e0d
Compare
Applied the suggestion. At first, I did not understand how is this different from implementing |
scylla-rust-wrapper/src/batch.rs
Outdated
#[no_mangle] | ||
pub unsafe extern "C" fn cass_batch_add_statement( | ||
batch: *mut CassBatch, | ||
statement: *const CassStatement, | ||
mut batch: CassExclusiveMutPtr<CassBatch>, | ||
statement: CassExclusiveConstPtr<CassStatement>, | ||
) -> CassError { | ||
let batch = ptr_to_ref_mut(batch); | ||
let batch = BoxFFI::as_mut_ref(&mut batch).unwrap(); | ||
let state = Arc::make_mut(&mut batch.state); | ||
let statement = ptr_to_ref(statement); | ||
let statement = BoxFFI::as_ref(&statement).unwrap(); | ||
|
||
match &statement.statement { |
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.
❓ 📌 Is this function still correct in case Arc::make_mut(&mut batch.state)
returns an Arc
to a new allocation, because there already have been multiple owners of state
?
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.
This is as valid, as it was before the changes - we did not change the logic. But to answer your question (is it valid), I'd need to investigate it. TBH, I haven't touched batches in cpp-rust-driver yet. However, all these Arc::make_mut
calls are a bit suspicious to me.
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.
Yep, this is out of scope of this PR. Just wanted to raise your awareness about the potential bug here.
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 investigated this. It looks fine to me. It looks like a small optimization.
The reason is that we need to clone fields of CassBatch
in cass_session_execute_batch
- we should not make any assumptions about the provided CassBatch
object in the functions - it could have been modified, or even freed:
let session_opt = ArcFFI::into_ref(session_raw).unwrap();
let batch_from_raw = BoxFFI::as_ref(&batch_raw).unwrap();
let mut state = batch_from_raw.state.clone();
let request_timeout_ms = batch_from_raw.batch_request_timeout_ms;
// DO NOT refer to `batch_from_raw` inside the async block, as I've done just to face a segfault.
let batch_exec_profile = batch_from_raw.exec_profile.clone();
So how the optimization works?
This statement let mut state = batch_from_raw.state.clone();
is cheap, because it clones an Arc.
When it works?
Well, Arc::make_mut
can potentially allocate, but this will not be the case if there is no concurrent execution of cass_session_execute_batch
in the background - i.e., there is no other owner of the state.
Notice that this is all sound, because we do not expose the state to the users in any way.
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.
👍 Please put the above findings in a comment there, not to confuse future readers.
3686e0d
to
f341734
Compare
Rebased on master |
f341734
to
8dfff0a
Compare
v2.1: improved docstrings as suggested by @wprzytula |
I've read the first few commits, and it talks about
As far as I can tell it copies the whole object, not Arc, and does not assume it is Arc-allocated. |
Sorry for the confusion. I remember that I made a mistake in some commit message, but forgot to fix it. I meant #[no_mangle]
pub unsafe extern "C" fn cass_data_type_add_sub_type(
data_type: CassSharedPtr<CassDataType>,
sub_data_type: CassSharedPtr<CassDataType>,
) -> CassError {
let data_type = ArcFFI::as_ref(&data_type).unwrap();
match data_type
.get_mut_unchecked()
.add_sub_data_type(ArcFFI::cloned_from_ptr(sub_data_type).unwrap())
{
Ok(()) => CassError::CASS_OK,
Err(e) => e,
}
} |
8dfff0a
to
d4d7b1e
Compare
Pushed fixed commit message ( |
The same mistake is present in |
d4d7b1e
to
ec6be4b
Compare
Other parts of the code make an assumption, that the pointer representing `CassDataType` was obtained from an Arc allocation. Take for example `cass_data_type_add_sub_type` - it clones an Arc. This is a bug, that was fortunately detected by applying more restrictions on the pointer types (introduced later in this PR).
The same bug as for collection types.
Again, if someone called `cass_data_type_add_sub_type` with a data type obtained from `cass_column_meta_data_type`, it would not be a pointer from an Arc allocation.
Weak::as_ptr() can return an invalid pointer. It can be even dangling (non-null). It's safer to try to upgrade to an Arc. If upgrade was successful, make use of RefFFI API to return a valid pointer. Otherwise, return non-dangling null pointer.
Before this PR, the pointer was obtained from a valid reference &CassFuture, which is totally fine. However, I want to reduce the ways one can obtain such pointer. For ArcFFI (shared pointers), I want them to be obtainable only in two ways: - `ArcFFI::as_ptr()` which accepts an &Arc - from the user, as a function parameter This way, we are guaranteed that the pointer comes from a valid Arc allocation (unless user provided pointer to some garbage, but there is no much we can do about it). If we assume that user provides a pointer returned from some prior call to API, we are guaranteed that it is valid, and comes from an Arc allocation (or is null). I don't want to allow ArcFFI api to create a pointer from a refernce, to prevent creating a pointer, from example from stack allocated object: ``` let future = CassFuture { ... }; let future_ptr = ArcFFI::as_ptr(&future); ``` This commit may not make much sense now, but all should be clear once I introduce traits restricting the pointer types later in this PR.
ec6be4b
to
cf2dcff
Compare
Rebased on master |
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 did not review the whole thing, because I spotted some important issues in argconv.rs. In general, please describe with more detail in precision what guarantees / requirements the types / methods require and provide. Without it it is a bit difficult to reason about the code.
scylla-rust-wrapper/src/metadata.rs
Outdated
view_meta: *const CassMaterializedViewMeta, | ||
) -> *const CassTableMeta { | ||
let view_meta = RefFFI::as_ref(view_meta); | ||
view_meta.base_table.as_ptr() | ||
|
||
match view_meta.base_table.upgrade() { | ||
Some(arc) => RefFFI::as_ptr(&arc), | ||
None => std::ptr::null(), | ||
} |
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.
Is it possible for this upgrade to fail? Or does it indicate a bug in the driver?
If it is possible, can we have a test for that?
scylla-rust-wrapper/src/future.rs
Outdated
impl BoundCallback { | ||
fn invoke(self, fut: &CassFuture) { | ||
fn invoke(self, fut_ptr: *const CassFuture) { | ||
unsafe { | ||
self.cb.unwrap()(fut as *const CassFuture, self.data); | ||
self.cb.unwrap()(fut_ptr, self.data); | ||
} | ||
} | ||
} |
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.
Shouldn't this invoke
method be unsafe? It does have a requirement for its argument to be a correct (alingment, lifetime etc) pointer.
scylla-rust-wrapper/src/future.rs
Outdated
pub fn set_callback( | ||
&self, | ||
self_ptr: *const CassFuture, | ||
cb: CassFutureCallback, | ||
data: *mut c_void, | ||
) -> CassError { | ||
let mut lock = self.state.lock().unwrap(); | ||
if lock.callback.is_some() { | ||
// Another callback has been already set |
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.
Here self_ptr
should refer to the same CassFuture
as self
, right?
Can you instead make the receiver of this function an arc? self: &Arc<CassFuture>
scylla-rust-wrapper/src/argconv.rs
Outdated
pub trait BoxFFI: Sized { | ||
fn into_ptr<M: Mutability>(self: Box<Self>) -> CassExclusivePtr<Self, M> { | ||
CassExclusivePtr::from_box(self) | ||
} | ||
unsafe fn from_ptr(ptr: *mut Self) -> Box<Self> { | ||
#[allow(clippy::disallowed_methods)] | ||
Box::from_raw(ptr) | ||
fn from_ptr<M: Mutability>(ptr: CassExclusivePtr<Self, M>) -> Option<Box<Self>> { | ||
ptr.into_box() | ||
} | ||
unsafe fn as_maybe_ref<'a>(ptr: *const Self) -> Option<&'a Self> { | ||
#[allow(clippy::disallowed_methods)] | ||
fn as_ref<M: Mutability>(ptr: &CassExclusivePtr<Self, M>) -> Option<&Self> { | ||
ptr.as_ref() | ||
} | ||
unsafe fn as_ref<'a>(ptr: *const Self) -> &'a Self { | ||
#[allow(clippy::disallowed_methods)] | ||
ptr.as_ref().unwrap() | ||
fn into_ref<'a, M: Mutability>(ptr: CassExclusivePtr<Self, M>) -> Option<&'a Self> { | ||
ptr.into_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.
Do we need to expose into_ref
? It is quite easy to cause a memory leak with 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.
I investigated where we previously needed to erase the lifetime. There were two cases:
- iterators with
'static
references. I replaced them with generic lifetimes - CassSession
'static
reference passed to an async block. I changed it, so we pass anArc
to the async block. This is a good change (and a fix), because cpp-driver assumes that session object can be prematurely free'd by the user. It should not cause segfaults in executions that haven't been awaited yet. What's more, there is a test that expects that prematurely freed session awaits all the futures before closing. We are not doing that yet, thus the test is not enabled.
After adjusting these two things, I could get rid of into_ref
method.
cf2dcff
to
66b329e
Compare
v3: New
|
cpp-driver does not increase the reference count. The lifetime of the iterator is bound to the lifetime of result.
It's more readable (and safe) to have an explicit lifetime instead of lifetime-erased references.
cpp-driver assumes that session object can be prematurely dropped. This means, that we should increase the reference count in functions where we pass the session to an async block. This will prevent UAF. There actually is a test case for this (AsyncTests::Close). However, we cannot enable it yet, since it expects that prematurely dropped session awaits all async tasks and before closing.
It's required to enable the doctests for the crate. I also marked the code snippets from documentation in binding.rs with `text`. They should not be run as doc tests.
66b329e
to
6225480
Compare
This commit introduces a `CassPtr` type, generic over pointer `Properties`. It allows specific pointer-to-reference conversions based on the guarantees provided by the pointer type.
6225480
to
bc520d3
Compare
Existing `Ref/Box/Arc`FFIs are adjusted, so they now allow interaction with new pointer type. You can say, that for a user of `argconv` API, new type is opaque. The only way to operate on it is to use the corresponding ffi API.
Disallow implementing two different APIs for specific type.
bc520d3
to
69d46b3
Compare
This is a follow-up to: #207.
It defines a
CassPtr
type, wrapper overOption<NonNull<T>>
, generic over the pointerProperties
. Two properties we do care about:It gives us even more guarantees about the pointers. Specifically, it gives us the guarantee about the origin of the pointer - they can only be constructed via
Box/Ref/ArcFFI
APIs. We lacked this guarantee before this PR.Pre-review checklist
[ ] I have enabled appropriate tests in.github/workflows/build.yml
ingtest_filter
.[ ] I have enabled appropriate tests in.github/workflows/cassandra.yml
ingtest_filter
.