Skip to content

fix ObjectDisposedException in synchronous js binding#5260

Open
skillbert wants to merge 1 commit into
cefsharp:masterfrom
skillbert:master
Open

fix ObjectDisposedException in synchronous js binding#5260
skillbert wants to merge 1 commit into
cefsharp:masterfrom
skillbert:master

Conversation

@skillbert
Copy link
Copy Markdown

@skillbert skillbert commented Jun 1, 2026

Fixes:
#5232

Summary:
CEF 145 introduced several issues with synchronous js bindings. This PR addresses an issue with the WCF channel used for synchronous js bindings being broken after reloading the page.
It seems like the CEF update made it so that CefBrowser ids can still be reused when the underlying CefBrowser has to be recreated (A code comment states that this happens on cross-origin navigation, but it seems to happen at any navigation). When CEF recreates the CefBrowser like this it fires the OnBrowserCreated event before firing the corresponding OnBrowserDestroyed of the old browser with same id.

Changes:

  • Dispose the old browser in OnBrowserCreated if the given id already exists in _browserWrappers

How Has This Been Tested?
I didn't see a way to enable WCF in the unit tests, so it has been tested manually.

  • Run CefSharp.WinForms.Example
  • Go to button click JS binding demo page (in nav bar: script->listen for button click->go to demo page)
  • Inject button js code (navbar: script->listen for button click->inject javascript code)
  • Click the "Click me!" button, observe native popup
  • Reload the page by clicking "Go" in address bar
  • Inject js code again in the navbar
  • Click the "Click me!" button again. Previously this would fail due disposed WCF channel, it now works

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Updated documentation

Checklist:

  • Tested the code(if applicable)
  • Commented my code
  • Changed the documentation(if applicable)
  • New files have a license disclaimer
  • The formatting is consistent with the project (project supports .editorconfig)

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Enhanced browser lifecycle management when browsers are recreated with identical identifiers
    • Improved browser instance validation and cleanup to prevent resource leaks and ensure stable wrapper management

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves browser-wrapper lifecycle management in the CEF wrapper to handle cases where the browser engine reuses identifiers. It adds browser identity comparison, an updater callback mechanism, and guarded creation/destruction logic to clean up stale wrappers and prevent mismatched references.

Changes

Browser wrapper lifecycle improvements

Layer / File(s) Summary
Contracts and helper methods
CefSharp.BrowserSubprocess.Core/CefBrowserWrapper.h, CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h
IsSameBrowser method added to check if a wrapper's underlying browser matches another reference; friend declaration grants CefBrowserWrapperUpdater access to private wrapper map members.
Wrapper map updater callback mechanism
CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp
CefBrowserWrapperUpdater struct implements a callback that triggers the destruction event for old wrappers, deletes them, and returns the new wrapper when a duplicate browser ID is inserted into the map.
Browser creation and destruction integration
CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp
Browser creation switches from simple add to add-or-update using the updater callback; browser destruction becomes guarded, verifying the stored wrapper still matches the browser instance before removal and invoking cleanup handlers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • cefsharp/CefSharp#5229: Both PRs modify CefAppUnmanagedWrapper browser wrapper lifecycle in OnBrowserCreated/OnBrowserDestroyed to handle missing or stale wrapper cases.

Suggested reviewers

  • GoutamSarkarHyland
  • amaitland

Poem

