Where Pixels Go Wrong: Chrome’s Graphics Stack
Where Pixels Go Wrong: Chrome’s Graphics Stack
Drawing a triangle with WebGL comes down to one call, gl.drawArrays(gl.TRIANGLES, 0, 3). Drawing text on a canvas is one call too, ctx.fillText("Hello", 10, 50). Behind each call is a long stack of Chrome code: the page’s own objects, a message channel into a separate process, a set of checks, shader compilers, a translation layer for the operating system’s graphics API, and finally the GPU driver.
By September 22, 2026, Chrome had fixed 506 CVEs in that stack this year. That is more than the nine years before combined. Chrome as a whole jumped too: 2,908 CVEs so far in 2026, compared with 190 in all of 2025.

Figure 1. Chrome graphics CVEs per year, with graphics’ share of all Chrome CVEs under each year. 2026 runs through September 22.
More of the bugs attackers use are in graphics, too. From 2016 to 2022, 2 of the 45 Chrome bugs exploited in the wild were in graphics. Since 2023, 7 of 30 have been. V8 is still the largest group with 17, but graphics is now second.

Figure 2. Chrome CVEs marked as exploited in the wild per year, split into graphics, V8 and the rest of Chrome.
Our V8 survey looked at the bugs in Chrome’s JavaScript engine. This post does the same for its graphics stack. It follows a draw call from the page to the GPU driver, one layer at a time, and looks at what Chrome fixed at each one. We used every graphics CVE in chrome-cves since 2017, the commits that fixed them, and the nine that were exploited in the wild. Each of those nine gets a short case study.
TLDR: graphics bugs show up at every layer of the stack, and attackers use them more than they used to. The standout is uninitialized memory: 91 fixes in 2026, against 6 in the nine years before.
The path
Take the drawArrays call. The page runs in the renderer process, which isn’t allowed to talk to the GPU. So Blink, Chrome’s rendering engine, turns the call into a command and writes it into memory it shares with the GPU process. The GPU process reads the command, checks that the buffers and textures (images stored on the GPU) it names are valid for this draw, and uses the shaders the page supplied earlier. Shaders are small programs that run on the GPU, and Chrome compiled them for this GPU when the page handed them over. Finally the GPU process translates the work into the platform’s own graphics API, such as Metal on a Mac, and hands it to the driver.
That’s the path for WebGL, where the translating is done by ANGLE. WebGPU takes the same path through Dawn. Text, CSS and Canvas 2D take a shorter one, through Skia, which turns drawing commands into the same kind of GPU work.

Figure 3. The layers of Chrome’s graphics stack, with the number of 2026 CVEs whose fix changed each layer. Everything right of the dashed line runs in the GPU process. 21 compositing CVEs and 23 that we couldn’t place aren’t shown.
Figure 4 counts the 2026 CVEs at each layer, placed by the files their fix changed.

Figure 4. 2026 graphics CVEs by the layer their fix changed, including compositing and the CVEs we couldn’t place.
The backends are the largest layer, and the command buffer is second. Each section below covers one layer and the exploited bugs fixed there.
Layer 1: The page

The page owns JavaScript objects like WebGLRenderingContext, GPUDevice and CanvasRenderingContext2D. Behind each one is C++ code in Blink that keeps track of GPU resources for it. These objects are created and freed by the page and cleaned up by the garbage collector, so many bugs here are about object lifetimes.
CVE-2021-30554: a trace that skipped part of the object
Chrome filed this use-after-free under WebGL and fixed it in June 2021 [7]. The fix is in WebXR, the API for VR and AR headsets. A WebXR session draws with WebGL through a layer object, XRWebGLLayer. Blink’s garbage collector finds live objects by asking each one to report the objects it holds, through a Trace method. The base class of XR layers reported only part of what it held.
From the fix, in third_party/blink/renderer/modules/xr/xr_layer.cc:
From the fix, in third_party/blink/renderer/modules/:
xr/xr_layer.cc
XRLayer is an event target, so it holds things like event listeners. By calling the tracing method of a more basic class, it skipped them, and the garbage collector could free objects that were still in use.
Layer 2: Crossing into the GPU process

The renderer writes its commands into a shared memory buffer, the command buffer, and the GPU process reads, checks and runs them. It can’t trust anything in that buffer, because a compromised renderer can write whatever it likes there. WebGPU does the same through Dawn’s “wire”, which turns WebGPU calls into messages and runs them again in the GPU process. Chrome’s design documents name security as the first goal of the command buffer, because it is what stands between a compromised renderer and the GPU driver [8].
This boundary also decides how serious a bug is. The GPU process has its own sandbox on most platforms, but on Android and on Linux with X11 it runs without one [9]. Chrome’s severity guidelines rate memory corruption in the GPU process that a page can reach without first compromising the renderer as Critical where the GPU process has no sandbox, and High where it has one [10]. 13% of 2026 graphics CVEs are rated Critical, twice the share for Chrome as a whole (6.5%).

