Tags: NativeScript/ios
Tags
fix(runtime): null-initialize Runtime::isolate_ (#460) Init assigns isolate_ only after the context and its bindings are set up, so from construction until then the member holds an indeterminate value. The destructor and GetIsolate() read it unconditionally, and currentRuntime_ already points at the runtime from the constructor on. Initialize it to nullptr, like napiEnv_, so a runtime that has not completed Init reports no isolate instead of garbage.
fix(runtime): interop memory-safety fixes from the worker memory-corr… …uption hunt (#458) * fix(runtime): single-owner adapter wrappers and reentrancy-safe disposal The collection adapters attached an ObjCDataWrapper to their JS object unconditionally, and DisposeValue's ObjCObject branch releases the adapter with the wrapper pointer cached in a local -- the adapter's -dealloc, running inside that release, freed the same wrapper the tail then deleted again. The double-free's recycled chunk corrupted live allocations (captured in the field as a registered persistent's slot word zeroed while its node stayed armed), surfacing as three distinct GC crash signatures on worker isolates within seconds of heavy collection marshalling. Ownership is now single and explicit: a JS object's internal field holds at most one wrapper and owns it; the first adapter to attach wins, a later one stays detached and never writes or clears the field; dataWrapper_ is a claim token for recognising our own wrapper, not an ownership handle. Every path that runs arbitrary code between reading the field and freeing it -- DisposeValue's tail and __releaseNativeCounterpart -- re-reads the field and frees only a wrapper still attached. DictionaryAdapter also gains the object_->Reset() the other adapters already had (its absence leaked the armed global-handle node), and its key enumerators retain the adapter -- NSEnumerator semantics -- so the reset cannot empty the persistent under a live enumeration. New GCFinalizerTests specs pin the ownership contract under the production workload mix (adapter marshalling interleaved with native TextDecoder/atob churn, finalizer-driven releases, a worker-isolate variant); they are tripwires -- the old double-free needs a guard-malloc/ASan lane to abort deterministically. Suite 1512/0. * fix(interop): zero-initialize the NSError out-parameter buffer A callee writes *error only on failure, so on success the read-back found whatever the malloc chunk last held. A non-null stale value was then sent localizedDescription and -- read through a __strong pointer -- retained and released by ARC: an over-release of whatever object now lives at that address, prematurely freeing live allocations whose owners keep writing through dangling references. Those writes landing in recycled GC bookkeeping produced the worker-isolate crash family this branch chases; under MallocScribble the stale read reproduces deterministically at boot as an unrecognized-selector throw on the scribble pattern. The other transient interop buffers are fully written before any read; this was the only uninitialized read-back. * feat(worker): name looper threads after their entry script Crash reports previously showed every worker as an anonymous NSOperationQueue thread; the thread name now carries the worker id and script basename (worker3:pixelmap-socket.js). * fix(runtime): pin the backing store for the NSDataAdapter's lifetime The adapter's persistent pins the JS object, not its bytes: a postMessage transfer detaches the ArrayBuffer and hands the store to another isolate, whose GC can free the memory while native code still holds this NSData — an async reader/writer then touches a freed, recycled chunk. Holding the BackingStore shared_ptr keeps the bytes alive for the adapter's lifetime, which is the contract NSData callers assume, and lets -bytes answer without unlocked cross-thread V8 access. The never-materialized-view branch now serves one stable copy freed in dealloc instead of leaking a fresh malloc per call. * fix(runtime): snapshot the NSDataAdapter length with its pinned store NSData is immutable — length must not change for the object's lifetime — but the live ByteLength() read reported zero after a transfer detach while the pinned bytes stayed valid, and it was also the adapter's last unlocked cross-thread V8 access. * fix(runtime): materialize the on-heap view copy at adapter init The lazy first--bytes copy ran isolate APIs from whatever thread the caller was on, could race concurrent callers on the publication, and a view detached before the first call would expose an uninitialized allocation. Copying during init — isolate owned, view alive — removes the lazy path entirely, so every -bytes branch answers from native storage. * fix(runtime): free the adapter's wrapper claim after isolate teardown An adapter released after its isolate died skipped the whole cleanup block and orphaned its attached wrapper — one 48-byte leak per adapter on every worker teardown. With the isolate gone the JS object and every other reader or deleter of the claim are gone too (all IsValid-gated), so the owner can free it unconditionally. * fix(runtime): let the JSBlock own the block cache attached to its function Passing a JS function as a block argument caches a BlockWrapper on the function so repeat calls reuse the same block. That wrapper was freed only from the JSBlock dispose helper, and only by looking it up through the cached function -- which needs a live isolate. A block built on a worker isolate that outlives it, or released after it, skipped that branch entirely and orphaned the wrapper: two 48-byte tns::BlockWrapper leaks per TestRunner run, both allocated under WorkerWrapper::BackgroundLooper (NativeCallbackWorker and TeardownCrashWorker install an NSNotificationCenter observer block at module load). The block now carries the wrapper pointer, so disposal frees it without the isolate, and native code holding the block keeps the wrapper reachable in the meantime. While the isolate is alive the wrapper is freed only when the function's slot still points at it, the same ownership rule ObjectManager::DisposeValue applies -- __releaseNativeCounterpart can retire the same wrapper first. Disposal stays inline and callback_->Reset() stays unconditional. * docs(runtime): document the known losing-emplace leak in isImplementedInClass Freeing the loser is unsafe (never-initialized instance of an arbitrary class, arbitrary thread) and parking it merely converts the leak into perpetual retention; the leak stays, visible and explained, tracked by issue #459. * fix(runtime): make the adapter the sole owner of its wrapper claim __releaseNativeCounterpart could delete an adapter's attached claim while a native reference kept the adapter alive; if the isolate then died before the adapter's -dealloc, the teardown branch freed the stale pointer again. Claims are now marked and retirement paths leave them attached — the only deleter is the adapter's own -dealloc, in either isolate state.
fix(runtime): interop memory-safety fixes from the worker memory-corr… …uption hunt (#458) * fix(runtime): single-owner adapter wrappers and reentrancy-safe disposal The collection adapters attached an ObjCDataWrapper to their JS object unconditionally, and DisposeValue's ObjCObject branch releases the adapter with the wrapper pointer cached in a local -- the adapter's -dealloc, running inside that release, freed the same wrapper the tail then deleted again. The double-free's recycled chunk corrupted live allocations (captured in the field as a registered persistent's slot word zeroed while its node stayed armed), surfacing as three distinct GC crash signatures on worker isolates within seconds of heavy collection marshalling. Ownership is now single and explicit: a JS object's internal field holds at most one wrapper and owns it; the first adapter to attach wins, a later one stays detached and never writes or clears the field; dataWrapper_ is a claim token for recognising our own wrapper, not an ownership handle. Every path that runs arbitrary code between reading the field and freeing it -- DisposeValue's tail and __releaseNativeCounterpart -- re-reads the field and frees only a wrapper still attached. DictionaryAdapter also gains the object_->Reset() the other adapters already had (its absence leaked the armed global-handle node), and its key enumerators retain the adapter -- NSEnumerator semantics -- so the reset cannot empty the persistent under a live enumeration. New GCFinalizerTests specs pin the ownership contract under the production workload mix (adapter marshalling interleaved with native TextDecoder/atob churn, finalizer-driven releases, a worker-isolate variant); they are tripwires -- the old double-free needs a guard-malloc/ASan lane to abort deterministically. Suite 1512/0. * fix(interop): zero-initialize the NSError out-parameter buffer A callee writes *error only on failure, so on success the read-back found whatever the malloc chunk last held. A non-null stale value was then sent localizedDescription and -- read through a __strong pointer -- retained and released by ARC: an over-release of whatever object now lives at that address, prematurely freeing live allocations whose owners keep writing through dangling references. Those writes landing in recycled GC bookkeeping produced the worker-isolate crash family this branch chases; under MallocScribble the stale read reproduces deterministically at boot as an unrecognized-selector throw on the scribble pattern. The other transient interop buffers are fully written before any read; this was the only uninitialized read-back. * feat(worker): name looper threads after their entry script Crash reports previously showed every worker as an anonymous NSOperationQueue thread; the thread name now carries the worker id and script basename (worker3:pixelmap-socket.js). * fix(runtime): pin the backing store for the NSDataAdapter's lifetime The adapter's persistent pins the JS object, not its bytes: a postMessage transfer detaches the ArrayBuffer and hands the store to another isolate, whose GC can free the memory while native code still holds this NSData — an async reader/writer then touches a freed, recycled chunk. Holding the BackingStore shared_ptr keeps the bytes alive for the adapter's lifetime, which is the contract NSData callers assume, and lets -bytes answer without unlocked cross-thread V8 access. The never-materialized-view branch now serves one stable copy freed in dealloc instead of leaking a fresh malloc per call. * fix(runtime): snapshot the NSDataAdapter length with its pinned store NSData is immutable — length must not change for the object's lifetime — but the live ByteLength() read reported zero after a transfer detach while the pinned bytes stayed valid, and it was also the adapter's last unlocked cross-thread V8 access. * fix(runtime): materialize the on-heap view copy at adapter init The lazy first--bytes copy ran isolate APIs from whatever thread the caller was on, could race concurrent callers on the publication, and a view detached before the first call would expose an uninitialized allocation. Copying during init — isolate owned, view alive — removes the lazy path entirely, so every -bytes branch answers from native storage. * fix(runtime): free the adapter's wrapper claim after isolate teardown An adapter released after its isolate died skipped the whole cleanup block and orphaned its attached wrapper — one 48-byte leak per adapter on every worker teardown. With the isolate gone the JS object and every other reader or deleter of the claim are gone too (all IsValid-gated), so the owner can free it unconditionally. * fix(runtime): let the JSBlock own the block cache attached to its function Passing a JS function as a block argument caches a BlockWrapper on the function so repeat calls reuse the same block. That wrapper was freed only from the JSBlock dispose helper, and only by looking it up through the cached function -- which needs a live isolate. A block built on a worker isolate that outlives it, or released after it, skipped that branch entirely and orphaned the wrapper: two 48-byte tns::BlockWrapper leaks per TestRunner run, both allocated under WorkerWrapper::BackgroundLooper (NativeCallbackWorker and TeardownCrashWorker install an NSNotificationCenter observer block at module load). The block now carries the wrapper pointer, so disposal frees it without the isolate, and native code holding the block keeps the wrapper reachable in the meantime. While the isolate is alive the wrapper is freed only when the function's slot still points at it, the same ownership rule ObjectManager::DisposeValue applies -- __releaseNativeCounterpart can retire the same wrapper first. Disposal stays inline and callback_->Reset() stays unconditional. * docs(runtime): document the known losing-emplace leak in isImplementedInClass Freeing the loser is unsafe (never-initialized instance of an arbitrary class, arbitrary thread) and parking it merely converts the leak into perpetual retention; the leak stays, visible and explained, tracked by issue #459. * fix(runtime): make the adapter the sole owner of its wrapper claim __releaseNativeCounterpart could delete an adapter's attached claim while a native reference kept the adapter alive; if the isolate then died before the adapter's -dealloc, the teardown branch freed the stale pointer again. Claims are now marked and retirement paths leave them attached — the only deleter is the adapter's own -dealloc, in either isolate state.
fix(runtime): finalizer-safe handle ownership and deferred JSBlock te… …ardown (#457) * fix(runtime): finalizer-safe ownership for registered handles and deferred JSBlock teardown An in-flight kFinalizer callback could have its state freed or its node reset underneath it, breaking the contract that a finalizer either resets its handle or re-arms it and corrupting the drain's bookkeeping (the production V8_Fatal CHECKs on worker isolates). ObjectWeakCallbackState now has exactly two deleting sites -- FinalizerCallback's disposed branch and DisposeAllRegistered -- and every other retirement resets the persistent first, which frees the node, clears the pending bit and guarantees no further callback. A disposing flag makes a reentrant retirement (a -dealloc reached from DisposeValue calling __releaseNativeCounterpart) defer to the frame that owns the state. FinalizerCallback re-checks handle emptiness after DisposeValue: an adapter dealloc can reset the very persistent being finalized, and ClearWeak on an empty handle writes through a dead slot, so a handle emptied underneath its callback is retired, never re-armed. __releaseNativeCounterpart gains its missing Reset -- it retired registrations by deleting the state while leaving the node rooted forever with parameter() dangling at freed memory. The JSBlock dispose helper no longer does V8 work inline: the last native release can land on any thread, including inside the finalizer drain via dealloc cascades, so handle teardown posts to the owning isolate's event loop (a refused post means the isolate is gone and only native memory remains). The block pointer is cleared synchronously, and marshalling builds a fresh block for a wrapper whose JSBlock already died. Removes DisposerPHV (dead since VisitHandlesWithClassIds went away) and an unlocked, guardless Reset in NSDataAdapter's dealloc. Suite green; new GCFinalizerTests specs cover the retired-handle collectability contract, dealloc-cascade reentrancy, and the natively-held-block production shape. Reverting only the added Reset crashes the runtime outright. * fix(runtime): keep JSBlock dispose inline and guard the teardown walk's claim The dispose helper goes back to inline teardown under the isolate Locker: callback_ is a strong, unregistered persistent, so resetting it never touches the finalizer drain's bookkeeping, and a foreign-thread Locker into the block's own isolate is legitimate now that extended class names are worker-scoped. The deferred posting -- and the cleared-block re-marshal machinery it required -- is removed; the unconditional callback_ Reset stays, since an already-detached callback still owns its node. FinalizerCallback now honors the disposing claim on entry: a nested collection during DisposeAllRegistered's walk can condemn a pre-claimed state, and disposing it there would free memory the walk still holds. The callback re-arms its node -- satisfying the finalizer contract -- and leaves clear, reset and delete to the owner. Block-collectability specs poll instead of assuming a single tick suffices; drain interleaving makes one tick a coin flip either way.
PreviousNext