Six Days, $40,000 of Compute: How an AI-Assisted Chain Broke Chrome's Sandbox

·9 min read·Evergreen Tools Team

On October 13, 2026, XBOW announced that its platform had earned a $250,000 award from Google's Chrome Vulnerability Reward Program, described as the first full-chain Chrome award under Google's 2026 rules and the first granted to an autonomous offensive security platform. One human researcher and XBOW's AI agents built a complete Chrome exploit for Windows over six days, using OpenAI's GPT-5.6 Sol models, at an estimated $40,000 of compute. The chain crossed two sandbox boundaries using three previously unknown bugs, and the trigger was loading a web page. The detail worth keeping is not the number of bugs. It is how ordinary each bug looks on its own.

A close-up of a circuit board

Three separate bugs chained across two sandbox boundaries

What was claimed, and what was confirmed

XBOW says its platform and one security researcher built a full Chrome exploit for Windows over six days. The work chained three previously unknown bugs into a single attack that ran code outside Chrome's sandbox. XBOW reported every bug through Google's official program, Google's engineers reproduced and confirmed the exploit and shipped fixes, and public root-cause reports exist for all three bugs. XBOW states the vulnerabilities were never used against Chrome users and that it will spend the award on securing NGOs. The company estimates one billion people were running the affected Chrome on Windows before the fixes. The model involved is named as OpenAI's GPT-5.6 Sol, and the compute estimate is XBOW's own at roughly $40,000.

# Severity triage on single findings misses chains. Ask the graph instead.
from collections import deque

# Boundaries, from least to most privileged.
BOUNDARIES = ["renderer", "v8-heap-sandbox", "browser-process", "os-user"]

FINDINGS = [
    {"id": "F-1", "component": "v8", "from": "renderer", "to": "v8-heap-sandbox",
     "needs_user_action": False},
    {"id": "F-2", "component": "webp-decoder", "from": "v8-heap-sandbox", "to": "browser-process",
     "needs_user_action": False},
    {"id": "F-3", "component": "crashpad", "from": "browser-process", "to": "os-user",
     "needs_user_action": False},
    {"id": "F-4", "component": "settings-ui", "from": "renderer", "to": "renderer",
     "needs_user_action": True},
]

def edges(findings):
    graph = {}
    for f in findings:
        if f["from"] != f["to"]:
            graph.setdefault(f["from"], []).append(f)
    return graph

def chains_to(graph, target="os-user", start="renderer"):
    queue, seen = deque([(start, [])]), set()
    while queue:
        node, path = queue.popleft()
        for edge in graph.get(node, []):
            if edge["id"] in {p["id"] for p in path}:
                continue
            new_path = path + [edge]
            if edge["to"] == target:
                yield new_path
            elif (edge["to"], len(new_path)) not in seen:
                seen.add((edge["to"], len(new_path)))
                queue.append((edge["to"], new_path))

for chain in chains_to(edges(FINDINGS)):
    print("chain:", " -> ".join(f["id"] for f in chain))

# Three findings that would each be triaged on their own compose into one
# full-chain path from page load to code running outside the sandbox.

The three stages of the chain

Stage one was a bug in V8, Chrome's JavaScript engine, which gave the attacker control of memory inside the engine's own sandbox. Stage two was a bug in Chrome's animated WebP image decoder, tracked as CVE-2026-85045, which broke out of that sandbox and produced a single precise write to native memory. Stage three was a bug in Crashpad, Chrome's crash handler, tracked as CVE-2026-78952, which runs outside the sandbox and turned that write into full code execution. The result, in XBOW's words, is a program launched outside Chrome's sandbox, triggered by nothing more than loading a web page, with no download and no click. Each stage is a different class of mistake in a different component, owned by a different team in most organisations.

# Make the boundary model explicit, then refuse findings that do not fit it.
import json

BOUNDARY_MODEL = {
    "renderer": {"hosts_untrusted_content": True, "privilege": 1},
    "v8-heap-sandbox": {"hosts_untrusted_content": True, "privilege": 1},
    "gpu": {"hosts_untrusted_content": True, "privilege": 2},
    "network-service": {"hosts_untrusted_content": True, "privilege": 1},
    "browser-process": {"hosts_untrusted_content": False, "privilege": 3},
    "os-user": {"hosts_untrusted_content": False, "privilege": 4},
}

