CVE-2026-10918
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/viz/service/layers/layer_context_impl_unittest.cc |
modified |
Files Changed
components/viz/service/layers/layer_context_impl.cccomponents/viz/service/layers/layer_context_impl_unittest.cc
Patch
From c00f3a04a7f317448e1d1b545e77f8fb9223fdb7 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Thu, 23 Apr 2026 17:14:14 -0700
Subject: [PATCH] [TreesInViz] set_needs_update_draw_properties() when update fails.
Otherwise, layer tree could be left in an inconsistent state.
TEST=viz_unittests
Bug: 498259721
Change-Id: Icac4d2cc81a32fd0275c49d2293e9f1ad1608476
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7791946
Auto-Submit: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Tzarial <zork@chromium.org>
Reviewed-by: Tzarial <zork@chromium.org>
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619855}
---
diff --git a/components/viz/service/layers/layer_context_impl.cc b/components/viz/service/layers/layer_context_impl.cc
index a806d042..af49f96 100644
--- a/components/viz/service/layers/layer_context_impl.cc
+++ b/components/viz/service/layers/layer_context_impl.cc
@@ -10,6 +10,7 @@
#include <utility>
#include <vector>
+#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
@@ -1851,6 +1852,15 @@
cc::LayerTreeImpl& layers = *host_impl_->active_tree();
cc::PropertyTrees& property_trees = *layers.property_trees();
+ // Any update to the display tree requires a new draw properties update if
+ // validation fails and returns early, because we may have already mutated
+ // some state (like taking render surfaces or resizing trees).
+ base::ScopedClosureRunner cleanup(base::BindOnce(
+ [](cc::LayerTreeImpl* layers) {
+ layers->set_needs_update_draw_properties();
+ },
+ &layers));
+
std::vector<std::unique_ptr<cc::RenderSurfaceImpl>> old_render_surfaces;
property_trees.effect_tree_mutable().TakeRenderSurfaces(&old_render_surfaces);
@@ -2209,6 +2219,7 @@
layers.MoveChangeTrackingToLayers();
}
+ cleanup.ReplaceClosure(base::DoNothing());
return base::ok();
}
diff --git a/components/viz/service/layers/layer_context_impl_unittest.cc b/components/viz/service/layers/layer_context_impl_unittest.cc
index 22ee827c..4ee162f2 100644
--- a/components/viz/service/layers/layer_context_impl_unittest.cc
+++ b/components/viz/service/layers/layer_context_impl_unittest.cc
@@ -18,6 +18,7 @@
#include "cc/layers/nine_patch_layer_impl.h"
#include "cc/layers/nine_patch_thumb_scrollbar_layer_impl.h"
#include "cc/layers/painted_scrollbar_layer_impl.h"
+#include "cc/layers/render_surface_impl.h"
#include "cc/layers/solid_color_scrollbar_layer_impl.h"
#include "cc/layers/surface_layer_impl.h"
#include "cc/layers/texture_layer_impl.h"
@@ -4837,5 +4838,43 @@
EXPECT_TRUE(result.has_value()) << (result.has_value() ? "" : result.error());
}
+TEST_F(LayerContextImplTest, DoUpdateDisplayTreeEarlyReturnUAF) {
+ // 1. Initial valid update to set up the tree and populate
+ // render_surface_list_.
+ auto update = CreateDefaultUpdate();
+ // The secondary root effect node created in ResetTestState() already has
+ // kRoot reason.
+
+ auto result = layer_context_impl_->DoUpdateDisplayTree(std::move(update));
+ ASSERT_TRUE(result.has_value());
+
+ // Trigger a draw to populate render_surface_list_.
+ layer_context_impl_->host_impl()->active_tree()->UpdateDrawProperties(
+ /*update_tiles=*/true, /*update_image_animation_controller=*/true);
+
+ const auto& render_surface_list =
+ layer_context_impl_->host_impl()->active_tree()->GetRenderSurfaceList();
+ ASSERT_FALSE(render_surface_list.empty());
+
+ // 2. A failing update that returns early in DoUpdateDisplayTree after
+ // TakeRenderSurfaces.
+ auto update2 = CreateDefaultUpdate();
+ // We can make UpdateTransformTreeProperties fail by setting an invalid
+ // page_scale_factor.
+ update2->transform_tree_update = mojom::TransformTreeUpdate::New();
+ update2->transform_tree_update->page_scale_factor = -1.0f;
+
+ auto result2 = layer_context_impl_->DoUpdateDisplayTree(std::move(update2));
+ ASSERT_FALSE(result2.has_value());
+ EXPECT_EQ(result2.error(), "Invalid page_scale_factor");
+
+ // 3. Check if needs_update_draw_properties() is true.
+ // If it's false, we have a UAF potential because render_surface_list still
+ // has pointers to freed surfaces.
+ EXPECT_TRUE(layer_context_impl_->host_impl()
+ ->active_tree()
+ ->needs_update_draw_properties());
+}
+
} // namespace
} // namespace viz
Regression Test / PoC
diff --git a/components/viz/service/layers/layer_context_impl_unittest.cc b/components/viz/service/layers/layer_context_impl_unittest.cc
index 22ee827c..4ee162f2 100644
--- a/components/viz/service/layers/layer_context_impl_unittest.cc
+++ b/components/viz/service/layers/layer_context_impl_unittest.cc
@@ -18,6 +18,7 @@
#include "cc/layers/nine_patch_layer_impl.h"
#include "cc/layers/nine_patch_thumb_scrollbar_layer_impl.h"
#include "cc/layers/painted_scrollbar_layer_impl.h"
+#include "cc/layers/render_surface_impl.h"
#include "cc/layers/solid_color_scrollbar_layer_impl.h"
#include "cc/layers/surface_layer_impl.h"
#include "cc/layers/texture_layer_impl.h"
@@ -4837,5 +4838,43 @@
EXPECT_TRUE(result.has_value()) << (result.has_value() ? "" : result.error());
}
+TEST_F(LayerContextImplTest, DoUpdateDisplayTreeEarlyReturnUAF) {
+ // 1. Initial valid update to set up the tree and populate
+ // render_surface_list_.
+ auto update = CreateDefaultUpdate();
+ // The secondary root effect node created in ResetTestState() already has
+ // kRoot reason.
+
+ auto result = layer_context_impl_->DoUpdateDisplayTree(std::move(update));
+ ASSERT_TRUE(result.has_value());
+
+ // Trigger a draw to populate render_surface_list_.
+ layer_context_impl_->host_impl()->active_tree()->UpdateDrawProperties(
+ /*update_tiles=*/true, /*update_image_animation_controller=*/true);
+
+ const auto& render_surface_list =
+ layer_context_impl_->host_impl()->active_tree()->GetRenderSurfaceList();
+ ASSERT_FALSE(render_surface_list.empty());
+
+ // 2. A failing update that returns early in DoUpdateDisplayTree after
+ // TakeRenderSurfaces.
+ auto update2 = CreateDefaultUpdate();
+ // We can make UpdateTransformTreeProperties fail by setting an invalid
+ // page_scale_factor.
+ update2->transform_tree_update = mojom::TransformTreeUpdate::New();
+ update2->transform_tree_update->page_scale_factor = -1.0f;
+
+ auto result2 = layer_context_impl_->DoUpdateDisplayTree(std::move(update2));
+ ASSERT_FALSE(result2.has_value());
+ EXPECT_EQ(result2.error(), "Invalid page_scale_factor");
+
+ // 3. Check if needs_update_draw_properties() is true.
+ // If it's false, we have a UAF potential because render_surface_list still
+ // has pointers to freed surfaces.
+ EXPECT_TRUE(layer_context_impl_->host_impl()
+ ->active_tree()
+ ->needs_update_draw_properties());
+}
+
} // namespace
} // namespace viz
Original Bug Report
UAF in GPU process via LayerContextImpl::DoUpdateDisplayTree aborted update
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A Use-After-Free (UAF) vulnerability exists in the GPU process when kTreesInViz is enabled. LayerContextImpl resizes property trees, freeing RenderSurfaceImpl objects, but can abort early on validation errors without updating needs_update_draw_properties_, leaving dangling pointers in LayerTreeImpl’s render surface list.
Affected files:
components/viz/service/layers/layer_context_impl.cccc/trees/damage_tracker.cccc/trees/layer_tree_impl.cccc/trees/property_tree.cc
Estimated timestamp from git blame: 2025-06-04
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in the GPU process within the LayerContextImpl component, affecting the kTreesInViz code path. The root cause is that DoUpdateDisplayTree performs irreversible state mutations—specifically shrinking property trees and freeing RenderSurfaceImpl objects—before completing the validation of the display tree update.
If validation fails subsequently (e.g., due to an invalid page_scale_factor), the update is aborted early. This early return bypasses the logic that sets needs_update_draw_properties_ = true. Consequently, LayerTreeImpl is left in a “torn” state where its render_surface_list_ (which uses RAW_PTR_EXCLUSION) contains raw pointers to the now-freed RenderSurfaceImpl heap allocations. A subsequent draw operation will dereference these dangling pointers.
Potential Exploitation Steps
Note: These are suggested steps based on static analysis. Our tooling agent does not yet have the ability to run code to provide a working proof of concept.
- Initial Setup: A compromised Renderer process establishes a valid
mojom::LayerContextconnection to the GPU process. - Clear Draw Flag: The Renderer sends a valid
mojom::LayerTreeUpdatewithframe_has_damage = true. This forces a successful draw cycle, which setsLayerTreeImpl::needs_update_draw_properties_tofalseand populatesrender_surface_list_with valid pointers. - Trigger the Free: The Renderer crafts a malicious
LayerTreeUpdatewhereupdate->num_effect_nodesis smaller than the current size.DoUpdateDisplayTreecallsResizePropertyTree, which physically removes effect nodes and frees their associatedRenderSurfaceImplobjects. - Heap Grooming: The attacker includes a large array in
update->surface_ranges. The allocation of thebase::flat_setbacking vector for these ranges lands in the recently freedRenderSurfaceImplheap slots. The attacker fills this array with crafted bytes representing a fakeRenderSurfaceImplobject (including a forgeddamage_tracker_pointer). - Trigger the Abort: The attacker sets
update->page_scale_factortoNaN. Validation fails atlayer_context_impl.cc:1976, causing an early return.needs_update_draw_properties_remainsfalse. - Trigger the Draw: The early return calls
ReportBadMessage, which queues an asynchronous Mojo teardown task. Before it runs, the attacker sendsSetNeedsBeginFrame(true)via theCompositorFrameSinkinterface on the same pipe. This schedules a localBeginFrametask on the Viz thread. - The UAF: The
BeginFrametask executes first, callingLayerTreeHostImpl::PrepareToDraw. Becauseneeds_update_draw_properties_isfalse, it skips recalculating the draw properties and keeps the stalerender_surface_list_. - RCE Primitives:
PrepareToDrawcallsDamageTracker::UpdateDamageTracking, which iterates the stale list and executesrender_surface->damage_tracker()->PrepareForUpdate(). Using the attacker’s forgeddamage_tracker_pointer,PrepareForUpdate()executes with a controlledthiscontext. This provides the attacker with an arbitrary 32-bit increment primitive (mailbox_id_++) and an arbitrary vector clear primitive (contributing_surfaces_.clear()), which can be chained to achieve Remote Code Execution (RCE) in the GPU process.
Suggested Fix
Ensure that the needs_update_draw_properties_ flag is set to true (or the render_surface_list_ is explicitly cleared) whenever property trees are physically mutated, regardless of whether the overall DoUpdateDisplayTree function succeeds or fails.
For example, ResizePropertyTree could accept the LayerTreeImpl reference and call set_needs_update_draw_properties() internally if it returns true. Alternatively, DoUpdateDisplayTree could use a base::ScopedClosureRunner to ensure state cleanup on early returns.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.