Figure 5. Chromium’s severity ratings for 2026 CVEs, graphics compared with all of Chrome.
The most common bug at this layer is uninitialized memory, 29% of its 103 CVEs in 2026. “Leftover pixels” below looks at those.
CVE-2022-4135: checked against the type, not the texture
This heap buffer overflow was fixed in November 2022 [11]. Textures have mip levels, smaller copies of the image at half the size, a quarter, and so on. The command decoder is the part of the GPU process that reads each command and checks it. For texture commands, it asked whether the mip level was valid for the texture’s type, for example any 2D texture. It didn’t ask whether this particular texture had that many levels. The fix adds a check against the texture itself and uses it throughout the decoder.
Added by the fix, in gpu/command_buffer/service/texture_manager.cc:
Added by the fix, in gpu/command_buffer/service/:
texture_manager.cc
MaxValidMipLevel() is the size of the texture’s own list of levels. Checking against the type’s limit instead let a level index go past the end of that list.
CVE-2026-5281: a callback that outlived its device
This one is a use-after-free in Dawn, fixed in March 2026 [12]. When the page releases a WebGPU device, the wire server in the GPU process frees its record of that device. Devices can call back into the wire later, for example to report an error. The server cleared some of those callbacks before freeing the record, but not all of them. The fix swaps one call for another.
From the fix, in generator/templates/dawn/wire/server/ServerDoers.cpp (comments left out):
From the fix, in generator/templates/dawn/wire/server/ (comments left out):
ServerDoers.cpp
The old call cleared only the logging callback. Destroying the device also clears the error callback before the record is freed, so no callback is left pointing at freed memory.
Layer 3: Validation

Before a draw runs, the GPU process has to check that everything it will touch is valid for this call. The buffers must be big enough, the offsets in range and the formats matching, and no resource can be used in two conflicting ways at once. In our map, this layer is the front ends of ANGLE and Dawn, the code that tracks state and validates calls before any backend sees them.
Figure 6 splits each layer by bug type.

Figure 6. The kinds of bugs fixed at each layer in 2026, from Chrome’s one-line descriptions. The numbers are counts of CVEs.
CVE-2025-6558: writing to a buffer the GPU is still using
Chrome called this “incorrect validation of untrusted input” in ANGLE and fixed it in July 2025 [13]. Transform feedback lets a shader write its output into buffers. WebGL forbids using a buffer for transform feedback and for something else at the same time. ANGLE already checked one form of that rule. The fix adds another. While transform feedback is active and not paused, a buffer bound to it can’t have new data written into it through bufferData or bufferSubData.
Added by the fix, in src/libANGLE/validationES2.cpp, in both ValidateBufferData and ValidateBufferSubData:
Without this check, a page could change a buffer’s storage while the GPU was still writing into it.
Layer 4: Shader compilers

For WebGL and WebGPU, the page writes its shaders itself, in GLSL or WGSL. The GPU can’t run them as written, so Chrome compiles them. ANGLE’s shader translator handles GLSL, and Tint, part of Dawn, handles WGSL. On Windows, WebGPU also passes shaders through Microsoft’s DirectX Shader Compiler (DXC).
Skia has its own shader language, SkSL, but a page never writes it. Skia writes SkSL itself, from what the page draws: a gradient, a blur or a blend mode each adds its own piece of shader code. The page can’t write the shader, but it decides which pieces go into it.
47 CVEs in 2026 were fixed at this layer, and more than half of them (55%) are out-of-bounds bugs.
This layer also has bugs that weren’t in Chrome’s own code. In 2024, 14 of the 24 Dawn CVEs were fixed by moving Dawn to a patched version of DXC [14]. Those were bugs in Microsoft’s compiler, reachable from any web page through WebGPU on Windows.
CVE-2023-2136: parameters left out of the count
Chrome described this as an integer overflow in Skia and fixed it in April 2023 [15]. SkSL limits how much stack space a function’s variables can use. The compiler counted every local variable against that limit, but not the function’s parameters, so a function with very large parameters could go past the limit unchecked. The fix is one loop.
Added by the fix, in src/sksl/ir/SkSLFunctionDefinition.cpp, in the Finalizer constructor:
Added by the fix, in src/sksl/ir/SkSLFunctionDefinition.cpp,
in the Finalizer constructor:
The stack-size check now sees everything that goes on the stack, parameters included.
Layer 5: Backends and drivers

After validation and compilation, ANGLE and Dawn turn the work into calls to the platform’s graphics API. That’s Direct3D on Windows, Metal on macOS, Vulkan on Android and Linux, and OpenGL where needed. This code is different on every platform, so a bug here may affect only some users. It is also the last Chrome code before the GPU driver, and it includes Chrome’s list of workarounds for specific drivers. It’s the largest layer, and out-of-bounds bugs are its most common kind.
Code here is also shared outside Chrome. ANGLE is the WebGL backend for Chrome and for Firefox on Windows [16], and Safari’s WebGL runs on ANGLE’s Metal backend [17]. Both exploited bugs at this layer were fixed by Apple as well. One of them, CVE-2025-24201, has no public Chrome fix. Apple fixed it in WebKit as an out-of-bounds write that could let web content “break out of Web Content sandbox”, and called it a supplementary fix for an attack blocked in iOS 17.2 [18]. We placed it at this layer only because Chrome labeled it “GPU on Mac” [19].
CVE-2025-14174: a buffer sized for the wrong image
This out-of-bounds access in ANGLE was fixed in December 2025 [20]. When a page uploads depth data to a texture, ANGLE’s Metal backend first copies it into a temporary buffer. It sized that buffer with the size of one full image in the source data (pixelsDepthPitch), which the page can influence through its pixel storage settings (gl.pixelStorei), instead of the size of the area being copied.
From the fix, in src/libANGLE/renderer/metal/TextureMtl.mm:
From the fix, in src/libANGLE/renderer/metal/:
TextureMtl.mm
When the source image was shorter than the texture area, the buffer was too small for the copy that followed.
Apple credits itself and Google’s Threat Analysis Group for this bug, and fixed it in WebKit two days after Chrome [21].
Layer 6: Skia