def validate(finding, model):
    problems = []
    for side in ("from", "to"):
        if finding[side] not in model:
            problems.append("unknown boundary: " + finding[side])
    if not problems:
        privileges = (model[finding["from"]]["privilege"], model[finding["to"]]["privilege"])
        finding["privilege_gain"] = privileges[1] - privileges[0]
    return problems

finding = {"id": "F-2", "component": "webp-decoder",
           "from": "v8-heap-sandbox", "to": "browser-process"}
print(validate(finding, BOUNDARY_MODEL), finding.get("privilege_gain"))

# If your triage tooling cannot say which boundary a finding crosses,
# every finding looks like a bug in a parser and priority becomes a coin flip.
Streams of security code on a screen

The trigger was loading a page, with no download and no click

Why chaining is what breaks severity scoring

The three bugs are individually the kind of finding that gets triaged down. A memory corruption bug inside a JavaScript engine's sandbox, a decoder that produces one out-of-bounds write, a crash handler with a memory-safety issue: each is real, and each is a long way from remote code execution on its own. Chained, they are a full escape. XBOW's chief executive Oege de Moor makes the point directly, saying that separate and perhaps even low-severity vulnerabilities can now be chained into a high-severity exploit path, and that a chain which previously took months took an estimated $40,000 of compute and six days. That is a statement about triage economics, not about any single CVE. If your pipeline scores findings one at a time, it is structurally blind to the thing that made this newsworthy.

# Score by reachable privilege and user interaction, not by CVSS alone.
def priority(finding, model):
    score = 0
    score += 4 * finding.get("privilege_gain", 0)
    if model[finding["from"]]["hosts_untrusted_content"]:
        score += 3          # reachable from content, not from a local attacker
    if not finding["needs_user_action"]:
        score += 3          # no click required
    if finding.get("runs_outside_sandbox"):
        score += 2
    if finding.get("component") in {"v8", "webp-decoder", "crashpad", "ipc"}:
        score += 1          # known chain ingredient
    return score

FINDINGS = [
    {"id": "F-1", "component": "v8", "privilege_gain": 0, "needs_user_action": False,
     "runs_outside_sandbox": False},
    {"id": "F-2", "component": "webp-decoder", "privilege_gain": 2, "needs_user_action": False,
     "runs_outside_sandbox": False},
    {"id": "F-3", "component": "crashpad", "privilege_gain": 1, "needs_user_action": False,
     "runs_outside_sandbox": True},
    {"id": "F-4", "component": "settings-ui", "privilege_gain": 0, "needs_user_action": True,
     "runs_outside_sandbox": False},
]

for f in sorted(FINDINGS, key=lambda f: -priority(f, BOUNDARY_MODEL)):
    print(priority(f, BOUNDARY_MODEL), f["id"], f["component"])

# The same three lines that look unremarkable alone come out on top together.

The defender's version of the same arithmetic

The defensive reading follows the same shape. If attackers can compose small bugs quickly and cheaply, then the defender's job is not to find the exploitable bug, it is to find the exploitable path among many findings, and to do it at a similar speed. That points to three concrete changes. Model your boundaries explicitly, so every finding can state which boundary it crosses and in which direction. Score by reachable privilege and required user interaction, not by CVSS in isolation. And treat components that take untrusted input, plus anything that runs outside a sandbox, as their own patch lane with a much shorter SLA, because those are the links a chain needs. None of this is new advice. What is new is the cost of ignoring it.

# Components that take untrusted input need a shorter SLA than UI code.
import json
from datetime import date, timedelta

SLA = {
    "untrusted-input-parsers": 7,
    "sandbox-helpers": 7,
    "crash-handling": 14,
    "ui": 90,
}

COMPONENT_CLASS = {
    "v8": "untrusted-input-parsers",
    "webp-decoder": "untrusted-input-parsers",
    "pdfium": "untrusted-input-parsers",
    "crashpad": "crash-handling",
    "settings-ui": "ui",
}

