fix(ffi): make FFI struct fields private to close Drop soundness hole - #10431
fix(ffi): make FFI struct fields private to close Drop soundness hole#10431bit2swaz wants to merge 1 commit into
Conversation
|
I believe you fixed the MIRI error on #10433 Merging up to retrigger and get a clean run |
4ad96c5 to
43db76b
Compare
|
looks like we had them (or at least 2/3) private before wonder if @ashdnazg could weigh in here? |
|
right, thanks for bringing that up. i can see that #9772 made these i think both goals can coexist tho. the only soundness relevant fields are one direction i think would suit this would be to make the fields private and give back that one capability through a small @ashdnazg do you mutate the struct in place (swap |
43db76b to
540fd48
Compare
|
Thanks for pinging me! Since I don't want to assume anything about the existing implementation and where the structs came from, I'm accessing the fields directly and I store the original let private_data = Box::new(PrivateData {
original_private_data: schema.private_data,
original_release: schema.release,
// other stuff
});
schema.private_data = Box::into_raw(private_data) as *mut c_void;
schema.release = Some(release_schema);My release function does what it needs with my private data, then restores the original release and private data, and finally calls the original release function: let private_data = unsafe { Box::from_raw(schema.private_data as *mut PrivateData) };
schema.release = private_data.original_release;
schema.private_data = private_data.original_private_data;
// call release if not nullSo in my opinion getters and |
|
There is another problem though - the function
We might want to make these functions unsafe and not public. |
|
ah nice, that settles it then. getters + unsafe setters it is. maps right onto your flow too; read both out, overwrite both, restore on release, and you never have to guess where the struct came from. ill add also, good one pointing out the anyways, appreciate you hopping in on this :) |
|
Sounds good, thanks! |
… on FFI C interface structs
540fd48 to
543edbd
Compare
|
alright, made the changes. went with getters + unsafe setters like we talked about on all three structs ( the |
|
I don't think you can leave any fields For instance, changing |
|
In addition, you can't know what an externally received |
| /// must correctly release this stream (usually by chaining to the returned | ||
| /// one) and must match the [`FFI_ArrowArrayStream::private_data`] it reads. | ||
| /// A wrong callback is undefined behavior on drop. | ||
| pub unsafe fn set_release( |
There was a problem hiding this comment.
Another potential API could be
unsafe fn new_from_parts(...) -> Self {
...
}To allow someone to set the fields as they want, but acknoledging it is not safe. The benefit of the API in this PR is that we won't have to modify the existing API if a new field is added to FFI_Stream in the future
| pub release: Option<unsafe extern "C" fn(arg1: *mut FFI_ArrowArray)>, | ||
| /// Producer-provided release callback. | ||
| /// | ||
| /// Private so safe code can't install a callback that [`Drop`] would invoke. |
There was a problem hiding this comment.
If the goal is to avoid potentially unsafe access do we have to change the other fields to so they aren't pub? For example if I set the buffers field to something invalid that would cause a crash / undefined behavior without requiring an unsafe block 🤔
|
yeah youre both right, ill make all the fields private. i was wrong that the data fields were harmless, my bad on that @ashdnazg's point on reads are already covered, all three structs have typed getters for every field, so nothing new needed there on the write side, ill keep the per-field the |
Which issue does this PR close?
Closes #10429
Rationale for this change
FFI_ArrowArray,FFI_ArrowSchema, andFFI_ArrowArrayStreameach have apub releasefield holding a producer-supplied function pointer, and theirDropimpl calls it. Because the field is public, safe code can build one of these structs with a bogusreleaseand dropping it runs that pointer as a function. That is undefined behavior reachable from safe code.private_datahas the same problem: the release callback owns it, so a mismatched pointer is UB on drop too.The fix has to keep the cross-thread wrap-release use case from #9771 working, where a consumer swaps in its own
release+private_dataand chains back to the original.What changes are included in this PR?
releaseandprivate_dataprivate on all three structs. The other fields staypub, since they can't cause UB and are part of the existing public API.release()andprivate_data().unsafesettersset_release()andset_private_data()that swap the value and return the old one without freeing it, so a consumer can save the originals and chain back on drop. This is the Expose FFI data structures fields #9771 pattern, now behindunsafewhere it belongs.#[repr(C)]is unchanged, so the FFI ABI is the same.Are these changes tested?
Yes. Each struct gets a test that installs a wrapping
release, drops the struct, and checks the wrapper ran and chained to the original. The existing FFI round-trip tests still pass, and the new tests pass under Miri with no leaks or use-after-free.Are there any user-facing changes?
Yes.
releaseandprivate_dataare no longer public fields on the three structs. Code that read or wrote them directly should use the new getters andunsafesetters instead. Everything else is unchanged.