Most of what a page draws isn’t WebGL at all. Text, CSS and Canvas 2D go through Skia, which records drawing operations, merges them into batches and turns them into GPU work. It saves work wherever it can, by combining draws and caching results. For years Skia’s GPU backend was Ganesh. Chrome has been moving to a newer one, Graphite, first on Apple Silicon Macs [22].
CVE-2023-6345: an addition that overflowed
This is another integer overflow in Skia, fixed in November 2023 [23]. To save work, Ganesh combines several mesh draws into one when it can. Before combining two meshes, it checked that their total vertex count fit in the 16-bit numbers it uses to refer to vertices. It computed that total by adding the two counts first, and the addition itself could overflow.
From the fix, in src/gpu/ganesh/ops/DrawMeshOp.cpp:
Before the fix, a total that overflowed could pass the check, so two meshes too large to combine were combined anyway. Comparing against limit - b can’t overflow.
Zero Day Engineering published an analysis of this patch when the bug was disclosed, including what it meant on different platforms [24].
CVE-2026-3909: a cache key without the format
This out-of-bounds write in Skia was fixed in March 2026 [25]. A glyph is the drawn shape of a character. Skia draws text from glyph atlases, large textures that hold many rendered glyphs. There are separate atlases for different glyph formats, for example plain coverage masks and full-color glyphs. The cache that maps a glyph to its place in an atlas used only the glyph’s ID as the key, so the same glyph asked for in two formats could get an entry that pointed into the wrong atlas. The fix adds the format to the key, in both Ganesh and Graphite.
Added by the fix, in src/gpu/ganesh/text/GlyphData.h:
A cache key that leaves out part of what makes an entry unique hands back the wrong memory.

Figure 7. Before the fix, one cache entry served a glyph in both formats, so a request for the color version could be sent into the mask atlas. After it, each format has its own entry. The glyph ID and atlas slots are made up.
Leftover pixels
One kind of bug stands out in graphics. When the GPU allocates a new texture or buffer, the memory isn’t empty. It holds whatever was there before, possibly from another tab or application. This is a known risk. The WebGL specification requires that resources always contain initialized data [2], and Chrome’s command buffer design says every texture must be cleared before use so a page can’t read leftover video memory [8].
From 2017 to 2025, Chrome described 6 graphics CVEs as uninitialized memory. In 2026 it described 91 that way, with 34 in ANGLE, 25 in the GPU process, 15 in Skia, 12 in Dawn, 4 in WebGL and 1 in Canvas. That’s 18% of graphics CVEs this year. In V8 it’s 3%, or 4 CVEs (see our V8 survey).
Clearing every new resource right away would be slow, so Chrome clears a resource only when it’s first used, and marks it as cleared. From then on, that mark is what keeps the old contents away from the page.

Figure 8. The top row is how clearing is meant to work. The bottom row is the bug, where the mark says “cleared” but the memory still holds old contents and the page can read it. The counts are how many 2026 fixes went wrong each way.
We read the fix for each of the 91. In 65, GPU memory could reach the page without being cleared or written, and Figure 8 shows the four ways that happened. The other 26 are ordinary uninitialized variables, or fixes that don’t show enough to tell.
One example is CVE-2026-11109, fixed in ANGLE’s OpenGL backend [26]. WebGL 2 lets a page turn on “rasterizer discard”, which tells the GPU to skip drawing pixels. ANGLE clears new resources by drawing into them, so with rasterizer discard on, the clear itself was skipped.
Added by the fix, in src/libANGLE/renderer/gl/BlitGL.cpp:
With discard forced off, the clear really writes the memory it marks as cleared.
The same mistake was fixed in four other places in 2026, all in Chrome’s own command decoder. None of the nine graphics CVEs marked as exploited in the wild is described as uninitialized memory.
Nine exploited bugs, all along the path