def due_dates(findings, disclosure=date(2026, 10, 13)):
    plan = []
    for f in findings:
        klass = COMPONENT_CLASS.get(f["component"], "ui")
        plan.append({"id": f["id"], "component": f["component"],
                     "class": klass, "due": str(disclosure + timedelta(days=SLA[klass]))})
    return plan

print(json.dumps(due_dates([{"id": "F-2", "component": "webp-decoder"},
                            {"id": "F-3", "component": "crashpad"}]), indent=2))

# The decoder and the crash handler are the two links that make the chain work.
# Neither is user-facing, and neither is usually on the fast lane.
A padlock and encryption concept

A sandbox does not remove bugs; it makes them harder to combine

What is worth adopting this week

Five things fit in a week. Draw the boundary model for the browsers, office suites and agent runtimes you ship, and write it down as data rather than a diagram. Add a field to every finding that says which boundary it crosses. Sort your queue by privilege gain first. Put untrusted-input parsers, sandbox helpers and crash handlers on a seven-day SLA, and say so in the same document as the ninety-day UI SLA, so the difference is visible to whoever argues about priorities. Finally, verify that your users are actually on a patched version, because a fixed bug in the source does not help anyone still running last quarter's build, and that gap is measurable with a user-agent breakdown and a version floor.

# The economics are the argument. Compute the attacker's cost, then yours.
CHAIN = {
    "bugs_required": 3,
    "days": 6,
    "compute_usd": 40000,
    "researcher_days": 6,
    "affected_users": 1_000_000_000,
}

BOUNTY = {"full_chain": 250000, "single_rce": 45000, "sandbox_escape": 25000}

def attacker_model(chain, attempts_per_bug=8):
    # What it costs to find a chain if each bug needs several attempts.
    compute = chain["compute_usd"] * attempts_per_bug
    return {
        "compute_usd": compute,
        "per_user_fraction_of_a_cent": round(compute / chain["affected_users"] * 100, 2),
        "cheaper_than_full_chain_bounty": compute < BOUNTY["full_chain"],
    }

print(attacker_model(CHAIN))

# That is the whole argument for automating triage. When a high-severity path
# costs less to construct than the bounty for reporting it, defenders cannot
# keep ranking findings one at a time, by hand, once a month.

What is not proven

This is a vendor announcement, and the vendor is arguing that autonomous offence is the best defence, which is a position it profits from. The compute figure is XBOW's own estimate. Google confirmed and fixed the bugs, but not the framing, the timeline or the cost. The $250,000 reward is meaningful, and it is also program-defined and specific to a full chain under the 2026 rules, so it is not a general price for an exploit. Read the event as a signal about pace rather than as a benchmark, and take the structural lesson, that small bugs compose, rather than the marketing one.

📌 Frequently Asked Questions

What did XBOW announce?

That its platform earned a $250,000 award from Google's Chrome Vulnerability Reward Program for a full-chain Chrome exploit, described as the first full-chain award under Google's 2026 rules and the first granted to an autonomous offensive security platform. One researcher worked with XBOW's AI agents for six days.

Which vulnerabilities made up the chain?

A bug in V8, Chrome's JavaScript engine, which gave memory control inside the engine's sandbox; CVE-2026-85045 in Chrome's animated WebP image decoder, which broke out of that sandbox; and CVE-2026-78952 in Crashpad, the crash handler that runs outside the sandbox, which turned the result into full code execution.

Did users need to click or download anything?

No. According to XBOW, the chain was triggered by loading a web page, with no user download or click required. Before the fixes, the affected class of flaw sat in the path of an estimated one billion people running Chrome on Windows.

Is the exploit in circulation?

XBOW says it reported every bug through Google's official program, that Google reproduced and confirmed the exploit and shipped fixes, and that the vulnerabilities were never used against Chrome users. Public root-cause reports for all three bugs are available.

Why do chains matter for vulnerability triage?

Because individually low-severity findings can compose into a full escape, and single-finding triage cannot see that composition. Rank findings by the privilege boundary they cross and whether user interaction is required, and give untrusted-input parsers and sandbox helpers shorter patch deadlines.