fix(ffi): make FFI struct fields private to close Drop soundness hole - #10431
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 |
|
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
|
Jefffrey
left a comment
There was a problem hiding this comment.
some suggestions on the documentation; we dont need to repeat ourselves in the internal struct field documentation, just simply state its for safety reasons. then on the setters themselves we can put the safety requirements.
(this goes for the other structs too)
done and applied it to all three structs. the struct fields just say theyre private for safety now, and the safety details live on the setters. i moved the wrap-release note + #9771 link onto btw, i applied your suggestions by hand so the three structs stay consistent, so the diff already has them. best to not click "commit suggestion" or itll double up. lmk if the wording works for you :) |
Jefffrey
left a comment
There was a problem hiding this comment.
- does this also close #10253?
was doing a bit of digging and seems FFI_ArrowArrayStream was generated/added with pub fields from the start, so doesnt seem there was any historical reason to make it public. I guess when using bindgen it just generated as public by default?
yep. #10253 was the same hole on the stream: safe try_new ends up calling a caller-supplied private fields shut that path and #10253 offered private fields as one of its two fixes. |
Which issue does this PR close?
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.