Figure 9. The nine graphics CVEs Chrome marked as exploited in the wild, placed by the layer their fix changed. CVE-2025-24201 (marked with an asterisk) has no public Chrome fix, so it is placed by its Chrome label, “GPU on Mac”.
Eight of the nine have public Chrome fixes, and those fixes span all six layers.
| CVE | Fixed in Chrome | Component | Layer | What the Fix Changed |
|---|---|---|---|---|
| CVE-2021-30554 | June 2021 | WebGL | the page | XR layers report everything they hold to the garbage collector |
| CVE-2022-4135 | November 2022 | GPU | command buffer | Mip levels are checked against the texture, not its type |
| CVE-2026-5281 | March 2026 | Dawn | command buffer | Releasing a WebGPU device destroys it, which clears every callback |
| CVE-2025-6558 | July 2025 | ANGLE and GPU | validation | A buffer in use by transform feedback can’t get new data |
| CVE-2023-2136 | April 2023 | Skia | shader compilers | Function parameters count toward SkSL’s stack limit |
| CVE-2025-14174 | December 2025 | ANGLE | backends | Metal’s temporary buffer is sized for the area copied |
| CVE-2025-24201 | March 2025 | GPU on Mac | backends (by label) | No public Chrome fix |
| CVE-2023-6345 | November 2023 | Skia | Skia | Mesh vertex counts are checked without overflowing |
| CVE-2026-3909 | March 2026 | Skia | Skia | The glyph cache key includes the format |
Recap
Graphics bugs turn up at every layer of the stack, and so do the ones attackers use. Chrome’s labels are only a rough guide, so look at where each fix lands. The standout in 2026 is leftover memory: in 65 fixes, GPU memory could reach a page before it was cleared, so any code that marks memory as cleared is worth a close look. The case studies come down to a few small mistakes: an object freed while still in use, a size checked against the wrong limit, or a check that skips part of its input. And since Safari and Firefox on Windows also use ANGLE, a bug there can reach more than one browser.
About the data
We used chrome-cves [1] and v8-cves [27], both built from Chrome Releases [28], through September 22, 2026. A CVE counts as graphics if Chrome filed it under ANGLE, GPU, Dawn or WebGPU, Skia, WebGL, Compositing, Canvas or SwiftShader, which gives 710 since 2017. We placed each one on a layer by the files its fix changed, ignoring tests, and sorted bug types from Chrome’s one-line descriptions. The 91 uninitialized-memory fixes we sorted by hand, against their full diffs. A fix shows where a bug was fixed, which is not always where it started, and 2026 isn’t over yet.
References
- chrome-cves. https://github.com/str8outtaheap/chrome-cves
- WebGL 1.0 specification. https://registry.khronos.org/webgl/specs/latest/1.0/
- ANGLE. https://chromium.googlesource.com/angle/angle/
- WebGPU specification. https://www.w3.org/TR/webgpu/
- Dawn. https://dawn.googlesource.com/dawn
- Skia. https://skia.org/
- CVE-2021-30554 fix: “Ensure that XRLayer includes base EventTarget in Trace.” https://chromium.googlesource.com/chromium/src/+/01b6f7e0a70648d7c7302454993f0bf86d5a0241 Chrome 91.0.4472.114, June 17, 2021. https://chromereleases.googleblog.com/2021/06/stable-channel-update-for-desktop_17.html
- Chromium, “GPU Command Buffer” design document. https://www.chromium.org/developers/design-documents/gpu-command-buffer/
- Chromium, “Process Sandboxes by Platform.” https://chromium.googlesource.com/chromium/src/+/main/docs/security/process-sandboxes-by-platform.md
- Chromium, “Severity Guidelines for Security Issues.” https://chromium.googlesource.com/chromium/src/+/main/docs/security/severity-guidelines.md
- CVE-2022-4135 fix: “Fix potential OOB problem with validating command decoder.” https://chromium.googlesource.com/chromium/src/+/6b4af5d8208398839e90c7adc2da22c0288d6b47 Chrome 107.0.5304.121, November 24, 2022. https://chromereleases.googleblog.com/2022/11/stable-channel-update-for-desktop_24.html
- CVE-2026-5281 fix: “[dawn][wire] Ensure that Devices on the Server always call Destroy.” https://dawn.googlesource.com/dawn/+/adcf333bd486a7f0a1c5c4a52c8ea5ae54e15c32 Chrome 146.0.7680.177, March 31, 2026. https://chromereleases.googleblog.com/2026/03/stable-channel-update-for-desktop_31.html
- CVE-2025-6558 fix: “Validate buffers bound for transform feedback are not modified.” https://chromium.googlesource.com/angle/angle/+/2f8193ecfe1ed464374ae56235cfdc112343f9c3 Chrome 138.0.7204.157, July 15, 2025. https://chromereleases.googleblog.com/2025/07/stable-channel-update-for-desktop_15.html
- Example DXC update in Dawn (CVE-2024-2885): “DEPS: Update DXC to patched branch.” https://dawn.googlesource.com/dawn/+/d654129e99a74c339a1aadce340d6aa62d3ded51
- CVE-2023-2136 fix: “Enforce program stack limits on function parameters.” https://skia.googlesource.com/skia/+/4dc748f14c6650cb45c7086a39af1760bfda41d2 Chrome 112.0.5615.137, April 18, 2023. https://chromereleases.googleblog.com/2023/04/stable-channel-update-for-desktop_18.html
- ANGLE README. https://chromium.googlesource.com/angle/angle/+/main/README.md
- WebKit bug 220076, “Enable Metal ANGLE backend for WebGL.” https://bugs.webkit.org/show_bug.cgi?id=220076
- Apple, “About the security content of iOS 18.3.2 and iPadOS 18.3.2.” https://support.apple.com/en-us/122281
- Chrome 134.0.6998.88, March 10, 2025. https://chromereleases.googleblog.com/2025/03/stable-channel-update-for-desktop_10.html
- CVE-2025-14174 fix: “Metal: Don’t use pixelsDepthPitch to size buffers.” https://chromium.googlesource.com/angle/angle/+/95a32cb37edbb90eac0b83727b38fedbbb32307b Chrome 143.0.7499.109, December 10, 2025. https://chromereleases.googleblog.com/2025/12/stable-channel-update-for-desktop_10.html
- Apple, “About the security content of Safari 26.2.” https://support.apple.com/en-us/125892
- Chromium blog, “Introducing Skia Graphite: Chrome’s rasterization backend for the future,” July 8, 2025. https://blog.google/chromium/introducing-skia-graphite-chromes/
- CVE-2023-6345 fix: “Avoid combining extremely large meshes.” https://skia.googlesource.com/skia/+/6169a1fabae1743709bc9641ad43fcbb6a4f62e1 Chrome 119.0.6045.199, November 28, 2023. https://chromereleases.googleblog.com/2023/11/stable-channel-update-for-desktop_28.html
- Zero Day Engineering, “Google Chrome Skia Vulnerability Insights (CVE-2023-6345),” November 30, 2023. https://zerodayengineering.com/insights/chrome-skia-cve-2023-6345.html
- CVE-2026-3909 fix: “Make sure we are getting the correct atlas for glyph mask format.” https://skia.googlesource.com/skia/+/0cab3e4ee34b3bca6ba7df676639d73ffe4b2135 Chrome 146.0.7680.75, March 12, 2026. https://chromereleases.googleblog.com/2026/03/stable-channel-update-for-desktop_12.html
- CVE-2026-11109 fix: “GL: Disable rasterizer discard for robust resource init.” https://chromium.googlesource.com/angle/angle/+/ef5a8a5275da668cf381adc8f87ee17767be4573
- v8-cves. https://github.com/str8outtaheap/v8-cves
- Chrome Releases. https://chromereleases.googleblog.com/
Where Pixels Go Wrong: Chrome’s Graphics Stack
Where Pixels Go Wrong: Chrome’s Graphics Stack
Drawing a triangle with WebGL comes down to one call, gl.drawArrays(gl.TRIANGLES, 0, 3). Drawing text on a canvas is one call too, ctx.fillText("Hello", 10, 50). Behind each call is a long stack of Chrome code: the page’s own objects, a message channel into a separate process, a set of checks, shader compilers, a translation layer for the operating system’s graphics API, and finally the GPU driver.
By September 22, 2026, Chrome had fixed 506 CVEs in that stack this year. That is more than the nine years before combined. Chrome as a whole jumped too: 2,908 CVEs so far in 2026, compared with 190 in all of 2025.

