Skip to content

Free input data pointer when rustbuffer_from_bytes throws#39

Open
LautaroPetaccio wants to merge 1 commit intolivekit:mainfrom
LautaroPetaccio:fix/free-allocate-with-bytes-data-pointer-on-error
Open

Free input data pointer when rustbuffer_from_bytes throws#39
LautaroPetaccio wants to merge 1 commit intolivekit:mainfrom
LautaroPetaccio:fix/free-allocate-with-bytes-data-pointer-on-error

Conversation

@LautaroPetaccio
Copy link
Copy Markdown

Summary

UniffiRustBufferValue.allocateWithBytes only frees its input-data pointer on the happy path. Move the freePointer call into a finally block so the allocation is reclaimed even when the underlying Rust call throws.

Why this is a leak

The current shape is:

const [ dataPointer ] = createPointer({ ... });
const rustBuffer = uniffiCaller.rustCall(/* ... */);  // may throw
freePointer({ paramsValue: [dataPointer], ... });

ffi-rs's createPointer for a U8Array does (in utils/dataprocess.rs):

RsArgsValue::U8Array(buffer, _) => {
  let buffer = buffer.unwrap();
  let ptr = buffer.as_ptr();
  std::mem::forget(buffer);
  Ok(Box::into_raw(Box::new(ptr)) as *mut c_void)
}

It heap-allocates a wrapper Box<*mut u8> that must be reclaimed via freePointer (the pointee Uint8Array itself is V8-managed and GC'd separately). Because JsExternal does not free its underlying memory on garbage collection — per ffi-rs's documented contract — the wrapper Box stays live until the matching freePointer runs.

If uniffiCaller.rustCall throws — which is exactly what happens when Rust returns CALL_ERROR, panics, or any lower-level exception bubbles up — control skips over the freePointer call. The pointer leaks. The leak is small per occurrence but is deterministic on every error path that flows through this allocator (notably anywhere allocateEmpty or buffer construction is used during error-buffer round-tripping).

How the fix works

Wrap the rustCall and the UniffiRustBufferValue construction in try { ... } finally { freePointer(...) }. The freePointer arguments are unchanged — same paramsType, same paramsValue, PointerType.RsPointer — so the success path frees the wrapper exactly as before, and the failure path now does too. ffi-rs's free path for U8Array (free_rs_pointer_memory) just Box::from_raws the wrapper, leaving the underlying buffer untouched, so this is safe to run unconditionally.

UniffiRustBufferValue.allocateWithBytes calls createPointer to wrap the
input bytes, hands the inner pointer to ffi_rustbuffer_from_bytes
through uniffiCaller.rustCall, and only then calls freePointer. If the
rustCall throws (panic, CALL_ERROR with a lifted error, or any other
exception), control jumps over the freePointer call and the wrapper Box
allocated by ffi-rs is leaked. The leak is small per occurrence but is
guaranteed on every failure path that goes through this allocator.

Move the freePointer call into a finally block so the pointer is always
released, regardless of how the rustCall returns.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant