fix(ffi): make FFI_ArrowSchema::with_metadata unsafe - #10764
Conversation
There was a problem hiding this comment.
I thought a bunch more about this
I think the real usecase for the with_metadata is for Arrow-rs itself to add metadata to the schema. I can't really think of a usecase where someone could/should be modifying the metadata on a schema that came from another implementation (e.g py arrow)
Therefore I suggest:
- mark
with_metadataasunsafe(as you have done) - Document that the metadata must come from arrow and that if it already had metadata, the old metadata will be leaked
- Leave the behavior the same
I think there are too many risks to changing the existing behavior and not much upside. Simply getting this function marked unsafe I think is the "safest" approach (no pun intended)
|
e can also document we shouldn't use with_schema with an empty schema |
|
agreed, ill revert that commit then. a schema with
the original on an empty schema was
will do one correction: re setting metadata doesnt leak. the old bytes are in arrow-rs/arrow-schema/src/ffi.rs Lines 179 to 188 in dcc801a note it there instead, or leave for later? |
|
done and pushed. dropped the empty handling, since it no longer makes the empty case safe, i switched the PR to "documents #10286" instead of "closes" |
Lstarsky0
left a comment
There was a problem hiding this comment.
The hole is real — try_new sets release and private_data together at ffi.rs:155 and :173, and nothing else in the type ties them — but I don't think "there's nothing to check" holds. The release callback is the discriminator. Any conforming producer installs its own, arrow-rs installs release_schema, and empty() leaves it None, so self.release() — already public at ffi.rs:286 — answers exactly the question with_metadata needs, via ptr::fn_addr_eq against release_schema.
It fails in the safe direction. A schema this crate did not build cannot carry that address, so the worst case is a spurious CDataInterface error rather than UB. That would let with_metadata stay a safe fn returning Err, which also sidesteps the part I'd want spelled out below.
The one case it gets wrong is the wrapping pattern the comment at ffi.rs:297 describes: a consumer that saves the old callback and installs its own still owns arrow-rs private_data but no longer matches, so it would get an error where today it works. Whether that is acceptable is a judgement call, but it is a much smaller cost than the current one and it is worth weighing explicitly rather than ruling the check out.
Two smaller things.
The title says "guard null private_data" and the diff has no guard — the # Safety clause forbids empty(), which is a contract, not a check. The null case is the one that is trivially checkable even without the release comparison, so it reads like something got dropped.
Turning a safe pub fn into unsafe fn breaks every downstream caller at compile time. That may well be the right call, but the description doesn't say which release this is aimed at, and the safe-fn-plus-error shape above would not need one.
|
the release-callback check was option B on #10679 and i did try it. your wrapping case is a second false reject: a schema that wrapped its release via arrow-rs/arrow-schema/src/ffi.rs Line 315 in 06edc8f thats why @alamb, @Jefffrey and i landed on on the smaller two: title is stale from an earlier null-guard version, ill fix it. and yeah its breaking, goes in the next major |
Which issue does this PR close?
FFI_ArrowSchema::with_metadatawhen used with empty schema #10286 (see the note below)Rationale for this change
with_metadatareadsself.private_dataas aSchemaPrivateDataand writes to it, here. butprivate_datais only aSchemaPrivateDatawhen arrow-rs built the schema. so on any other schema this is undefined behavior, and you can trigger it from safe code two ways:private_data, so reading it as ours is UBFFI_ArrowSchema::with_metadatawhen used with empty schema #10286):empty()setsprivate_datato null, so the read becomesBox::from_raw(null)we cant tell these apart at runtime: the c data interface says
private_datais opaque, so theres nothing to check. the fix is to have the caller promise the schema is ours.What changes are included in this PR?
with_metadatais nowunsafe. the safety doc saysselfmust be a schema this crate produced, and spells out that a foreign schema orempty()is undefined behavior.TryFrom<&Field>andTryFrom<&Schema>) now use anunsafeblock. both build the schema themselves, so they meet the new rule.Are these changes tested?
the existing
test_set_field_metadatacovers the supported path: it builds a schema withtry_new, adds metadata (empty, single, and multi-entry, including re-setting), and checks the round-trip. it passes under miri with the same-Zmiri-disable-isolationconfig CI uses.no test drives the foreign or
empty()case, since both are now undefined behavior you'd have to opt into withunsafe.Are there any user-facing changes?
yes, this is breaking:
FFI_ArrowSchema::with_metadatais nowunsafe, so callers need anunsafeblock and must pass a schema arrow-rs built.Note on #10286
this doesn't make
with_metadataon an empty schema safe. it makes it documented misuse of anunsafefunction, which the safety doc now calls out. if you'd rather keep #10286 open for a separate change, say so and i'll relink it.