Figure 1. Chrome graphics CVEs per year, with graphics’ share of all Chrome CVEs under each year. 2026 runs through September 22.
More of the bugs attackers use are in graphics, too. From 2016 to 2022, 2 of the 45 Chrome bugs exploited in the wild were in graphics. Since 2023, 7 of 30 have been. V8 is still the largest group with 17, but graphics is now second.

Figure 2. Chrome CVEs marked as exploited in the wild per year, split into graphics, V8 and the rest of Chrome.
Our V8 survey looked at the bugs in Chrome’s JavaScript engine. This post does the same for its graphics stack. It follows a draw call from the page to the GPU driver, one layer at a time, and looks at what Chrome fixed at each one. We used every graphics CVE in chrome-cves since 2017, the commits that fixed them, and the nine that were exploited in the wild. Each of those nine gets a short case study.
TLDR: graphics bugs show up at every layer of the stack, and attackers use them more than they used to. The standout is uninitialized memory: 91 fixes in 2026, against 6 in the nine years before.
The path
Take the drawArrays call. The page runs in the renderer process, which isn’t allowed to talk to the GPU. So Blink, Chrome’s rendering engine, turns the call into a command and writes it into memory it shares with the GPU process. The GPU process reads the command, checks that the buffers and textures (images stored on the GPU) it names are valid for this draw, and uses the shaders the page supplied earlier. Shaders are small programs that run on the GPU, and Chrome compiled them for this GPU when the page handed them over. Finally the GPU process translates the work into the platform’s own graphics API, such as Metal on a Mac, and hands it to the driver.
That’s the path for WebGL, where the translating is done by ANGLE. WebGPU takes the same path through Dawn. Text, CSS and Canvas 2D take a shorter one, through Skia, which turns drawing commands into the same kind of GPU work.

Figure 3. The layers of Chrome’s graphics stack, with the number of 2026 CVEs whose fix changed each layer. Everything right of the dashed line runs in the GPU process. 21 compositing CVEs and 23 that we couldn’t place aren’t shown.
Figure 4 counts the 2026 CVEs at each layer, placed by the files their fix changed.

Figure 4. 2026 graphics CVEs by the layer their fix changed, including compositing and the CVEs we couldn’t place.
The backends are the largest layer, and the command buffer is second. Each section below covers one layer and the exploited bugs fixed there.
Layer 1: The page

The page owns JavaScript objects like WebGLRenderingContext, GPUDevice and CanvasRenderingContext2D. Behind each one is C++ code in Blink that keeps track of GPU resources for it. These objects are created and freed by the page and cleaned up by the garbage collector, so many bugs here are about object lifetimes.
CVE-2021-30554: a trace that skipped part of the object
Chrome filed this use-after-free under WebGL and fixed it in June 2021 [7]. The fix is in WebXR, the API for VR and AR headsets. A WebXR session draws with WebGL through a layer object, XRWebGLLayer. Blink’s garbage collector finds live objects by asking each one to report the objects it holds, through a Trace method. The base class of XR layers reported only part of what it held.
From the fix, in third_party/blink/renderer/modules/xr/xr_layer.cc:
From the fix, in third_party/blink/renderer/modules/:
xr/xr_layer.cc
XRLayer is an event target, so it holds things like event listeners. By calling the tracing method of a more basic class, it skipped them, and the garbage collector could free objects that were still in use.
Layer 2: Crossing into the GPU process

The renderer writes its commands into a shared memory buffer, the command buffer, and the GPU process reads, checks and runs them. It can’t trust anything in that buffer, because a compromised renderer can write whatever it likes there. WebGPU does the same through Dawn’s “wire”, which turns WebGPU calls into messages and runs them again in the GPU process. Chrome’s design documents name security as the first goal of the command buffer, because it is what stands between a compromised renderer and the GPU driver [8].
This boundary also decides how serious a bug is. The GPU process has its own sandbox on most platforms, but on Android and on Linux with X11 it runs without one [9]. Chrome’s severity guidelines rate memory corruption in the GPU process that a page can reach without first compromising the renderer as Critical where the GPU process has no sandbox, and High where it has one [10]. 13% of 2026 graphics CVEs are rated Critical, twice the share for Chrome as a whole (6.5%).

