Skip to content

fix: prevent WeakReference::lock() from resurrecting a value mid final-release - #1467

Open
jslok wants to merge 1 commit into
mrousavy:mainfrom
jslok:fix/weakreference-lock-resurrection-race
Open

fix: prevent WeakReference::lock() from resurrecting a value mid final-release#1467
jslok wants to merge 1 commit into
mrousavy:mainfrom
jslok:fix/weakreference-lock-resurrection-race

Conversation

@jslok

@jslok jslok commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Part 1 of 3 - #1464 split into atomic PRs as requested. This one is fully standalone and can merge on its own; the other two build on it (order: this one, then #1468, then #1469).

The race

lock() checks isDeleted and then unconditionally increments the strong count. But ~BorrowingReference decrements the count before calling forceDestroyValue(), and holds no mutex while doing so. A lock() landing in that window hands out a strong reference to a value whose final release is already in flight - and the resurrected reference's destructor then runs a second forceDestroyValue() concurrently with the releaser's.

Nitro allows HybridObject/callback releases on any thread, so the window is reachable.

The fix

lock() now claims the strong count with an increment-if-not-zero compare-exchange loop, exactly like weak_ptr::lock(). A zero strong count means the final release is already under way, so returning null there is simply correct. The private WeakReference -> BorrowingReference lock-constructor no longer increments, since lock() has already claimed the count.

Testing

Compile-checked (NDK clang, C++20) and reasoned through; this specific change has not been soaked on-device on its own, so please review closely. Formatted to match config/.clang-format output in the surrounding code.

🤖 Generated with Claude Code

…l-release

lock() checked isDeleted and then unconditionally incremented the strong
count, but ~BorrowingReference decrements the count BEFORE calling
forceDestroyValue() and holds no mutex while doing so. A lock() landing in
that window handed out a strong reference to a value whose final release
was already in flight, and the resurrected reference's destructor then ran
a second forceDestroyValue() concurrently with the releaser's.

lock() now claims the strong count with an increment-if-not-zero
compare-exchange loop, exactly like weak_ptr::lock(), and returns null if
the count already reached zero. The private WeakReference ->
BorrowingReference lock-constructor no longer increments, since lock() has
already claimed the count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
nitro-docs Skipped Skipped Aug 4, 2026 10:56pm

Request Review

@hmelonjp

hmelonjp commented Aug 6, 2026

Copy link
Copy Markdown

Stumbled onto this while investigating a memory leak

I can confirm that this fixes a real, measurable leak for us — release builds on physical
devices, before/after on the same hardware, on both platforms.

RN 0.86 (bridgeless, Hermes), react-native-vision-camera 5.2.1, a HybridObject
method called per frame at 15 fps from a useFrameOutput frame processor.
Android: native heap alloc via dumpsys meminfo. iOS: physical footprint via
devicectl. Both fit the minimum of each 60-minute window, since per-frame churn
swings the instantaneous figure by tens of percent while a leak raises the floor.

device build span memory floor trend
Samsung SC-53D, Android 16 0.36.5 9.3 h 86 → 275 MB, no plateau +15.3 MB/h (R²=0.97)
Samsung SC-53D, Android 16 0.36.5 + #1467 + #1468 + #1469 28.0 h 113.6–115.6 MB none — 27 consecutive hourly floors spanning 2.0 MB
Samsung SM-S931Z, Android 16 0.36.5 24.2 h 116 → 851 MB, no plateau +30.7 MB/h (R²=0.99)
Samsung SM-S931Z, Android 16 0.36.5 + #1467 + #1468 + #1469 17.1 h 107.0–111.5 MB none — 16 consecutive hourly floors spanning 4.5 MB
iPhone SE2, iOS 18 0.36.5 8.0 h 98 → 313 MB, no plateau +30.5 MB/h (R²=0.998)
iPhone SE2, iOS 18 0.36.5 + #1467 + #1468 + #1469 9.8 h 90.0–94.9 MB none — 10 consecutive hourly floors spanning 4.9 MB

Thank you so much

Looking forward for the release that includes these

@mrousavy

mrousavy commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Thanks for validating the fix, I haven't merged it yet because I am not fully happy with code-style.

I will get to this very soon to make the code better myself, then merge.

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.

3 participants