🐰 A wrapper map now guards its state,
With callbacks fired before it's too late,
Old browsers fade when IDs recycle,
Fresh wrappers bloom—no more stale icicle,
CEF's dance with duplicates, now deftly in place! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: addressing ObjectDisposedException in synchronous JS binding after page reload, which is the core issue in the CEF 145 update.
Description check ✅ Passed The PR description is comprehensive and follows most template sections with clear explanations of the issue, changes, and manual testing performed.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp`:
- Around line 164-178: The removal can delete a different wrapper due to the
race between TryGetValue and TryRemove; change the logic to perform TryRemove
first and then verify the removed wrapper matches the expected browser before
invoking and deleting it. Specifically, use
_browserWrappers->TryRemove(browserId, wrapper) and then check wrapper !=
nullptr && wrapper->IsSameBrowser(browser) (or compare to currentValue if you
retained it) before calling _onBrowserDestroyed->Invoke(wrapper) and delete
wrapper; ensure you only delete the wrapper that you verified matches the
incoming CefRefPtr by using IsSameBrowser on the actually-removed wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6446284d-0a3b-481f-887e-5847315259aa

📥 Commits

Reviewing files that changed from the base of the PR and between f8ac2d9 and b260b35.

📒 Files selected for processing (3)
  • CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp
  • CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h
  • CefSharp.BrowserSubprocess.Core/CefBrowserWrapper.h

Comment on lines +164 to 178
int browserId = browser->GetIdentifier();
CefBrowserWrapper^ currentValue;
if (_browserWrappers->TryGetValue(browserId, currentValue)) {
// Check if another wrapper hasn't already taken our place
if (currentValue->IsSameBrowser(browser)) {
CefBrowserWrapper^ wrapper;
// This is technically a race condition since the entry for browser id might have changed between TryGetValue
// and TryRemove. No clear way to fix this with ConcurrentDictionary
if (_browserWrappers->TryRemove(browserId, wrapper))
{
_onBrowserDestroyed->Invoke(wrapper);
delete wrapper;
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Race condition can delete the wrong wrapper.

The code checks currentValue->IsSameBrowser(browser) but then TryRemove outputs to a different variable wrapper. If the dictionary entry changes between TryGetValue and TryRemove (as acknowledged in the comment), the deleted wrapper could differ from the checked currentValue.

While CEF render process callbacks are typically serialized on the render thread (making true races unlikely), the current structure doesn't guarantee correctness. Consider using a pattern that atomically checks and removes, or at minimum, re-verify identity after removal:

Suggested defensive check
-                    CefBrowserWrapper^ wrapper;
-                    // This is technically a race condition since the entry for browser id might have changed between TryGetValue
-                    // and TryRemove. No clear way to fix this with ConcurrentDictionary
-                    if (_browserWrappers->TryRemove(browserId, wrapper))
+                    CefBrowserWrapper^ removedWrapper;
+                    if (_browserWrappers->TryRemove(browserId, removedWrapper))
                     {
-                        _onBrowserDestroyed->Invoke(wrapper);
-                        delete wrapper;
+                        // Re-verify we removed the wrapper we intended to remove
+                        if (removedWrapper->IsSameBrowser(browser))
+                        {
+                            _onBrowserDestroyed->Invoke(removedWrapper);
+                            delete removedWrapper;
+                        }
+                        else
+                        {
+                            // Raced with AddOrUpdate; put the new wrapper back
+                            _browserWrappers->TryAdd(browserId, removedWrapper);
+                        }
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int browserId = browser->GetIdentifier();
CefBrowserWrapper^ currentValue;
if (_browserWrappers->TryGetValue(browserId, currentValue)) {
// Check if another wrapper hasn't already taken our place
if (currentValue->IsSameBrowser(browser)) {
CefBrowserWrapper^ wrapper;
// This is technically a race condition since the entry for browser id might have changed between TryGetValue
// and TryRemove. No clear way to fix this with ConcurrentDictionary
if (_browserWrappers->TryRemove(browserId, wrapper))
{
_onBrowserDestroyed->Invoke(wrapper);
delete wrapper;
}
}
}
int browserId = browser->GetIdentifier();
CefBrowserWrapper^ currentValue;
if (_browserWrappers->TryGetValue(browserId, currentValue)) {
// Check if another wrapper hasn't already taken our place
if (currentValue->IsSameBrowser(browser)) {
CefBrowserWrapper^ removedWrapper;
if (_browserWrappers->TryRemove(browserId, removedWrapper))
{
// Re-verify we removed the wrapper we intended to remove
if (removedWrapper->IsSameBrowser(browser))
{
_onBrowserDestroyed->Invoke(removedWrapper);
delete removedWrapper;
}
else
{
// Raced with AddOrUpdate; put the new wrapper back
_browserWrappers->TryAdd(browserId, removedWrapper);
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp` around lines 164
- 178, The removal can delete a different wrapper due to the race between
TryGetValue and TryRemove; change the logic to perform TryRemove first and then
verify the removed wrapper matches the expected browser before invoking and
deleting it. Specifically, use _browserWrappers->TryRemove(browserId, wrapper)
and then check wrapper != nullptr && wrapper->IsSameBrowser(browser) (or compare
to currentValue if you retained it) before calling
_onBrowserDestroyed->Invoke(wrapper) and delete wrapper; ensure you only delete
the wrapper that you verified matches the incoming CefRefPtr by using
IsSameBrowser on the actually-removed wrapper.

@AppVeyorBot
Copy link
Copy Markdown

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.

2 participants