Figure 5. Chromium’s severity ratings for 2026 CVEs, graphics compared with all of Chrome.
The most common bug at this layer is uninitialized memory, 29% of its 103 CVEs in 2026. “Leftover pixels” below looks at those.
CVE-2022-4135: checked against the type, not the texture
This heap buffer overflow was fixed in November 2022 [11]. Textures have mip levels, smaller copies of the image at half the size, a quarter, and so on. The command decoder is the part of the GPU process that reads each command and checks it. For texture commands, it asked whether the mip level was valid for the texture’s type, for example any 2D texture. It didn’t ask whether this particular texture had that many levels. The fix adds a check against the texture itself and uses it throughout the decoder.
Added by the fix, in gpu/command_buffer/service/texture_manager.cc:
Added by the fix, in gpu/command_buffer/service/:
texture_manager.cc
MaxValidMipLevel() is the size of the texture’s own list of levels. Checking against the type’s limit instead let a level index go past the end of that list.
CVE-2026-5281: a callback that outlived its device
This one is a use-after-free in Dawn, fixed in March 2026 [12]. When the page releases a WebGPU device, the wire server in the GPU process frees its record of that device. Devices can call back into the wire later, for example to report an error. The server cleared some of those callbacks before freeing the record, but not all of them. The fix swaps one call for another.
From the fix, in generator/templates/dawn/wire/server/ServerDoers.cpp (comments left out):
From the fix, in generator/templates/dawn/wire/server/ (comments left out):
ServerDoers.cpp
The old call cleared only the logging callback. Destroying the device also clears the error callback before the record is freed, so no callback is left pointing at freed memory.
Layer 3: Validation

Before a draw runs, the GPU process has to check that everything it will touch is valid for this call. The buffers must be big enough, the offsets in range and the formats matching, and no resource can be used in two conflicting ways at once. In our map, this layer is the front ends of ANGLE and Dawn, the code that tracks state and validates calls before any backend sees them.
Figure 6 splits each layer by bug type.

Figure 6. The kinds of bugs fixed at each layer in 2026, from Chrome’s one-line descriptions. The numbers are counts of CVEs.
CVE-2025-6558: writing to a buffer the GPU is still using
Chrome called this “incorrect validation of untrusted input” in ANGLE and fixed it in July 2025 [13]. Transform feedback lets a shader write its output into buffers. WebGL forbids using a buffer for transform feedback and for something else at the same time. ANGLE already checked one form of that rule. The fix adds another. While transform feedback is active and not paused, a buffer bound to it can’t have new data written into it through bufferData or bufferSubData.
Added by the fix, in src/libANGLE/validationES2.cpp, in both ValidateBufferData and ValidateBufferSubData:
Without this check, a page could change a buffer’s storage while the GPU was still writing into it.
Layer 4: Shader compilers

For WebGL and WebGPU, the page writes its shaders itself, in GLSL or WGSL. The GPU can’t run them as written, so Chrome compiles them. ANGLE’s shader translator handles GLSL, and Tint, part of Dawn, handles WGSL. On Windows, WebGPU also passes shaders through Microsoft’s DirectX Shader Compiler (DXC).
Skia has its own shader language, SkSL, but a page never writes it. Skia writes SkSL itself, from what the page draws: a gradient, a blur or a blend mode each adds its own piece of shader code. The page can’t write the shader, but it decides which pieces go into it.
47 CVEs in 2026 were fixed at this layer, and more than half of them (55%) are out-of-bounds bugs.
This layer also has bugs that weren’t in Chrome’s own code. In 2024, 14 of the 24 Dawn CVEs were fixed by moving Dawn to a patched version of DXC [14]. Those were bugs in Microsoft’s compiler, reachable from any web page through WebGPU on Windows.
CVE-2023-2136: parameters left out of the count
Chrome described this as an integer overflow in Skia and fixed it in April 2023 [15]. SkSL limits how much stack space a function’s variables can use. The compiler counted every local variable against that limit, but not the function’s parameters, so a function with very large parameters could go past the limit unchecked. The fix is one loop.
Added by the fix, in src/sksl/ir/SkSLFunctionDefinition.cpp, in the Finalizer constructor:
Added by the fix, in src/sksl/ir/SkSLFunctionDefinition.cpp,
in the Finalizer constructor:
The stack-size check now sees everything that goes on the stack, parameters included.
Layer 5: Backends and drivers

After validation and compilation, ANGLE and Dawn turn the work into calls to the platform’s graphics API. That’s Direct3D on Windows, Metal on macOS, Vulkan on Android and Linux, and OpenGL where needed. This code is different on every platform, so a bug here may affect only some users. It is also the last Chrome code before the GPU driver, and it includes Chrome’s list of workarounds for specific drivers. It’s the largest layer, and out-of-bounds bugs are its most common kind.
Code here is also shared outside Chrome. ANGLE is the WebGL backend for Chrome and for Firefox on Windows [16], and Safari’s WebGL runs on ANGLE’s Metal backend [17]. Both exploited bugs at this layer were fixed by Apple as well. One of them, CVE-2025-24201, has no public Chrome fix. Apple fixed it in WebKit as an out-of-bounds write that could let web content “break out of Web Content sandbox”, and called it a supplementary fix for an attack blocked in iOS 17.2 [18]. We placed it at this layer only because Chrome labeled it “GPU on Mac” [19].
CVE-2025-14174: a buffer sized for the wrong image
This out-of-bounds access in ANGLE was fixed in December 2025 [20]. When a page uploads depth data to a texture, ANGLE’s Metal backend first copies it into a temporary buffer. It sized that buffer with the size of one full image in the source data (pixelsDepthPitch), which the page can influence through its pixel storage settings (gl.pixelStorei), instead of the size of the area being copied.
From the fix, in src/libANGLE/renderer/metal/TextureMtl.mm:
From the fix, in src/libANGLE/renderer/metal/:
TextureMtl.mm
When the source image was shorter than the texture area, the buffer was too small for the copy that followed.
Apple credits itself and Google’s Threat Analysis Group for this bug, and fixed it in WebKit two days after Chrome [21].
Layer 6: Skia

