Skip to content

Browser crash (CHECK failure) when the refreshed New Tab Page loads in a DevTools-created isolated browser context #60146

Open
brave/brave-core
#40971

Description

@katsar0v

Description

The whole browser process crashes (deliberate CHECK failure, SIGTRAP) when the refreshed New Tab Page (brave_new_tab_page_refresh) loads in an off-the-record browser context that is not a regular Private window, such as a context created over the DevTools protocol with Target.createBrowserContext.

This hits anyone whose Brave is driven by automation that uses isolated contexts, for example Puppeteer's browser.createBrowserContext() or the Chrome DevTools MCP new_page tool with isolatedContext (used by AI coding agents). The crash takes down the user's main browser with all its windows. I hit it 17 times between Sep 28 and Oct 25, across 1.94.121 and 1.97.59.

Regular Private windows are not affected. They get the Private New Tab Page.

Steps to reproduce

  1. Start Brave with remote debugging and a fresh profile:
    brave-browser --user-data-dir=$(mktemp -d) --remote-debugging-port=9333 --no-first-run about:blank
    (I verified the clean-profile repro with --headless=new; my 17 real-world crashes were in normal windowed sessions and have the same crash PC.)
  2. Over CDP, create an isolated browser context and open the New Tab Page in it:
    Target.createBrowserContext → Target.createTarget { url: "chrome://newtab/", browserContextId }
  3. The browser process dies within about a second.

Minimal Node 22+ script (no dependencies):

const { webSocketDebuggerUrl } = await (await fetch("http://127.0.0.1:9333/json/version")).json();
const ws = new WebSocket(webSocketDebuggerUrl);
await new Promise((r) => (ws.onopen = r));
let id = 0;
const send = (method, params = {}) => new Promise((r) => {
  const myId = ++id;
  ws.addEventListener("message", function h(e) {
    const m = JSON.parse(e.data);
    if (m.id === myId) { ws.removeEventListener("message", h); r(m.result); }
  });
  ws.send(JSON.stringify({ id: myId, method, params }));
});
const { browserContextId } = await send("Target.createBrowserContext");
await send("Target.createTarget", { url: "chrome://newtab/", browserContextId });
ws.onclose = () => console.log("browser crashed");

Control: the same Target.createTarget call without browserContextId (default profile) loads the New Tab Page normally.

Actual result

The browser process terminates with SIGTRAP (Trace/breakpoint trap (core dumped)), and every window closes.

Expected result

The New Tab Page loads in the isolated context, or the Private New Tab Page is shown as in regular Private windows, and the browser keeps running.

Reproduces how often

Easily reproduced

Brave version (brave://version info)

Brave 1.97.59, Chromium 155.0.8059.85
Revision: c7357bfd2a76c2467ba1ae62593a81ae92c05205
OS: Linux, Ubuntu 24.04.5 LTS (x86_64), GNOME 46 on Wayland

Channel information

  • release (stable)
  • beta
  • nightly

Reproducibility

  • with Brave Shields disabled
  • with Brave Rewards disabled
  • in the latest version of Chrome

Miscellaneous information

Crash analysis (from local Crashpad minidumps and disassembly of the shipped 1.97.59 binary):

  • ptype=browser, crashing thread is the UI thread, signal 5 (int3; ud2 = official-build CHECK).
  • Crash PC is the same in every dump of a given build: brave+0xb861553 on 1.97.59 (3 of 3 dumps) and brave+0xaec3e9b on 1.94.121 (14 of 14). A clean-profile repro on 1.97.59 run under gdb stopped at the same brave+0xb861553.
  • Call path (frames identified from strings referenced by each function):
    blink.mojom.BrowserInterfaceBroker → PerWebUIBrowserInterfaceBroker (per_web_ui_browser_interface_broker.cc) → binder for brave_new_tab_page_refresh.mojom.NewTabPageHandler → constructor of a helper that observes ntp.custom_links_visible / ntp.shortcust_visible. That constructor CHECKs its first pointer argument, which is null.
  • That argument comes from a getter that appears to return nullptr when Profile::IsOffTheRecord() is true. A DevTools-created context is off the record but not the primary incognito profile, so it seems to get the regular (refresh) NTP instead of the private one, and its handler is built without that dependency.

Workaround (verified): brave://flags/#brave-ntp-refresh-enabled → Disabled (--disable-features=BraveNewTabPageRefreshEnabled). With it disabled, the same repro loads the New Tab Page in isolated contexts without crashing.

Possibly related: #56326 (browser-process CHECK crash from New Tab Page shortcut tiles on macOS). Its crash keys also include tabdrag-event-source=mouse, which every one of my real-world dumps has too. I don't think tab dragging is the trigger: the clean-profile repro above involves no tab drag.

Local minidumps are available on request.

Activity

  1. geniussingare commented on Oct 25, 2026

    @geniussingare

    I looked at the brave-core code path for this and I think the cause is a gap in how the New Tab Page variant is chosen. I haven't built or reproduced this locally, so please correct me where I'm wrong.

    Where it goes wrong

    • BravePrivateNewTabUIConfig::IsWebUIEnabled() (browser/ui/webui/private_new_tab_page/brave_private_new_tab_ui.cc) returns true only for IsIncognitoProfile() || IsTor() || IsGuestSession() || IsEnterpriseIsolatedModeProfile().
    • BraveWebUIControllerFactory (browser/ui/webui/brave_web_ui_controller_factory.cc, chrome::kChromeUINewTabHost branch) assumes private profiles never get there (it has a DCHECK for that) and returns BraveNewTabPageUI when kBraveNewTabPageRefreshEnabled is on.
    • A context from Target.createBrowserContext is off the record but apparently none of the four types above, so it gets the regular refreshed NTP.
    • BraveNewTabPageUI::BindInterface(NewTabPageHandler) then constructs TopSitesFacade, whose constructor starts with CHECK(most_visited_sites_) (brave_new_tab_page_refresh/top_sites_facade.cc:45). It also observes kNtpShortcutsVisible / kNtpCustomLinksVisible, which matches the reporter's crash analysis. I believe ChromeMostVisitedSitesFactory::NewForProfile() returns null for off-the-record profiles, but that's Chromium code and I haven't verified it.

    Possible fixes

    1. Root cause: make the private NTP config also cover these off-the-record contexts, and keep the DCHECK in the factory and IsPrivateNewTab() in brave_new_tab_message_handler.cc consistent. This gives the expected result in the report (Private NTP shown, no crash). One concern: a broad IsOffTheRecord() check must not catch the AI Chat agent profile, which the factory handles separately in the same branch.
    2. Defensive hardening: let TopSitesFacade handle a null most_visited_sites_ (empty list, no CHECK), or have BindInterface return early for profiles that don't support it.

    A browser test that opens chrome://newtab in an off-the-record, non-incognito profile would cover either fix.

    Maintainers: which direction do you prefer? I'm happy to work on a PR if this issue is free to pick up.

  2. self-assigned this
    on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

  • Status
    To do

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions