diff --git a/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc index 94139b6ce7bba..49471e2632f94 100644 --- a/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc +++ b/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc @@ -7,6 +7,10 @@ #include #include "base/command_line.h" +#include "base/compiler_specific.h" +#include "base/containers/span.h" +#include "base/logging.h" +#include "base/strings/stringprintf.h" #include "build/build_config.h" #include "cc/test/pixel_comparator.h" #include "cc/test/pixel_test_utils.h" @@ -29,11 +33,15 @@ #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/include/core/SkImage.h" #include "third_party/skia/include/gpu/ganesh/GrBackendSemaphore.h" #include "third_party/skia/include/gpu/ganesh/GrBackendSurface.h" +#include "third_party/skia/include/gpu/ganesh/SkImageGanesh.h" #include "third_party/skia/include/private/chromium/GrPromiseImageTexture.h" #include "ui/gfx/color_space.h" #include "ui/gfx/gpu_memory_buffer_handle.h" +#include "ui/gl/gl_bindings.h" #include "ui/gl/gl_implementation.h" #include "ui/gl/gl_version_info.h" #include "ui/gl/progress_reporter.h" @@ -604,6 +612,195 @@ TEST_P(GLTextureImageBackingFactoryInitialDataTest, InitialData) { } } +// ============================================================================ +// SECURITY: SetCleared() called unconditionally after unchecked glTexSubImage2D +// ---------------------------------------------------------------------------- +// GLTextureHolder::Initialize() issues glTexStorage2DEXT (allocates undefined +// VRAM — robust-resource-init is disabled on the SharedContextState GL context +// in production, and in this test fixture) followed by glTexSubImage2D, with NO +// glGetError() check. GLTextureImageBacking::InitializeGLTexture() then calls +// SetCleared() unconditionally whenever pixel_data was non-empty. +// +// If the upload fails at the driver level (GL_OUT_OF_MEMORY on the host +// staging-buffer allocation under attacker-induced memory pressure), the +// texture retains its undefined allocator residue, but the backing is still +// marked cleared. Subsequent BeginScopedReadAccess() — gated only by +// IsCleared() — then permits readback of uninitialized GPU memory, which is +// shared across all origins in the GPU process. +// +// This test simulates driver-level glTexSubImage2D failure by hooking the +// DriverGL function pointer (the only way to deterministically reproduce the +// condition without a real GPU under memory pressure). It then asserts: +// (1) backing->IsCleared() == true despite the failed upload ← THE BUG +// (2) BeginScopedReadAccess() succeeds (gate defeated) +// (3) Read-back pixels do NOT match the uploaded data, i.e. the texture +// still holds uninitialized contents. +// ============================================================================ + +namespace { + +// Saved real driver entry, restored after the test. +gl::glTexSubImage2DProc g_real_tex_sub_image_2d = nullptr; +int g_failed_tex_sub_image_2d_calls = 0; + +void GL_BINDING_CALL FailingTexSubImage2D(GLenum target, GLint level, GLint, + GLint, GLsizei, GLsizei, GLenum format, + GLenum type, const void*) { + // Simulates a driver-level glTexSubImage2D failure: per the GL spec, when + // a TexSubImage call generates an error the command has no effect and the + // texture contents are left unchanged (i.e. the undefined storage from + // glTexStorage2DEXT remains). We forward to the real entry point with a + // negative width so the driver itself records a GL error — this is the + // observable state a real GL_OUT_OF_MEMORY would leave behind. + ++g_failed_tex_sub_image_2d_calls; + g_real_tex_sub_image_2d(target, level, 0, 0, /*width=*/-1, /*height=*/-1, + format, type, nullptr); +} + +} // namespace + +TEST_F(GLTextureImageBackingFactoryTest, + Security_SetClearedAfterFailedTexSubImage2D) { + auto format = viz::SinglePlaneFormat::kRGBA_8888; + gfx::Size size(64, 64); + auto color_space = gfx::ColorSpace::CreateSRGB(); + GrSurfaceOrigin surface_origin = kTopLeft_GrSurfaceOrigin; + SkAlphaType alpha_type = kPremul_SkAlphaType; + gpu::SharedImageUsageSet usage = + SHARED_IMAGE_USAGE_GLES2_READ | SHARED_IMAGE_USAGE_RASTER_READ; + + // Seed the GL allocator with a texture full of a recognisable "secret" + // pattern, then delete it so its storage becomes allocator residue. This + // models cross-origin texture content from another tab in the GPU process. + { + gl::GLApi* api = gl::g_current_gl_context; + GLuint seed = 0; + api->glGenTexturesFn(1, &seed); + api->glBindTextureFn(GL_TEXTURE_2D, seed); + api->glTexStorage2DEXTFn(GL_TEXTURE_2D, 1, GL_RGBA8, size.width(), + size.height()); + std::vector secret(size_t{4} * size.width() * size.height(), 0xDE); + api->glTexSubImage2DFn(GL_TEXTURE_2D, 0, 0, 0, size.width(), size.height(), + GL_RGBA, GL_UNSIGNED_BYTE, secret.data()); + api->glBindTextureFn(GL_TEXTURE_2D, 0); + api->glDeleteTexturesFn(1, &seed); + } + + // Distinctive sentinel for the *attacker-supplied* upload data. If the + // upload had succeeded, every byte read back would be 0xAA. + size_t required_size = format.MaybeEstimatedSizeInBytes(size).value(); + std::vector initial_data(required_size, 0xAA); + + ASSERT_TRUE(backing_factory_->CanCreateSharedImage( + usage, format, size, /*thread_safe=*/false, gfx::EMPTY_BUFFER, + GrContextType::kGL, initial_data)); + + // ---- Hook glTexSubImage2D at the driver layer to simulate OOM. ---------- + gl::DriverGL* driver = gl::GetThreadLocalCurrentGL()->Driver; + ASSERT_TRUE(driver); + g_real_tex_sub_image_2d = driver->fn.glTexSubImage2DFn; + g_failed_tex_sub_image_2d_calls = 0; + driver->fn.glTexSubImage2DFn = &FailingTexSubImage2D; + + auto backing = backing_factory_->CreateSharedImage( + Mailbox::Generate(), + {format, size, color_space, surface_origin, alpha_type, usage, + "SecurityPoC"}, + /*is_thread_safe=*/false, initial_data); + + // ---- Restore the real driver entry before any further GL work. ---------- + driver->fn.glTexSubImage2DFn = g_real_tex_sub_image_2d; + + ASSERT_TRUE(backing); + // The vulnerable code path was reached: GLTextureHolder::Initialize tried + // to upload via glTexSubImage2D and the (simulated) driver failed it, + // leaving a GL error pending — exactly what GL_OUT_OF_MEMORY would do. + ASSERT_GE(g_failed_tex_sub_image_2d_calls, 1); + GLenum pending_error = gl::g_current_gl_context->glGetErrorFn(); + LOG(ERROR) << "[BUG] glTexSubImage2D left pending GL error 0x" << std::hex + << pending_error + << " which GLTextureHolder::Initialize never checked"; + EXPECT_NE(pending_error, static_cast(GL_NO_ERROR)); + // Drain any remaining errors so they don't interfere with readback. + while (gl::g_current_gl_context->glGetErrorFn() != GL_NO_ERROR) { + } + + // *** VULNERABILITY *** + // The upload to the freshly-allocated texture FAILED, yet the backing is + // marked cleared. This defeats the IsCleared() gate that guards + // BeginScopedReadAccess() / BeginScopedAccess(), allowing uninitialised + // GPU texture memory to be read. + EXPECT_TRUE(backing->IsCleared()) + << "[unexpected] backing was NOT marked cleared - bug already fixed?"; + LOG(ERROR) << "[BUG] SetCleared() fired despite failed glTexSubImage2D: " + << "IsCleared() = " << backing->IsCleared(); + + // Demonstrate that the IsCleared() gate is defeated and that what is read + // back is NOT the uploaded data — i.e. it is whatever was left in the + // texture's storage by glTexStorage2DEXT (allocator residue / zero, + // depending on the GL backend; on production hardware GL with robust + // resource init disabled it is genuine undefined VRAM). + Mailbox mailbox = backing->mailbox(); + auto shared_image = + shared_image_manager_.Register(std::move(backing), &memory_type_tracker_); + ASSERT_TRUE(shared_image); + + auto skia = shared_image_representation_factory_.ProduceSkia(mailbox, + context_state_); + ASSERT_TRUE(skia); + std::vector begin_sem, end_sem; + auto read = skia->BeginScopedReadAccess(&begin_sem, &end_sem); + // This MUST be null if IsCleared() were correctly false; the fact that it + // succeeds proves the gate at SharedImageRepresentation::IsCleared() is + // bypassed. + ASSERT_TRUE(read) << "BeginScopedReadAccess() refused — gate held; bug " + "appears fixed"; + + SkImageInfo dst_info = + SkImageInfo::Make(size.width(), size.height(), kRGBA_8888_SkColorType, + kPremul_SkAlphaType); + SkBitmap dst; + dst.allocPixels(dst_info); + auto* tex = read->promise_image_texture(0); + ASSERT_TRUE(tex); + auto sk_image = + SkImages::BorrowTextureFrom(gr_context(), tex->backendTexture(), + surface_origin, kRGBA_8888_SkColorType, + alpha_type, /*colorSpace=*/nullptr); + ASSERT_TRUE(sk_image); + ASSERT_TRUE(sk_image->readPixels(gr_context(), dst.pixmap(), 0, 0)); + + size_t total_bytes = dst.computeByteSize(); + // SAFETY: SkBitmap::getPixels() returns a contiguous buffer of + // computeByteSize() bytes after a successful allocPixels(). + auto px = UNSAFE_BUFFERS(base::span( + static_cast(dst.getPixels()), total_bytes)); + size_t matching = 0; + for (uint8_t b : px) { + if (b == 0xAA) { + ++matching; + } + } + std::string hex; + for (size_t i = 0; i < 16 && i < px.size(); ++i) { + hex += base::StringPrintf("%02x ", px[i]); + } + LOG(ERROR) << "[BUG] Read back " << total_bytes << " bytes; " + << (total_bytes - matching) + << " bytes did NOT match uploaded sentinel 0xAA " + "(first 16 bytes: " << hex << ")"; + + // The upload was suppressed; the texture must NOT contain the sentinel. + // Whatever IS there is uninitialised storage (allocator residue on a + // production robust-init-disabled context). + EXPECT_NE(matching, total_bytes) + << "Texture unexpectedly contains the uploaded data"; + + read.reset(); + skia.reset(); + shared_image.reset(); +} + TEST_P(GLTextureImageBackingFactoryInitialDataTest, InitialDataWrongSize) { viz::SharedImageFormat format = get_format(); if (!IsFormatSupport(format)) {