Most of what a page draws isn’t WebGL at all. Text, CSS and Canvas 2D go through Skia, which records drawing operations, merges them into batches and turns them into GPU work. It saves work wherever it can, by combining draws and caching results. For years Skia’s GPU backend was Ganesh. Chrome has been moving to a newer one, Graphite, first on Apple Silicon Macs [22].
CVE-2023-6345: an addition that overflowed
This is another integer overflow in Skia, fixed in November 2023 [23]. To save work, Ganesh combines several mesh draws into one when it can. Before combining two meshes, it checked that their total vertex count fit in the 16-bit numbers it uses to refer to vertices. It computed that total by adding the two counts first, and the addition itself could overflow.
From the fix, in src/gpu/ganesh/ops/DrawMeshOp.cpp:
Before the fix, a total that overflowed could pass the check, so two meshes too large to combine were combined anyway. Comparing against limit - b can’t overflow.
Zero Day Engineering published an analysis of this patch when the bug was disclosed, including what it meant on different platforms [24].
CVE-2026-3909: a cache key without the format
This out-of-bounds write in Skia was fixed in March 2026 [25]. A glyph is the drawn shape of a character. Skia draws text from glyph atlases, large textures that hold many rendered glyphs. There are separate atlases for different glyph formats, for example plain coverage masks and full-color glyphs. The cache that maps a glyph to its place in an atlas used only the glyph’s ID as the key, so the same glyph asked for in two formats could get an entry that pointed into the wrong atlas. The fix adds the format to the key, in both Ganesh and Graphite.
Added by the fix, in src/gpu/ganesh/text/GlyphData.h:
A cache key that leaves out part of what makes an entry unique hands back the wrong memory.

Figure 7. Before the fix, one cache entry served a glyph in both formats, so a request for the color version could be sent into the mask atlas. After it, each format has its own entry. The glyph ID and atlas slots are made up.
Leftover pixels
One kind of bug stands out in graphics. When the GPU allocates a new texture or buffer, the memory isn’t empty. It holds whatever was there before, possibly from another tab or application. This is a known risk. The WebGL specification requires that resources always contain initialized data [2], and Chrome’s command buffer design says every texture must be cleared before use so a page can’t read leftover video memory [8].
From 2017 to 2025, Chrome described 6 graphics CVEs as uninitialized memory. In 2026 it described 91 that way, with 34 in ANGLE, 25 in the GPU process, 15 in Skia, 12 in Dawn, 4 in WebGL and 1 in Canvas. That’s 18% of graphics CVEs this year. In V8 it’s 3%, or 4 CVEs (see our V8 survey).
Clearing every new resource right away would be slow, so Chrome clears a resource only when it’s first used, and marks it as cleared. From then on, that mark is what keeps the old contents away from the page.

Figure 8. The top row is how clearing is meant to work. The bottom row is the bug, where the mark says “cleared” but the memory still holds old contents and the page can read it. The counts are how many 2026 fixes went wrong each way.
We read the fix for each of the 91. In 65, GPU memory could reach the page without being cleared or written, and Figure 8 shows the four ways that happened. The other 26 are ordinary uninitialized variables, or fixes that don’t show enough to tell.
One example is CVE-2026-11109, fixed in ANGLE’s OpenGL backend [26]. WebGL 2 lets a page turn on “rasterizer discard”, which tells the GPU to skip drawing pixels. ANGLE clears new resources by drawing into them, so with rasterizer discard on, the clear itself was skipped.
Added by the fix, in src/libANGLE/renderer/gl/BlitGL.cpp:
With discard forced off, the clear really writes the memory it marks as cleared.
The same mistake was fixed in four other places in 2026, all in Chrome’s own command decoder. None of the nine graphics CVEs marked as exploited in the wild is described as uninitialized memory.
Nine exploited bugs, all along the path

