Skip to content

Commit 6d65a8a

Browse files
authored
fix: fix dv descriptor map insert potential FFI pointer leak (#3265)
## What changes are proposed in this pull request? <!-- **Uncomment** this section if there are any changes affecting public APIs. Else, **delete** this section. ### This PR affects the following public APIs If there are breaking changes, please ensure the `breaking-changes` label gets added by CI, and describe why the changes are needed. Note that _new_ public APIs are not considered breaking. --> Currently, the rust -> FFI bridge -> caller contract is built on the following assumption: 1. **Borrow:** Rust accesses the handle with `as_ref()` (`&T`) or `as_mut()` (`& mut T`). The caller retains ownership and remains responsible for passing the handle to its `free_*` function. 2. **Unconditional consume:** Rust calls `into_inner()` before any fallible work. Rust owns the value from native entry onward and is responsible for dropping it on every result, including errors. The caller must not use or free the handle after the call. The `dv_descriptor_map_insert` did not comply with either of these 2 above invariants, as it ran potentially errorsome checks against an input string pointer before taking ownership of said pointer. This made it difficult for FFI callers to know when it was safe to consider the native memory fully handed-off to Rust. This PR ensures `dv_descriptor_map_insert` takes full ownership of the provided pointer before running path compliance checks so it remains compliant with the second invariant above. The intended goal of this is to make FFI integrations easier at the connector level, and pass more of the implementation complexity onto core kernel. ## How was this change tested? UTs
1 parent 4724eef commit 6d65a8a

3 files changed

Lines changed: 89 additions & 26 deletions

File tree

ffi/CLAUDE.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ options, etc.). Short-lived "plain old data" types like `ExternResult`, `KernelE
1717

1818
Every handle has a corresponding `free_*` function (e.g. `free_engine`, `free_snapshot`).
1919

20+
Handle parameters follow one of two ownership contracts:
21+
22+
1. **Borrow:** Rust accesses the handle with `as_ref()` or `as_mut()`. The caller retains
23+
ownership and remains responsible for passing the handle to its `free_*` function.
24+
2. **Unconditional consume:** Rust calls `into_inner()` before any fallible work. Rust owns the
25+
value from native entry onward and is responsible for dropping it on every result, including
26+
errors. The caller must not use or free the handle after the call.
27+
28+
Do not conditionally consume a handle only when a fallible operation succeeds. Every function's
29+
safety documentation must state whether each handle is borrowed or consumed regardless of the
30+
result. For consuming functions, perform string parsing, visitor decoding, validation, and other
31+
fallible work only after all consumed handles have been converted with `into_inner()`.
32+
2033
## Error Handling
2134

2235
Fallible functions return `ExternResult` (tagged union of Ok/Err). The caller provides an
@@ -166,9 +179,9 @@ transaction()
166179

167180
The engine authors the DV file and passes descriptor fields to `dv_descriptor_new`. The
168181
descriptor map and scan iterator are both consumed by `transaction_update_deletion_vectors`;
169-
descriptor handles are consumed by `dv_descriptor_map_insert` only on success and must be
170-
freed by the caller on error. DV updates require both the `deletionVectors` reader/writer
171-
feature and `delta.enableDeletionVectors=true`.
182+
descriptor handles are consumed by `dv_descriptor_map_insert` regardless of the result. DV
183+
updates require both the `deletionVectors` reader/writer feature and
184+
`delta.enableDeletionVectors=true`.
172185

173186
## Tracing & Metrics
174187

ffi/examples/update-dv/update_dv.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,12 @@ int main(int argc, char* argv[]) {
163163

164164
ExternResultbool insert_res =
165165
dv_descriptor_map_insert(map, data_file_path_slice, descriptor, engine);
166+
descriptor = NULL; // consumed by dv_descriptor_map_insert regardless of result
166167
if (insert_res.tag != Okbool) {
167168
err = (Error*)insert_res.err;
168169
print_error("dv_descriptor_map_insert failed.", err);
169170
goto cleanup;
170171
}
171-
descriptor = NULL; // consumed by dv_descriptor_map_insert on success
172172

173173
// === Build a fresh scan metadata iterator for the update call ===
174174
ExternResultHandleMutableFfiSnapshotBuilder snapshot_builder_res =

ffi/src/transaction/deletion_vector.rs

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ pub unsafe extern "C" fn free_dv_descriptor_map(map: Handle<ExclusiveDvDescripto
5757
map.drop_handle();
5858
}
5959

60-
/// Free a deletion vector descriptor handle. Only call this if the descriptor has not
61-
/// been moved into a map via [`dv_descriptor_map_insert`].
60+
/// Free a deletion vector descriptor handle. Only call this if the descriptor has not been
61+
/// consumed. [`dv_descriptor_map_insert`] consumes a descriptor handle regardless of its result.
6262
///
6363
/// # Safety
6464
///
@@ -173,9 +173,8 @@ fn dv_descriptor_new_impl(
173173
}
174174

175175
/// Insert a deletion vector descriptor into the map under the given data file path.
176-
/// Consumes the descriptor handle on success. On error (e.g. invalid `data_file_path`),
177-
/// the descriptor handle is left untouched and must still be released by the caller via
178-
/// [`free_dv_descriptor`].
176+
/// Consumes the descriptor handle regardless of whether the insertion succeeds. On error (e.g.
177+
/// invalid `data_file_path`), the descriptor is dropped.
179178
///
180179
/// `data_file_path` must be the data-file path exactly as it appears in the scan
181180
/// metadata produced by the kernel (the Add file action's `path` field). The kernel
@@ -185,7 +184,8 @@ fn dv_descriptor_new_impl(
185184
///
186185
/// # Safety
187186
///
188-
/// Caller must pass valid handles. The descriptor handle is consumed only on success.
187+
/// Caller must pass valid handles. The descriptor handle is consumed and must not be used or freed
188+
/// after this call, regardless of the result.
189189
#[no_mangle]
190190
pub unsafe extern "C" fn dv_descriptor_map_insert(
191191
mut map: Handle<ExclusiveDvDescriptorMap>,
@@ -195,24 +195,21 @@ pub unsafe extern "C" fn dv_descriptor_map_insert(
195195
) -> ExternResult<bool> {
196196
let map_ref = unsafe { map.as_mut() };
197197
let engine_ref = unsafe { engine.as_ref() };
198-
// Parse the path BEFORE taking ownership of the descriptor: if parsing fails the
199-
// descriptor must remain valid so the caller can free it (otherwise we get a UAF
200-
// when they retry or clean up).
198+
let descriptor = unsafe { descriptor.into_inner() };
201199
let path_result = unsafe { TryFromStringSlice::try_from_slice(&data_file_path) };
202-
dv_descriptor_map_insert_impl(map_ref, path_result, descriptor)
200+
dv_descriptor_map_insert_impl(map_ref, path_result, *descriptor)
203201
.map(|_| true)
204202
.into_extern_result(&engine_ref)
205203
}
206204

207205
fn dv_descriptor_map_insert_impl(
208206
map: &mut DvDescriptorMap,
209207
data_file_path: DeltaResult<&str>,
210-
descriptor: Handle<ExclusiveDvDescriptor>,
208+
descriptor: DeletionVectorDescriptor,
211209
) -> DeltaResult<()> {
212210
let path = data_file_path?;
213211
let owned_path = path.to_string();
214-
let descriptor = unsafe { descriptor.into_inner() };
215-
map.inner.insert(owned_path, *descriptor);
212+
map.inner.insert(owned_path, descriptor);
216213
Ok(())
217214
}
218215

@@ -270,7 +267,28 @@ fn transaction_update_deletion_vectors_impl(
270267

271268
#[cfg(test)]
272269
mod tests {
270+
use std::sync::Arc;
271+
272+
use delta_kernel::Engine;
273+
273274
use super::*;
275+
use crate::error::{AllocateError, AllocateErrorFn, KernelError};
276+
use crate::ffi_test_utils::{allocate_err, assert_extern_result_error_with_message};
277+
use crate::{free_engine, ExternEngine};
278+
279+
struct ErrorOnlyExternEngine {
280+
allocate_error: AllocateErrorFn,
281+
}
282+
283+
impl ExternEngine for ErrorOnlyExternEngine {
284+
fn engine(&self) -> Arc<dyn Engine> {
285+
panic!("error-only test engine does not expose a kernel engine")
286+
}
287+
288+
fn error_allocator(&self) -> &dyn AllocateError {
289+
&self.allocate_error
290+
}
291+
}
274292

275293
#[test]
276294
fn kernel_dv_storage_type_maps_to_kernel_storage_type() {
@@ -335,25 +353,57 @@ mod tests {
335353
}
336354

337355
#[test]
338-
fn dv_descriptor_map_insert_error_leaves_descriptor_freeable() {
356+
fn dv_descriptor_map_insert_invalid_path_does_not_insert() {
339357
let mut map = DvDescriptorMap {
340358
inner: HashMap::new(),
341359
};
342-
let descriptor =
360+
let descriptor = unsafe {
343361
dv_descriptor_new_impl(KernelDvStorageType::Inline, Ok("ABC"), false, 0, 4, 1)
344-
.expect("descriptor should be valid");
362+
.expect("descriptor should be valid")
363+
.into_inner()
364+
};
345365

346366
let result = dv_descriptor_map_insert_impl(
347367
&mut map,
348368
Err(Error::generic("bad data file path")),
349-
descriptor.shallow_copy(),
369+
*descriptor,
350370
);
351371

352-
assert!(
353-
result.is_err(),
354-
"insert should fail before consuming descriptor"
355-
);
372+
assert!(result.is_err(), "insert should fail for an invalid path");
356373
assert!(map.inner.is_empty());
357-
unsafe { free_dv_descriptor(descriptor) };
374+
}
375+
376+
#[test]
377+
fn dv_descriptor_map_insert_invalid_utf8_consumes_descriptor() {
378+
let engine: Arc<dyn ExternEngine> = Arc::new(ErrorOnlyExternEngine {
379+
allocate_error: allocate_err,
380+
});
381+
let engine: Handle<SharedExternEngine> = engine.into();
382+
let map = dv_descriptor_map_new();
383+
let descriptor =
384+
dv_descriptor_new_impl(KernelDvStorageType::Inline, Ok("ABC"), false, 0, 4, 1)
385+
.expect("descriptor should be valid");
386+
let invalid_utf8 = [0xff];
387+
let invalid_path = KernelStringSlice {
388+
ptr: invalid_utf8.as_ptr().cast(),
389+
len: invalid_utf8.len(),
390+
};
391+
392+
let result = unsafe {
393+
dv_descriptor_map_insert(
394+
map.shallow_copy(),
395+
invalid_path,
396+
descriptor,
397+
engine.shallow_copy(),
398+
)
399+
};
400+
401+
assert_extern_result_error_with_message(result, KernelError::Utf8Error, None);
402+
unsafe {
403+
free_dv_descriptor_map(map);
404+
free_engine(engine);
405+
}
406+
// The extern call consumed the descriptor on error. Deliberately having no descriptor
407+
// cleanup here lets Miri's leak check catch consume-after-parse regressions.
358408
}
359409
}

0 commit comments

Comments
 (0)