Chrome · SiteIsolation
CVE-2026-113079
UAF in SiteIsolation
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/viz/host/host_frame_sink_manager.cc |
modified | |
forcomponents/viz/host/host_frame_sink_manager.cc |
modified | |
ifcomponents/viz/service/frame_sinks/frame_sink_manager_impl.cc |
modified |
Files Changed
components/viz/host/host_frame_sink_manager_unittest.cccomponents/viz/host/host_frame_sink_manager.hservices/viz/privileged/mojom/compositing/frame_sink_manager.mojomcomponents/viz/service/display/surface_aggregator_unittest.cccomponents/viz/service/frame_sinks/frame_sink_manager_impl.cccomponents/viz/common/quads/compositor_frame_unittest.cccomponents/viz/host/host_frame_sink_manager.cccomponents/viz/service/frame_sinks/compositor_frame_sink_support_unittest.cccomponents/viz/common/BUILD.gncomponents/viz/common/quads/compositor_frame.hcomponents/viz/service/frame_sinks/frame_sink_manager_unittest.cccomponents/viz/common/quads/compositor_frame.cccomponents/viz/test/test_frame_sink_manager.hcomponents/viz/service/frame_sinks/frame_sink_manager_impl.h
Patch
From 99c367b908fb27d958d8e961ce3b041bcfd51a5c Mon Sep 17 00:00:00 2001 From: Charlie Reis <[email protected]> Date: Fri, 25 Sep 2026 11:21:35 -0700 Subject: [PATCH] Add a Viz notification for the first visually non-empty frame. When an unmodified blank tab navigates, the omnibox displays the pending URL as long as the initial empty document has not been modified. Today the browser process learns that the document was modified only from the renderer-controlled DidAccessInitialMainDocument IPC, so a compromised renderer can paint spoofed content into the initial empty document without sending it, leading to a URL spoof. Add a trustworthy signal for this by detecting visually non-empty content in Viz and notifying the browser process directly. A follow-up CL consumes the signal in RenderWidgetHostImpl. Specifically: 1. Add CompositorFrame::HasVisuallyNonEmptyContent(), which reports whether a frame contains visual content: more than one render pass, or a root pass with any quads other than a single SolidColorDrawQuad (the default background fill), or partial damage that could accumulate. 2. Add one-way mojom methods FrameSinkManager:: RequestNonEmptyFrameNotification() and CancelNonEmptyFrameNotification(), plus FrameSinkManagerClient::OnFirstNonEmptyFrame(). 3. Track armed FrameSinkIds in FrameSinkManagerImpl rather than in CompositorFrameSinkSupport, and fire the notification from SurfaceObserver::OnSurfaceActivated(). Keying the state on the FrameSinkId makes it span the entire RegisterFrameSinkId() to InvalidateFrameSinkId() lifetime, so a compromised renderer cannot discard it by submitting a frame and then closing its CompositorFrameSink pipe while the activated Surface remains on screen. 4. Have HostFrameSinkManager hold the pending callbacks and run them when Viz reports a frame. The report is an asynchronous Mojo message in every production configuration (out-of-process Viz, single process mode, and WebView), so Viz is not on the stack and the callback cannot re-enter surface activation. Only tests wire the client up directly with FrameSinkManagerImpl::SetLocalClient(). Callers must call RequestNonEmptyFrameNotification() before CreateCompositorFrameSink() on the same browser-to-Viz pipe, so that Viz is watching before the client can submit anything. This CL was mostly authored by AI. Bug: 40055319 Change-Id: I09721afc4425896279e765cbb2f0d48ddcf6bd16 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8283669 Reviewed-by: Kyle Charbonneau <[email protected]> Reviewed-by: Mustafa Emre Acer <[email protected]> Commit-Queue: Charlie Reis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1704976} --- diff --git a/components/viz/common/quads/compositor_frame.cc b/components/viz/common/quads/compositor_frame.cc index 26e075c6..37d881b6 100644 --- a/components/viz/common/quads/compositor_frame.cc +++ b/components/viz/common/quads/compositor_frame.cc @@ -10,6 +10,8 @@ #include "base/trace_event/trace_event.h" #include "base/trace_event/traced_value.h" #include "base/values.h" +#include "cc/paint/filter_operations.h" +#include "components/viz/common/quads/draw_quad.h" #include "components/viz/common/traced_value.h" namespace viz { @@ -33,6 +35,68 @@ return false; } +bool CompositorFrame::HasVisuallyNonEmptyContent() const { + if (render_pass_list.empty()) { + return false; + } + // Multiple render passes indicate intermediate passes (e.g. effects, filters, + // masks), which imply rendered content. + if (render_pass_list.size() > 1) { + return true; + } + + // An empty root pass or a single solid color quad (representing the default + // background fill) are both considered empty, as long as the frame damages + // its entire output_rect (see below). We do not check the single quad's + // coverage, clipping, or transform, because an attacker cannot realistically + // spoof a page with a single solid color quad and no resources, and avoiding + // strict rect checks prevents false positives from viewport sizing or scale + // rounding quirks. + // + // Anything else (e.g. tile, picture, texture, surface, debug border) + // represents rendered content and is considered non-empty. + // + // Note: Any transferable resources (textures, tile raster buffers, images) + // in `resource_list` also imply non-empty content, but checking for this is + // unnecessary for empty quad lists or single solid quads, since those quads + // do not reference resources. + const auto& root_pass = render_pass_list.back(); + + // Filters can paint arbitrary content from a single quad: ALPHA_THRESHOLD + // masks the pass to an arbitrary region, and REFERENCE applies an arbitrary + // PaintFilter graph. Both are deserialized from untrusted clients. A client + // cannot currently get filters on its own root pass rendered, because + // SurfaceAggregator drops them when it merges the pass into its embedder and + // only the embedder can prevent that merge, so this is defense in depth + // against that changing. It costs little in practice: a blank document never + // sets filters on its root pass, and legitimate CSS filters render into their + // own pass, which the check above already treats as non-empty. + if (!root_pass->filters.IsEmpty() || !root_pass->backdrop_filters.IsEmpty()) { + return true; + } + + // A frame that does not damage all of `output_rect` can leave pixels outside + // `damage_rect` on screen from earlier frames until the display compositor + // next damages and redraws the surface in full: SurfaceAggregator uses the + // root pass damage as the surface's damage, and DirectRenderer scissors the + // redraw to it. A client could otherwise draw content one small rect per + // frame, or erase rects back to the embedder's background color with an empty + // quad list, while every individual frame looks empty here. A blank document + // always damages its whole viewport: the first frame of a surface is fully + // damaged, and nothing in a blank document can damage a sub-rect on its own. + if (!root_pass->damage_rect.Contains(root_pass->output_rect)) { + return true; + } + + if (root_pass->quad_list.empty()) { + return false; + } + const bool has_single_solid_color_quad = + root_pass->quad_list.size() == 1 && + root_pass->quad_list.front()->material == DrawQuad::Material::kSolidColor; + return !has_single_solid_color_quad; +} + void CompositorFrame::AsValueInto(base::trace_event::TracedValue* value) const { value->BeginDictionary("metadata"); metadata.AsValueInto(value); diff --git a/components/viz/common/quads/compositor_frame.h b/components/viz/common/quads/compositor_frame.h index 78d0d08e..175b561 100644 --- a/components/viz/common/quads/compositor_frame.h +++ b/components/viz/common/quads/compositor_frame.h @@ -49,6 +49,16 @@ bool HasCopyOutputRequests() const; + // Returns true if this frame contains visually non-empty content (e.g. text, + // images, canvases, or non-background draw quads), as opposed to an empty + // frame or a frame containing only a solid background color. + // + // This lets Viz distinguish "the client painted something" from "the client + // produced a frame", without trusting any renderer-supplied signal. Notably, + // a client that merely reacts to a resize or a device scale factor change + // still produces empty frames by this definition. + bool HasVisuallyNonEmptyContent() const; + void AsValueInto(base::trace_event::TracedValue* value) const; std::string ToString() const; diff --git a/components/viz/host/host_frame_sink_manager.cc b/components/viz/host/host_frame_sink_manager.cc index e575f2b9..0b89f0a 100644 --- a/components/viz/host/host_frame_sink_manager.cc +++ b/components/viz/host/host_frame_sink_manager.cc @@ -111,12 +111,42 @@ return iter != frame_sink_data_map_.end() && iter->second.client != nullptr; } +void HostFrameSinkManager::RequestNonEmptyFrameNotification( + const FrameSinkId& frame_sink_id, + base::OnceClosure on_non_empty_frame) { + DCHECK(frame_sink_id.is_valid()); + CHECK(IsFrameSinkIdRegistered(frame_sink_id)); + DCHECK(on_non_empty_frame); + + // Any previously requested callback is simply dropped. It is never run, so + // there is no re-entrancy to worry about here. + non_empty_frame_callbacks_[frame_sink_id] = std::move(on_non_empty_frame); + + if (frame_sink_manager_) { + frame_sink_manager_->RequestNonEmptyFrameNotification(frame_sink_id); + } +} + +void HostFrameSinkManager::CancelNonEmptyFrameNotification( + const FrameSinkId& frame_sink_id) { + if (!non_empty_frame_callbacks_.erase(frame_sink_id)) { + return; + } + if (frame_sink_manager_) { + frame_sink_manager_->CancelNonEmptyFrameNotification(frame_sink_id); + } +} + void HostFrameSinkManager::InvalidateFrameSinkId( const FrameSinkId& frame_sink_id, HostFrameSinkClient* client, base::OnceClosure callback) { DCHECK(frame_sink_id.is_valid()); + // Viz drops its own request as part of invalidation, so there is no need to + // send CancelNonEmptyFrameNotification() here. + non_empty_frame_callbacks_.erase(frame_sink_id); + FrameSinkData& data = frame_sink_data_map_[frame_sink_id]; CHECK(data.IsFrameSinkRegistered()); CHECK_EQ(data.client, client); @@ -500,6 +530,13 @@ child_frame_sink_id); } } + + // Re-arm pending non-empty frame notifications, since the new Viz instance + // has no knowledge of them. This happens before any CompositorFrameSink is + // recreated, so the client still cannot paint before Viz is armed. + for (const auto& map_entry : non_empty_frame_callbacks_) { + frame_sink_manager_->RequestNonEmptyFrameNotification(map_entry.first); + } } void HostFrameSinkManager::OnFirstSurfaceActivation( @@ -517,6 +554,27 @@ } } +void HostFrameSinkManager::OnFirstNonEmptyFrame( + const FrameSinkId& frame_sink_id) { + auto it = non_empty_frame_callbacks_.find(frame_sink_id); + if (it == non_empty_frame_callbacks_.end()) { + return; + } + + // Move the callback out and erase the entry before running it, so that a + // callback which re-enters HostFrameSinkManager cannot invalidate `it`. + base::OnceClosure callback = std::move(it->second); + non_empty_frame_callbacks_.erase(it); + + // Run synchronously. In production this is an asynchronous Mojo message in + // all configurations (out-of-process Viz, single process, and Android + // WebView), so Viz is not on the stack and a consumer that tears down the + // widget or the surface in response cannot re-enter surface activation. Only + // tests connect directly via FrameSinkManagerImpl::SetLocalClient(), where + // FrameSinkManagerImpl calls its client from Surface::ActivateFrame(). + std::move(callback).Run(); +} + void HostFrameSinkManager::OnAggregatedHitTestRegionListUpdated( const FrameSinkId& frame_sink_id, const std::vector<AggregatedHitTestRegion>& hit_test_data) { diff --git a/components/viz/host/host_frame_sink_manager.h b/components/viz/host/host_frame_sink_manager.h index f7d8fc2..891beb5 100644 --- a/components/viz/host/host_frame_sink_manager.h +++ b/components/viz/host/host_frame_sink_manager.h @@ -109,6 +109,44 @@ // InvalidateFrameSinkId() has not been called. bool IsFrameSinkIdRegistered(const FrameSinkId& frame_sink_id) const; + // Requests a one-shot notification for when Viz activates a CompositorFrame + // with visually non-empty content for `frame_sink_id`, and runs + // `on_non_empty_frame` when that happens. This is a trustworthy signal that + // the client painted something, since it does not depend on any data supplied + // by the client. See https://crbug.com/40055319. + // + // `frame_sink_id` must already be registered with RegisterFrameSinkId(), + // because the request is dropped by InvalidateFrameSinkId(). + // + // `on_non_empty_frame` runs at most once, and is never run to signal failure. + // It runs synchronously when Viz reports the non-empty frame, which is an + // asynchronous Mojo message in all production configurations. If the frame + // sink is invalidated, or the notification is cancelled, or this + // HostFrameSinkManager is destroyed before that report arrives, the callback + // is dropped without running. + // + // Tests that connect to Viz directly with + // FrameSinkManagerImpl::SetLocalClient() have no Mojo message in between, so + // the callback may run while Viz is on the stack: either from within this + // call, if a non-empty frame has already activated, or from within + // Surface::ActivateFrame(). + // + // The request is retained across CompositorFrameSink recreation and GPU + // process crashes, so a renderer cannot disarm it. To observe the client's + // very first frame, call this before CreateCompositorFrameSink() for the same + // `frame_sink_id`; both messages are ordered on the same pipe to Viz. + // + // Calling this again for the same `frame_sink_id` replaces any previously + // requested callback. + void RequestNonEmptyFrameNotification(const FrameSinkId& frame_sink_id, + base::OnceClosure on_non_empty_frame); + + // Cancels a pending RequestNonEmptyFrameNotification() for `frame_sink_id`, + // dropping the callback without running it. Does nothing if none is pending, + // including when Viz has already reported a non-empty frame and the callback + // has run. + void CancelNonEmptyFrameNotification(const FrameSinkId& frame_sink_id); + // Invalidates `frame_sink_id` when the client is done submitting // CompositorFrames. If there is a CompositorFrameSink for `frame_sink_id` // then it will be destroyed and the message pipe to the client will be @@ -394,6 +432,7 @@ uint32_t frame_token, base::TimeTicks activation_time) override; void OnFirstSurfaceActivation(const SurfaceInfo& surface_info) override; + void OnFirstNonEmptyFrame(const FrameSinkId& frame_sink_id) override; void OnAggregatedHitTestRegionListUpdated( const FrameSinkId& frame_sink_id, const std::vector<AggregatedHitTestRegion>& hit_test_data) override; @@ -434,6 +473,13 @@ base::flat_map<FrameSinkId, base::ScopedClosureRunner> frame_sink_invalidate_callbacks_; + // Pending RequestNonEmptyFrameNotification() callbacks, keyed by FrameSinkId. + // + // These callbacks are never run to signal failure, so erasing an entry is + // always a correct teardown, and no callback ever runs from a destructor or + // from inside a container mutation. + base::flat_map<FrameSinkId, base::OnceClosure> non_empty_frame_callbacks_; + // If |frame_sink_manager_remote_| connection was lost. bool connection_was_lost_ = false; diff --git a/components/viz/service/frame_sinks/frame_sink_manager_impl.cc b/components/viz/service/frame_sinks/frame_sink_manager_impl.cc index 1569fa2..2be3e529 100644 --- a/components/viz/service/frame_sinks/frame_sink_manager_impl.cc +++ b/components/viz/service/frame_sinks/frame_sink_manager_impl.cc @@ -30,6 +30,7 @@ #include "components/input/utils.h" #include "components/viz/common/features.h" #include "components/viz/common/performance_hint_utils.h" +#include "components/viz/common/quads/compositor_frame.h" #include "components/viz/common/surfaces/subtree_capture_id.h" #include "components/viz/common/surfaces/video_capture_target.h" #include "components/viz/service/display/overdraw_tracker.h" @@ -226,6 +227,8 @@ MaybeEraseHitTestQuery(frame_sink_id); + pending_non_empty_frame_notifications_.erase(frame_sink_id); + // Destroy the [Root]CompositorFrameSinkImpl if there is one. sink_map_.erase(frame_sink_id); root_sink_map_.erase(frame_sink_id); @@ -233,6 +236,56 @@ frame_sink_data_.erase(frame_sink_id); } +void FrameSinkManagerImpl::RequestNonEmptyFrameNotification( + const FrameSinkId& frame_sink_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // The client only arms registered frame sinks, and messages are ordered on + // the same pipe, so registration has already arrived. The request is dropped + // again by InvalidateFrameSinkId(), so it cannot outlive registration. + CHECK(frame_sink_data_.contains(frame_sink_id)); + + pending_non_empty_frame_notifications_.insert(frame_sink_id); + + // Callers are expected to arm before Create(Root)CompositorFrameSink() so + // that no frame can be processed first. Handle a late request anyway, in case + // a non-empty frame has already activated: otherwise the notification would + // never arrive until the client happened to paint again. + // + // This is best effort and does not replace the ordering requirement. It only + // consults the client's current support and its last activated surface, so it + // misses a non-empty frame whose client has since closed its + // CompositorFrameSink, and one that has since been followed by an empty frame + // in a newer LocalSurfaceId. Security sensitive callers must arm first. + auto support_it = support_map_.find(frame_sink_id); + if (support_it == support_map_.end()) { + return; + } + Surface* surface = surface_manager_.GetSurfaceForId( + support_it->second->last_activated_surface_id()); + if (surface && surface->HasActiveFrame() && + surface->GetActiveFrame().HasVisuallyNonEmptyContent()) { + NotifyFirstNonEmptyFrame(frame_sink_id); + } +} + +void FrameSinkManagerImpl::CancelNonEmptyFrameNotification( + const FrameSinkId& frame_sink_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + pending_non_empty_frame_notifications_.erase(frame_sink_id); +} + +void FrameSinkManagerImpl::NotifyFirstNonEmptyFrame( + const FrameSinkId& frame_sink_id) { + // The erase makes this one-shot: subsequent non-empty frames are ignored + // until the client arms another request. + bool erased = pending_non_empty_frame_notifications_.erase(frame_sink_id); + CHECK(erased); + if (client_) { + client_->OnFirstNonEmptyFrame(frame_sink_id); + } +} + void FrameSinkManagerImpl::SetFrameSinkDebugLabel( const FrameSinkId& frame_sink_id, const std::string& debug_label) { ... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/viz/common/quads/compositor_frame_unittest.cc b/components/viz/common/quads/compositor_frame_unittest.cc
new file mode 100644
index 0000000..d534ef3
--- /dev/null
+++ b/components/viz/common/quads/compositor_frame_unittest.cc
@@ -0,0 +1,226 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/viz/common/quads/compositor_frame.h"
+
+#include "base/unguessable_token.h"
+#include "cc/paint/filter_operation.h"
+#include "components/viz/common/quads/compositor_render_pass.h"
+#include "components/viz/common/resources/resource_id.h"
+#include "components/viz/common/surfaces/frame_sink_id.h"
+#include "components/viz/common/surfaces/local_surface_id.h"
+#include "components/viz/common/surfaces/surface_id.h"
+#include "components/viz/common/surfaces/surface_range.h"
+#include "components/viz/test/compositor_frame_helpers.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkColor.h"
+#include "ui/gfx/geometry/rect.h"
+
+namespace viz {
+namespace {
+
+constexpr gfx::Rect kOutputRect(0, 0, 100, 100);
+constexpr gfx::Rect kDamageRect(0, 0, 100, 100);
+constexpr gfx::Rect kSmallRect(0, 0, 10, 10);
+
+SurfaceId MakeSurfaceId() {
+ return SurfaceId(
+ FrameSinkId(1, 1),
+ LocalSurfaceId(1, base::UnguessableToken::CreateForTesting(1u, 2u)));
+}
+
+// A frame with no render passes at all is empty.
+TEST(CompositorFrameTest, NoRenderPassesIsEmpty) {
+ CompositorFrame frame = CompositorFrameBuilder().Build();
+ EXPECT_FALSE(frame.HasVisuallyNonEmptyContent());
+}
+
+// A single render pass with no quads is empty. This is what a client produces
+// before it has painted anything.
+TEST(CompositorFrameTest, EmptyRootRenderPassIsEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder().AddRenderPass(kOutputRect, kDamageRect).Build();
+ EXPECT_FALSE(frame.HasVisuallyNonEmptyContent());
+}
+
+// A single solid color quad is the default background fill, so it is empty.
+TEST(CompositorFrameTest, SingleSolidColorQuadIsEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kWhite))
+ .Build();
+ EXPECT_FALSE(frame.HasVisuallyNonEmptyContent());
+}
+
+// The color of the single solid color quad does not matter; a client may paint
+// a non-white background (e.g. in dark mode) without having painted content.
+TEST(CompositorFrameTest, SingleNonWhiteSolidColorQuadIsEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kBlack))
+ .Build();
+ EXPECT_FALSE(frame.HasVisuallyNonEmptyContent());
+}
+
+// The single solid color quad is not required to cover the whole output_rect.
+// We deliberately do not check coverage or clipping, since a partial background
+// fill still cannot be used to spoof page content, and strict rect checks would
+// risk false positives from viewport sizing and scale rounding quirks. Note
+// that the frame must still damage its whole output_rect, which is what keeps
+// such quads from being combined across frames.
+TEST(CompositorFrameTest, SinglePartialSolidColorQuadWithFullDamageIsEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kSmallRect, SkColors::kRed))
+ .Build();
+ EXPECT_FALSE(frame.HasVisuallyNonEmptyContent());
+}
+
+// A frame that damages less than its output_rect leaves the pixels outside the
+// damage on screen from earlier frames, so a client could paint arbitrary
+// content one rect per frame while every frame looks like a background fill.
+TEST(CompositorFrameTest, PartialDamageIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kWhite)
+ .SetDamageRect(kSmallRect))
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// The same applies without any quads, since partial damage lets a client erase
+// rects back to the embedder's background color, which draws just as well as
+// painting them.
+TEST(CompositorFrameTest, EmptyRootPassWithPartialDamageIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder().AddRenderPass(kOutputRect, kSmallRect).Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// Two solid color quads can be combined into recognizable content, so they are
+// non-empty. Only a single solid color quad is treated as a background fill.
+TEST(CompositorFrameTest, TwoSolidColorQuadsAreNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kWhite)
+ .AddSolidColorQuad(kSmallRect, SkColors::kRed))
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// Any single quad that is not a solid color quad is non-empty, even on its own.
+// This is what keeps quad types that can draw arbitrary content (textures,
+// tiles, pictures, and debug borders) from being mistaken for a background.
+TEST(CompositorFrameTest, SingleTextureQuadIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(
+ RenderPassBuilder(kOutputRect)
+ .AddTextureQuad(kOutputRect, ResourceId(1),
+ TextureQuadParams{.needs_blending = false}))
+ .PopulateResources()
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// A surface quad embeds another client (e.g. an OOPIF, an OffscreenCanvas or a
+// video). This is what prevents a client from hiding content behind a nested
+// frame sink.
+TEST(CompositorFrameTest, SingleSurfaceQuadIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(
+ RenderPassBuilder(kOutputRect)
+ .AddSurfaceQuad(kOutputRect, SurfaceRange(MakeSurfaceId())))
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// More than one render pass implies intermediate passes (effects, filters,
+// masks), which only exist when there is content to apply them to.
+TEST(CompositorFrameTest, MultipleRenderPassesAreNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(
+ RenderPassBuilder(CompositorRenderPassId{2}, kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kWhite))
+ .AddRenderPass(
+ RenderPassBuilder(CompositorRenderPassId{1}, kOutputRect)
+ .AddRenderPassQuad(kOutputRect, CompositorRenderPassId{2}))
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// The single-quad exemption only applies when there is exactly one render pass,
+// so an empty root pass alongside another pass is still non-empty.
+TEST(CompositorFrameTest, EmptyRootPassWithSecondPassIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kWhite))
+ .AddRenderPass(kOutputRect, kDamageRect)
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// A filter on the root pass turns a single background quad into arbitrary
+// content, so it is non-empty despite the single solid color quad. An alpha
+// threshold filter masks the pass to an arbitrary set of rects, which is enough
+// to draw recognizable shapes or text out of one opaque quad.
+TEST(CompositorFrameTest, SingleSolidColorQuadWithFilterIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(
+ RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kBlack)
+ .AddFilter(cc::FilterOperation::CreateOpacityFilter(0.5f)))
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// The same, for the alpha threshold filter specifically, since its region is
+// deserialized from the client and can be an arbitrary union of rects.
+TEST(CompositorFrameTest, SingleSolidColorQuadWithAlphaThresholdIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(
+ RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kBlack)
+ .AddFilter(cc::FilterOperation::CreateAlphaThresholdFilter(
+ cc::FilterOperation::ShapeRects{kSmallRect})))
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// Backdrop filters sample and transform what is already on screen behind the
+// pass, so they can also produce visible content from a single quad.
+TEST(CompositorFrameTest, SingleSolidColorQuadWithBackdropFilterIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder()
+ .AddRenderPass(RenderPassBuilder(kOutputRect)
+ .AddSolidColorQuad(kOutputRect, SkColors::kWhite)
+ .AddBackdropFilter(
+ cc::FilterOperation::CreateInvertFilter(1.0f)))
+ .Build();
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+// A filter with no quads to apply it to still counts as non-empty, rather than
+// relying on the empty quad list alone to decide.
+TEST(CompositorFrameTest, EmptyRootPassWithFilterIsNonEmpty) {
+ CompositorFrame frame =
+ CompositorFrameBuilder().AddRenderPass(kOutputRect, kDamageRect).Build();
+ // Set the filter directly, because RenderPassBuilder requires a quad.
+ frame.render_pass_list.back()->filters.Append(
+ cc::FilterOperation::CreateInvertFilter(1.0f));
+ EXPECT_TRUE(frame.HasVisuallyNonEmptyContent());
+}
+
+} // namespace
+} // namespace viz
diff --git a/components/viz/host/host_frame_sink_manager_unittest.cc b/components/viz/host/host_frame_sink_manager_unittest.cc
index 050047b..89b2290 100644
--- a/components/viz/host/host_frame_sink_manager_unittest.cc
+++ b/components/viz/host/host_frame_sink_manager_unittest.cc
@@ -82,6 +82,10 @@
}
MOCK_METHOD1(MockInvalidateFrameSinkId,
void(const FrameSinkId& frame_sink_id));
+ MOCK_METHOD1(RequestNonEmptyFrameNotification,
+ void(const FrameSinkId& frame_sink_id));
+ MOCK_METHOD1(CancelNonEmptyFrameNotification,
+ void(const FrameSinkId& frame_sink_id));
MOCK_METHOD2(SetFrameSinkDebugLabel,
void(const FrameSinkId& frame_sink_id,
const std::string& debug_label));
@@ -171,6 +175,15 @@
testing::Mock::VerifyAndClearExpectations(&impl());
}
+ // RequestNonEmptyFrameNotification() requires a registered FrameSinkId.
+ void RegisterFrameSinkIdForNonEmptyFrame(const FrameSinkId& frame_sink_id) {
+ EXPECT_CALL(impl(), RegisterFrameSinkId(frame_sink_id,
+ false /* report_activation */));
+ RegisterFrameSinkIdWithFakeClient(frame_sink_id,
+ ReportFirstSurfaceActivation::kNo);
+ FlushHostAndVerifyExpectations();
+ }
+
// Destroys FrameSinkManagerImpl which kills the message pipes.
void KillGpu() { manager_impl_.reset(); }
@@ -601,4 +614,162 @@
EXPECT_FALSE(host().GetVizTouchStatePtr());
}
+// Verifies that the request is forwarded to Viz and that the callback runs when
+// Viz reports the first non-empty frame, at most once.
+TEST_F(HostFrameSinkManagerTest, NonEmptyFrameNotification) {
+ RegisterFrameSinkIdForNonEmptyFrame(kFrameSinkChild1);
+
+ bool callback_called = false;
+ EXPECT_CALL(impl(), RequestNonEmptyFrameNotification(kFrameSinkChild1));
+ host().RequestNonEmptyFrameNotification(
+ kFrameSinkChild1,
+ base::BindLambdaForTesting([&]() { callback_called = true; }));
+ FlushHostAndVerifyExpectations();
+ EXPECT_FALSE(callback_called);
+
+ // The callback runs synchronously when Viz reports the non-empty frame.
+ GetFrameSinkManagerClient()->OnFirstNonEmptyFrame(kFrameSinkChild1);
+ EXPECT_TRUE(callback_called);
+
+ // Any further notification for the same FrameSinkId is ignored.
+ callback_called = false;
+ GetFrameSinkManagerClient()->OnFirstNonEmptyFrame(kFrameSinkChild1);
+ EXPECT_FALSE(callback_called);
+}
+
+// Verifies that a notification for a FrameSinkId with no pending request (e.g.
+// one that was cancelled while the notification was in flight) is dropped.
+TEST_F(HostFrameSinkManagerTest, NonEmptyFrameNotificationWithoutRequest) {
+ GetFrameSinkManagerClient()->OnFirstNonEmptyFrame(kFrameSinkChild1);
+}
+
+// Verifies that a callback which requests another notification for the same
+// FrameSinkId does not corrupt the pending callback map.
+TEST_F(HostFrameSinkManagerTest, NonEmptyFrameNotificationReentrantRequest) {
+ RegisterFrameSinkIdForNonEmptyFrame(kFrameSinkChild1);
... (truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page