Figure 9. The nine graphics CVEs Chrome marked as exploited in the wild, placed by the layer their fix changed. CVE-2025-24201 (marked with an asterisk) has no public Chrome fix, so it is placed by its Chrome label, “GPU on Mac”.
Eight of the nine have public Chrome fixes, and those fixes span all six layers.
| CVE | Fixed in Chrome | Component | Layer | What the Fix Changed |
|---|---|---|---|---|
| CVE-2021-30554 | June 2021 | WebGL | the page | XR layers report everything they hold to the garbage collector |
| CVE-2022-4135 | November 2022 | GPU | command buffer | Mip levels are checked against the texture, not its type |
| CVE-2026-5281 | March 2026 | Dawn | command buffer | Releasing a WebGPU device destroys it, which clears every callback |
| CVE-2025-6558 | July 2025 | ANGLE and GPU | validation | A buffer in use by transform feedback can’t get new data |
| CVE-2023-2136 | April 2023 | Skia | shader compilers | Function parameters count toward SkSL’s stack limit |
| CVE-2025-14174 | December 2025 | ANGLE | backends | Metal’s temporary buffer is sized for the area copied |
| CVE-2025-24201 | March 2025 | GPU on Mac | backends (by label) | No public Chrome fix |
| CVE-2023-6345 | November 2023 | Skia | Skia | Mesh vertex counts are checked without overflowing |
| CVE-2026-3909 | March 2026 | Skia | Skia | The glyph cache key includes the format |
Recap
Graphics bugs turn up at every layer of the stack, and so do the ones attackers use. Chrome’s labels are only a rough guide, so look at where each fix lands. The standout in 2026 is leftover memory: in 65 fixes, GPU memory could reach a page before it was cleared, so any code that marks memory as cleared is worth a close look. The case studies come down to a few small mistakes: an object freed while still in use, a size checked against the wrong limit, or a check that skips part of its input. And since Safari and Firefox on Windows also use ANGLE, a bug there can reach more than one browser.
About the data
We used chrome-cves [1] and v8-cves [27], both built from Chrome Releases [28], through September 22, 2026. A CVE counts as graphics if Chrome filed it under ANGLE, GPU, Dawn or WebGPU, Skia, WebGL, Compositing, Canvas or SwiftShader, which gives 710 since 2017. We placed each one on a layer by the files its fix changed, ignoring tests, and sorted bug types from Chrome’s one-line descriptions. The 91 uninitialized-memory fixes we sorted by hand, against their full diffs. A fix shows where a bug was fixed, which is not always where it started, and 2026 isn’t over yet.
References
- chrome-cves. https://github.com/str8outtaheap/chrome-cves
- WebGL 1.0 specification. https://registry.khronos.org/webgl/specs/latest/1.0/
- ANGLE. https://chromium.googlesource.com/angle/angle/
- WebGPU specification. https://www.w3.org/TR/webgpu/
- Dawn. https://dawn.googlesource.com/dawn
- Skia. https://skia.org/
- CVE-2021-30554 fix: “Ensure that XRLayer includes base EventTarget in Trace.” https://chromium.googlesource.com/chromium/src/+/01b6f7e0a70648d7c7302454993f0bf86d5a0241 Chrome 91.0.4472.114, June 17, 2021. https://chromereleases.googleblog.com/2021/06/stable-channel-update-for-desktop_17.html
- Chromium, “GPU Command Buffer” design document. https://www.chromium.org/developers/design-documents/gpu-command-buffer/
- Chromium, “Process Sandboxes by Platform.” https://chromium.googlesource.com/chromium/src/+/main/docs/security/process-sandboxes-by-platform.md
- Chromium, “Severity Guidelines for Security Issues.” https://chromium.googlesource.com/chromium/src/+/main/docs/security/severity-guidelines.md
- CVE-2022-4135 fix: “Fix potential OOB problem with validating command decoder.” https://chromium.googlesource.com/chromium/src/+/6b4af5d8208398839e90c7adc2da22c0288d6b47 Chrome 107.0.5304.121, November 24, 2022. https://chromereleases.googleblog.com/2022/11/stable-channel-update-for-desktop_24.html
- CVE-2026-5281 fix: “[dawn][wire] Ensure that Devices on the Server always call Destroy.” https://dawn.googlesource.com/dawn/+/adcf333bd486a7f0a1c5c4a52c8ea5ae54e15c32 Chrome 146.0.7680.177, March 31, 2026. https://chromereleases.googleblog.com/2026/03/stable-channel-update-for-desktop_31.html
- CVE-2025-6558 fix: “Validate buffers bound for transform feedback are not modified.” https://chromium.googlesource.com/angle/angle/+/2f8193ecfe1ed464374ae56235cfdc112343f9c3 Chrome 138.0.7204.157, July 15, 2025. https://chromereleases.googleblog.com/2025/07/stable-channel-update-for-desktop_15.html
- Example DXC update in Dawn (CVE-2024-2885): “DEPS: Update DXC to patched branch.” https://dawn.googlesource.com/dawn/+/d654129e99a74c339a1aadce340d6aa62d3ded51
- CVE-2023-2136 fix: “Enforce program stack limits on function parameters.” https://skia.googlesource.com/skia/+/4dc748f14c6650cb45c7086a39af1760bfda41d2 Chrome 112.0.5615.137, April 18, 2023. https://chromereleases.googleblog.com/2023/04/stable-channel-update-for-desktop_18.html
- ANGLE README. https://chromium.googlesource.com/angle/angle/+/main/README.md
- WebKit bug 220076, “Enable Metal ANGLE backend for WebGL.” https://bugs.webkit.org/show_bug.cgi?id=220076
- Apple, “About the security content of iOS 18.3.2 and iPadOS 18.3.2.” https://support.apple.com/en-us/122281
- Chrome 134.0.6998.88, March 10, 2025. https://chromereleases.googleblog.com/2025/03/stable-channel-update-for-desktop_10.html
- CVE-2025-14174 fix: “Metal: Don’t use pixelsDepthPitch to size buffers.” https://chromium.googlesource.com/angle/angle/+/95a32cb37edbb90eac0b83727b38fedbbb32307b Chrome 143.0.7499.109, December 10, 2025. https://chromereleases.googleblog.com/2025/12/stable-channel-update-for-desktop_10.html
- Apple, “About the security content of Safari 26.2.” https://support.apple.com/en-us/125892
- Chromium blog, “Introducing Skia Graphite: Chrome’s rasterization backend for the future,” July 8, 2025. https://blog.google/chromium/introducing-skia-graphite-chromes/
- CVE-2023-6345 fix: “Avoid combining extremely large meshes.” https://skia.googlesource.com/skia/+/6169a1fabae1743709bc9641ad43fcbb6a4f62e1 Chrome 119.0.6045.199, November 28, 2023. https://chromereleases.googleblog.com/2023/11/stable-channel-update-for-desktop_28.html
- Zero Day Engineering, “Google Chrome Skia Vulnerability Insights (CVE-2023-6345),” November 30, 2023. https://zerodayengineering.com/insights/chrome-skia-cve-2023-6345.html
- CVE-2026-3909 fix: “Make sure we are getting the correct atlas for glyph mask format.” https://skia.googlesource.com/skia/+/0cab3e4ee34b3bca6ba7df676639d73ffe4b2135 Chrome 146.0.7680.75, March 12, 2026. https://chromereleases.googleblog.com/2026/03/stable-channel-update-for-desktop_12.html
- CVE-2026-11109 fix: “GL: Disable rasterizer discard for robust resource init.” https://chromium.googlesource.com/angle/angle/+/ef5a8a5275da668cf381adc8f87ee17767be4573
- v8-cves. https://github.com/str8outtaheap/v8-cves
- Chrome Releases. https://chromereleases.googleblog.com/
