Skip to content

fix(runtime): publish an isolate's Caches under the registry lock - #512

Closed
adrian-niculescu wants to merge 1 commit into
NativeScript:mainfrom
adrian-niculescu:fix/memory-pressure-cache-race
Closed

adrian-niculescu wants to merge 1 commit into
NativeScript:mainfrom
adrian-niculescu:fix/memory-pressure-cache-race

Conversation

@adrian-niculescu

@adrian-niculescu adrian-niculescu commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

If the system reports memory pressure while the app or a worker is still starting, the pressure handler and the starting thread touch the same isolate slot without synchronization. I have not seen this cause a failure and I would rate it low severity: it is a data race on a pointer, found while reading the code added in #507.

Runtime::NotifyMemoryPressure runs on a global queue and reads Caches::Get(isolate) for every registered isolate while holding the registry lock. CreateIsolate registers the isolate, but Runtime::Init stored the Caches slot later, outside that lock. In that window the handler normally finds the slot empty, gets the stand-in cache whose gate id matches no gate, and skips the isolate, which is harmless. The read is still unsynchronized with the write that publishes the pointer.

Init now stores the slot under the registry lock. Teardown already follows the same rule: it removes the isolate from the registry under the lock before Caches::Remove clears the slot.

To see the window, I put a sleep before Caches::Init and simulated memory warnings on the simulator during launch: the handler ran for the registered isolate and read the slot before it was set. No test added, since the overlap depends on timing. The TestRunner suite passes.

Summary by CodeRabbit

  • Bug Fixes
    • Improved runtime reliability during startup when the system is under memory pressure. Cache initialization and memory-pressure handling now coordinate their access, reducing the chance of startup issues in these conditions. No user-facing features or settings have changed.

Runtime::NotifyMemoryPressure runs on a global queue and, holding the
isolate registry lock, reads the Caches slot of every registered isolate to
get its gate id. CreateIsolate registers the isolate, but Runtime::Init
stored the slot afterwards without the lock. A memory pressure event that
arrives while a main or worker isolate is starting therefore reads the slot
on one thread while the starting thread writes it.

A reader that finds the slot empty gets the stand-in cache, whose gate id
matches no gate, and skips the isolate, so the practical outcome is benign.
What is left is an unsynchronized read of a pointer while it is published.

Init now stores the slot under the registry lock. Teardown already removes
the isolate from the registry under that lock before Caches::Remove clears
the slot.
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 13f6ad23-db8f-48c2-b36e-3896006a80c9
📥 Commits

Reviewing files that changed from the base of the PR and between 20935bd and 812696e.

📒 Files selected for processing: 1
  • NativeScript/runtime/Runtime.mm

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

📝 Walkthrough

Runtime::Init now initializes the isolate’s caches while holding isolatesMutex_. Memory-pressure notification reads the cache slot under that same lock.

Changes: Cache Initialization

Layer / File(s) Summary
Initialize caches under the isolate lock
NativeScript/runtime/Runtime.mm
Runtime::Init now acquires isolatesMutex_ before calling Caches::Init. Memory-pressure notification reads the cache slot under the same lock.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: edusperoni

Merge Risk: ⚪ Minimal · up to 81269

The cache-locking change has no identified issue requiring a fix before merge.

Pre-merge checks: Passed 5
Passed Passed checks: 5 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: publishing an isolate's Caches under the registry lock to fix the runtime synchronization issue.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checked the cache slot with care
While isolatesMutex_ guarded it there
Runtime::Init began
Caches::Init ran
And the lock kept both readers aware

Comment @coderabbitai help to get the list of available commands.

@adrian-niculescu
adrian-niculescu marked this pull request as ready for review October 13, 2026 20:00
edusperoni added a commit that referenced this pull request Oct 13, 2026
Setting up a runtime took CreateIsolate, Init and, for workers, SetWorkerId,
assembled by hand at each call site. The isolate joined the live registry in
CreateIsolate, before Init wrote its Caches slot, so a thread that found it
there (the memory-pressure handler, #512) could read that slot while it was
being written. CreateIsolate could also be called twice on one Runtime, and a
Runtime deleted before Init left its isolate registered and never disposed.

- Runtime(const RuntimeOptions&) creates the isolate, homed on the calling
  thread, and takes the limits and worker id up front, so IsWorker() is
  correct during a worker's Init. The caller still locks the isolate and can
  set it up (WatchHeapLimit) before calling Init().
- Init() publishes the isolate to the registry only after writing its Caches
  slot. With the teardown already unregistering before clearing it, a thread
  that finds an isolate in the registry may read its slots under the lock.
- A Runtime deleted before Init disposes its unpublished isolate.
- CreateIsolate and SetWorkerId are gone.

Reported in #512.
@adrian-niculescu
adrian-niculescu deleted the fix/memory-pressure-cache-race branch October 13, 2026 21:30
@adrian-niculescu

Copy link
Copy Markdown
Contributor Author

Closed in favor of #513, which fixes the same race differently: the isolate is added to the registry only after its Caches slot is written, so the window is gone instead of being locked around.

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