Free wrapper Box created by wrapPointer in toUint8Array#40
Open
LautaroPetaccio wants to merge 1 commit intolivekit:mainfrom
Open
Free wrapper Box created by wrapPointer in toUint8Array#40LautaroPetaccio wants to merge 1 commit intolivekit:mainfrom
LautaroPetaccio wants to merge 1 commit intolivekit:mainfrom
Conversation
UniffiRustBufferValue.toUint8Array calls wrapPointer to hand the buffer
data to restorePointer. ffi-rs's wrapPointer heap-allocates a Box that
wraps the inner pointer:
env.create_external(
Box::into_raw(Box::new(ptr)),
Some(std::mem::size_of::<*mut c_void>() as i64),
)
Per ffi-rs's documented contract, the JsExternal does not free its
underlying memory on garbage collection - the wrapper Box only goes
away if freePointer is called on it. Nothing here calls freePointer,
so every read of a Rust buffer through toUint8Array (and therefore
every consumeIntoUint8Array, which is hit on every error response in
uniffiCheckCallStatus) leaks a pointer-sized Box.
Capture the wrapPointer result in a local, pass it into restorePointer,
and reclaim it in a finally block. ffi-rs's free path for a U8Array is
just Box::from_raw on the wrapper, so the underlying buffer the wrapper
points at - which is owned by the Rust side and freed separately via
rustbuffer_free - is left intact.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
UniffiRustBufferValue.toUint8Arrayallocates a wrapperBoxviawrapPointerand never reclaims it. Free it in afinallyblock.Why this is a leak
toUint8Arraydoes:ffi-rs'swrap_pointerheap-allocates a freshBoxaround the inner pointer:Per ffi-rs's documented contract — "JsExternal objects do NOT automatically free memory upon garbage collection. Use
freePointerto free the pointer when it is no longer in use." — that wrapperBoxis only reclaimed whenfreePointeris called on the resulting external.restorePointeris documented (and implemented) as a read-only conversion. Nothing here callsfreePointer, so the wrapperBoxis leaked on every invocation.toUint8Arrayis on the hot path for error handling:uniffiCheckCallStatuscallsconsumeIntoUint8Array(which callstoUint8Array) for everyCALL_ERRORandCALL_UNEXPECTED_ERRORresponse with a non-nullerror_buf. It's also reachable directly from user code lifting buffer-shaped return values. Each call leaks one pointer-sizedBox.How the fix works
Capture the
wrapPointerresult in a local (wrapped) so it can be referenced in both thetryandfinallyhalves, and liftNumber(this.struct.len)intolengthso thearrayConstructorshape is byte-identical between therestorePointerand the matchingfreePointer. AfterrestorePointerreads the bytes out,freePointer({ paramsType: [arrayConstructor({ type: DataType.U8Array, length })], paramsValue: wrapped, pointerType: PointerType.RsPointer })runs unconditionally.ffi-rs's free path for a
U8Array(free_rs_pointer_memory→RefDataType::U8Array) isBox::from_raw(ptr as *mut *mut u8), which only reclaims the wrapper Box. The underlying buffer the wrapper points at is owned by the Rust side and is released separately throughrustbuffer_freewhendestroy()runs — so it stays intact.