Archive

Posts Tagged ‘coding’

Fixing Chrome’s “Aw, Snap!” STATUS_ACCESS_VIOLATION in CDP Automation

How a race condition between page navigation and JavaScript execution causes STATUS_ACCESS_VIOLATION crashes — and how to fix it properly.

C# · .NET·Puppeteer / CDP·Chrome Automation 💥

😬 Aw, Snap! Something went wrong displaying this page.

Chrome’s infamous crash page — and the Windows error behind it: STATUS_ACCESS_VIOLATION (0xC0000005). If you’re automating Chrome via CDP and seeing this, a race condition is likely the culprit.

If you’ve built a browser automation system using the Chrome DevTools Protocol — whether through Puppeteer, Playwright, or a custom CDP client — you may have encountered Chrome processes dying with a STATUS_ACCESS_VIOLATION error. The browser just vanishes. No clean exception, no useful log output. Just Chrome’s “Aw, Snap!” page, or worse, a completely dead process.

This error code (0xC0000005 on Windows) means the process attempted to read or write memory it doesn’t own. It’s a hard native crash, well below the level where .NET exception handling can help you. A try/catch around your CDP call won’t save you.

The Usual Suspects

Most writeups on this error point to GPU driver issues, sandbox misconfiguration, or DLL injection from antivirus software — and those are all valid causes. The standard advice is to throw flags like --disable-gpu, --no-sandbox, and --disable-dev-shm-usage at the problem until it goes away.

But there’s another cause that gets far less attention: injecting JavaScript into a page that’s mid-navigation. This is a timing issue, and it’s surprisingly easy to introduce.

The Race Condition

Consider a common automation pattern: click a button, wait for the resulting page to load, then execute some JavaScript on the new page. A naive implementation might look like this:

problematic// Click a button that triggers navigation
await ExecuteJavascript("document.querySelector('button').click();");

// Arbitrary delay, then poll readyState
await Task.Delay(1000);
while (true)
{
    await Task.Delay(500);
    var readyState = await ExecuteJavascript("document.readyState");
    if (readyState == "complete") break;
}

This pattern has a fundamental flaw. After clicking the button, Chrome begins tearing down the current document and loading a new one. There is a window — however brief — where the old document is gone but the new one isn’t yet attached. If ExecuteJavascript fires a CDP Runtime.evaluate command into that gap, Chrome is asked to execute JavaScript in a context that no longer exists.

The result isn’t a clean error. Chrome’s internal state becomes inconsistent, and the access violation follows.

Why does it only crash sometimes? Because the race condition is non-deterministic. On a fast machine or a fast network, the new page loads before the poll fires and everything works. On a slower day, the poll lands in the gap and Chrome crashes. This makes the bug look intermittent and hardware-dependent, when it’s actually a logic error.

What the Timeline Looks Like

Button click dispatched

CDP sends Runtime.evaluate with the click expression. Navigation begins.

⚠ Danger zone begins

Old document is being torn down. New document not yet attached to the frame.

document.readyState polled

If Runtime.evaluate fires here, Chrome has no valid document context to evaluate against.

STATUS_ACCESS_VIOLATION

Chrome dereferences a null or freed pointer. Process dies. “Aw, Snap!”

New document attached (if we were lucky)

If the poll happened to land here instead, readyState returns “loading” or “complete” and everything works fine.

The Fix: Let Chrome Tell You

The correct solution is to stop guessing when navigation is complete and instead subscribe to Chrome’s own navigation events. CDP exposes Page.loadEventFired precisely for this purpose — it fires when the new page’s load event has completed, meaning the document is fully attached and ready for JavaScript execution.

fixedprivate async Task WaitForPageLoad(ChromeSession session, int timeoutMs = 30000)
{
    var tcs = new TaskCompletionSource<bool>();

    session.Subscribe<LoadEventFiredEvent>(e => tcs.TrySetResult(true));

    var timeoutTask = Task.Delay(timeoutMs);
    var completed = await Task.WhenAny(tcs.Task, timeoutTask);

    if (completed == timeoutTask)
        throw new TimeoutException("Page load timed out");
}

Critically, the subscription must be set up before triggering navigation — not after. Otherwise there’s a small window where the event fires before you’re listening:

usage// Subscribe FIRST, then trigger navigation
var pageLoad = WaitForPageLoad(chromeSession);

await ExecuteJavascript("document.querySelector('button').click();");

// Now wait — Chrome will signal when it's actually ready
await pageLoad;

// Safe to execute JavaScript on the new page
await ExecuteJavascript("/* your code here */");

Why Not Just Catch the Exception?

A STATUS_ACCESS_VIOLATION is a native Windows exception originating inside the Chrome process itself. It is entirely outside the .NET runtime. Wrapping your CDP calls in try/catch does nothing — there is no managed exception to catch. The Chrome process simply dies.

Similarly, adding more Task.Delay calls doesn’t fix the race condition — it just makes it less likely to trigger on any given run, while leaving the underlying problem completely intact.

Applies to Puppeteer and Other CDP Clients Too

This issue isn’t specific to C# or any particular CDP library. The same race condition can occur in Node.js with Puppeteer, Python with pyppeteer, or any system that drives Chrome via the DevTools Protocol. Puppeteer’s page.waitForNavigation() and page.waitForLoadState() exist precisely to solve this problem — they’re wrappers around the same loadEventFired event.

If you’re rolling a custom CDP client in any language, the principle is the same: never rely on arbitrary delays or polling to determine when a page is ready for JavaScript execution. Subscribe to Page.loadEventFired or Page.frameStoppedLoading, and let Chrome do the signalling.


Summary

Root cause: JavaScript injected via CDP (Runtime.evaluate) during a page transition hits Chrome in an inconsistent internal state, causing a STATUS_ACCESS_VIOLATION native crash.

Why it’s intermittent: The race condition depends on timing — fast loads mask it, slow loads expose it.

The fix: Subscribe to Page.loadEventFiredbefore triggering navigation, and await the event before executing any JavaScript. Never use Task.Delay or document.readyState polling as a substitute for proper navigation events. Chrome DevTools Protocol · Browser Automation · STATUS_ACCESS_VIOLATION

Categories: Uncategorized Tags: , , ,