This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new c62002b036 fix(ffi): make FFI_ArrowSchema::with_metadata unsafe
(#10764)
c62002b036 is described below
commit c62002b036a0b0c6fd78053c03735f68aedf8ca4
Author: Aditya Mishra <[email protected]>
AuthorDate: Wed Aug 26 12:41:10 2026 +0530
fix(ffi): make FFI_ArrowSchema::with_metadata unsafe (#10764)
# Which issue does this PR close?
- Closes #10679
- Documents #10286 (see the note below)
# Rationale for this change
`with_metadata` reads `self.private_data` as a `SchemaPrivateData` and
writes to it,
[here](https://github.com/bit2swaz/arrow-rs/blob/28a831929ac894a7d0f6b9121c273d38841fc685/arrow-schema/src/ffi.rs#L255).
but `private_data` is only a `SchemaPrivateData` when arrow-rs built the
schema. so on any other schema this is undefined behavior, and you can
trigger it from safe code two ways:
- **foreign schema (#10679):** one imported from pyarrow, C++ or java
owns its own `private_data`, so reading it as ours is UB
- **empty schema (#10286):** `empty()` sets `private_data` to null, so
the read becomes `Box::from_raw(null)`
we cant tell these apart at runtime: the c data interface says
`private_data` is 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_metadata` is now
`unsafe`](https://github.com/bit2swaz/arrow-rs/blob/28a831929ac894a7d0f6b9121c273d38841fc685/arrow-schema/src/ffi.rs#L207-L215).
the safety doc says `self` must be a schema this crate produced, and
spells out that a foreign schema or `empty()` is undefined behavior.
- the two callers inside this crate (`TryFrom<&Field>` and
`TryFrom<&Schema>`) now use an `unsafe` block. both build the schema
themselves, so they meet the new rule.
# Are these changes tested?
the existing `test_set_field_metadata` covers the supported path: it
builds a schema with `try_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-isolation` config CI uses.
no test drives the foreign or `empty()` case, since both are now
undefined behavior you'd have to opt into with `unsafe`.
# Are there any user-facing changes?
yes, this is breaking: `FFI_ArrowSchema::with_metadata` is now `unsafe`,
so callers need an `unsafe` block and must pass a schema arrow-rs built.
# Note on #10286
this doesn't make `with_metadata` on an empty schema safe. it makes it
documented misuse of an `unsafe` function, 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.
---
arrow-schema/src/ffi.rs | 28 ++++++++++++++++++++--------
1 file changed, 20 insertions(+), 8 deletions(-)
diff --git a/arrow-schema/src/ffi.rs b/arrow-schema/src/ffi.rs
index 3e1fe38082..867d1409b9 100644
--- a/arrow-schema/src/ffi.rs
+++ b/arrow-schema/src/ffi.rs
@@ -203,8 +203,17 @@ impl FFI_ArrowSchema {
Ok(self)
}
- /// Add metadata to the schema
- pub fn with_metadata<I, S>(mut self, metadata: I) -> Result<Self,
ArrowError>
+ /// Add metadata to the schema.
+ ///
+ /// # Safety
+ ///
+ /// `self` must be a schema this crate produced (e.g. via
+ /// [`FFI_ArrowSchema::try_new`] or a `TryFrom`), not one from a foreign
+ /// producer and not [`FFI_ArrowSchema::empty`]. It reinterprets
+ /// `private_data` as our own type, so any other schema is undefined
+ /// behavior. See <https://github.com/apache/arrow-rs/issues/10679> and
+ /// <https://github.com/apache/arrow-rs/issues/10286>.
+ pub unsafe fn with_metadata<I, S>(mut self, metadata: I) -> Result<Self,
ArrowError>
where
I: IntoIterator<Item = (S, S)>,
S: AsRef<str>,
@@ -876,10 +885,11 @@ impl TryFrom<&Field> for FFI_ArrowSchema {
flags |= Flags::DICTIONARY_ORDERED;
}
- FFI_ArrowSchema::try_from(field.data_type())?
+ let schema = FFI_ArrowSchema::try_from(field.data_type())?
.with_name(field.name())?
- .with_flags(flags)?
- .with_metadata(field.metadata())
+ .with_flags(flags)?;
+ // SAFETY: schema was just constructed by this crate.
+ unsafe { schema.with_metadata(field.metadata()) }
}
}
@@ -888,8 +898,9 @@ impl TryFrom<&Schema> for FFI_ArrowSchema {
fn try_from(schema: &Schema) -> Result<Self, ArrowError> {
let dtype = DataType::Struct(schema.fields().clone());
- let c_schema =
FFI_ArrowSchema::try_from(&dtype)?.with_metadata(&schema.metadata)?;
- Ok(c_schema)
+ let c_schema = FFI_ArrowSchema::try_from(&dtype)?;
+ // SAFETY: c_schema was just constructed by this crate.
+ unsafe { c_schema.with_metadata(&schema.metadata) }
}
}
@@ -1083,7 +1094,8 @@ mod tests {
.unwrap();
for metadata in metadata_cases {
- schema = schema.with_metadata(&metadata).unwrap();
+ // SAFETY: schema was constructed by this crate via try_new.
+ schema = unsafe { schema.with_metadata(&metadata) }.unwrap();
let field = Field::try_from(&schema).unwrap();
assert_eq!(field.metadata(), &metadata);
}