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 2 commits 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! |
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 |
| 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 🤔
There was a problem hiding this comment.
kept just the per-field unsafe setters and skipped new_from_parts for now. lmk if youd rather have it. only fields anyone writes are release + private_data anyway (thats what @ashdnazg's wrap-release does, read both out, swap both in, restore on release). the setters do that in place on a struct from anywhere. new_from_parts builds a fresh one instead, wrong shape for wrapping an existing foreign struct and its signature has to change every time a field gets added, the churn you flagged the setters dodge.
its additive and non-breaking too so easy to add later if a real from-scratch case shows up. didnt wanna ship unsafe api with no caller yet so yeah
|
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 |
543edbd to
37bf69c
Compare
… on FFI C interface structs
37bf69c to
c721e1e
Compare
… safe-code UB hole
|
made those changes. all fields private on all three structs now. you were both right, the data fields are UB vectors too so leaving them kept the
|
Which issue does this PR close?
Closes #10429.
Rationale for this change
FFI_ArrowArray,FFI_ArrowSchema, andFFI_ArrowArrayStreamhad all of their fieldspub. Several of those fields carry invariants that theDropimpl and the import path rely on, so safe code could set them to values that trigger undefined behavior with nounsafeblock:FFI_ArrowSchema::formatornameto a pointer that did not come fromCString::into_rawcauses UB when the release callback frees it withCString::from_raw.FFI_ArrowArray::buffersto an invalid pointer causes UB when it is dereferenced withptr::read_unalignedon import.releasefn pointer (or the stream'sget_schema/get_next/get_last_errorfn pointers) to a bogus function causes UB when it is invoked on drop or import.An earlier revision of this PR made only
releaseandprivate_dataprivate and left the data fieldspub, on the assumption that the data fields were harmless. That was wrong: as noted in review, the pointer-carrying data fields are UB vectors too. Any write to these fields has to be treated as unsafe.What changes are included in this PR?
FFI_ArrowArray,FFI_ArrowSchema, andFFI_ArrowArrayStreamare now private.unsafesettersset_releaseandset_private_data, which swap in a new callback and private data and return the old values so the caller can chain into the original release. No other field needs a write path (the only fields any external consumer writes arereleaseandprivate_data).#[repr(C)]is unchanged, so the C Data Interface layout and ABI are identical.A separate soundness issue with
FFI_ArrowSchema::with_metadata/with_nameon foreign schemas was raised in review. It has a different root cause and changes the safe/unsafe surface of those methods, so it will be handled in its own issue and PR rather than bundled here.Are these changes tested?
Yes. Each struct has a
test_wrap_release_callbacktest covering the swap-and-restore path, and all three pass under Miri with no leak, use-after-free, or double-free. The existing FFI roundtrip tests also pass under Miri. The full workspace builds and tests with--features ffi.Are there any user-facing changes?
Yes, this is a breaking change. Code that read these fields directly must switch to the getters. Code that constructed these structs with a struct literal, or wrote fields directly, must use the safe constructors (
try_from,new,empty) or, forrelease/private_data, theunsafesetters. The#[repr(C)]layout is unchanged.