CVE-2026-85051
Overview
Background
- `DCHECK` vs `CHECK`
DCHECK(andDCHECK_EQ) are assertions compiled out of release builds, so their conditions are only enforced in debug builds, whereasCHECK/CHECK_EQare always evaluated and abort the process on failure.- Downcast helpers (`ToScrollTimeline`, `ToWorkletAnimation`, `ToTimelineTrigger`, `ToFilterAnimationCurve`, `ToScrollOffsetAnimationCurve`)
- inline helpers in
cc/animation/that take a base-class pointer (e.g.AnimationTimeline*,Animation*,AnimationTrigger*,gfx::AnimationCurve*) andstatic_castit to a concrete derived type after a type-tag check. - Type tag methods (`IsScrollTimeline`, `IsWorkletAnimation`, `IsTimelineTrigger`, `Type()`)
- virtual/runtime predicates that report the real dynamic type of an animation object so a downcast can be validated before it is trusted.
- `AnimationTrigger` / `TimelineTrigger` / `ScrollTimeline`
cccompositor animation objects; aTimelineTriggerfires based on an associated timeline, and itstimeline_member is expected to be aScrollTimelinewhen the trigger is updated.
Root Cause Analysis
Several cc/animation/ downcasts guarded the transition from a base pointer to a concrete derived type with DCHECK/DCHECK_EQ, which are stripped from release builds; the compiled shipping binary therefore performed the static_cast (and, in TimelineTrigger::Update, a reinterpret_cast<ScrollTimeline*>) with no runtime verification of the object’s real type. The most direct sink is TimelineTrigger::Update, where timeline_.Read(*this).get() was reinterpret_cast to ScrollTimeline* and only checked for non-null via DCHECK, never for actually being a ScrollTimeline; if the stored AnimationTimeline were some other subtype, subsequent calls such as IsActive(...) operate on a mis-typed object — classic type confusion. Similarly AnimationHost::UpdateTriggers asserted trigger->IsTimelineTrigger() with a DCHECK and then static_cast<TimelineTrigger*>, so a non-TimelineTrigger would be treated as one in release. The invariant violated is that a downcast must match the object’s dynamic type before the derived pointer is dereferenced.
The fix promotes every one of these assertions to CHECK/CHECK_EQ (and routes TimelineTrigger::Update through the type-checking ToScrollTimeline helper, plus introduces ToTimelineTrigger), so a type mismatch now aborts safely instead of silently proceeding with a confused pointer. This tightens the checks — turning debug-only assumptions into always-enforced guarantees rather than relaxing anything.
DCHECK-family assertions — absent from release builds — to validate downcasts, leaving the shipping compositor to static_cast/reinterpret_cast base pointers to derived types without any runtime type check. The fix promotes those assertions to always-on CHECKs so a mismatched type deterministically crashes instead of producing a type-confused object.Attack Path
- Set up an animation object graph Through web-exposed animation/scroll-timeline APIs, an attacker arranges for a compositor animation structure (a trigger, timeline, animation, or curve) whose stored base pointer does not have the concrete type the downcast helper assumes. (inferred)
- Reach the unchecked downcast
Compositor animation updates invoke a path such as
AnimationHost::UpdateTriggers→TimelineTrigger::Update, where the base pointer is cast toTimelineTrigger*/ScrollTimeline*with only a release-strippedDCHECKguarding the type. - Trigger type confusion
In a release build the cast proceeds regardless of the real type, and subsequent member access (e.g.
ScrollTimeline::IsActive) reads/writes through a pointer to an object of the wrong type, yielding a type-confused access. (inferred) - Escalate the confused access A controlled type mismatch can turn into an out-of-bounds or otherwise corrupt memory access exploitable for further compromise in the process hosting compositing. (inferred)
Impact Assessment
cc animation code. Exploitation requires driving the animation/timeline machinery into a state where a downcast target’s dynamic type differs from the expected one. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8. A specific severity rating are not stated in the material provided.Changed Functions
| Function | Change | Notes |
|---|---|---|
ToScrollOffsetAnimationCurvecc/animation/scroll_offset_animation_curve.cc |
modified | |
TEST_Fcc/animation/scroll_timeline_unittest.cc |
modified | |
TEST_Fcc/animation/worklet_animation_unittest.cc |
modified |
Files Changed
cc/animation/worklet_animation_unittest.cccc/animation/timeline_trigger.cccc/animation/timeline_trigger.hcc/animation/scroll_timeline_unittest.cccc/animation/worklet_animation.hcc/animation/scroll_offset_animation_curve.cccc/animation/filter_animation_curve.cccc/animation/scroll_timeline.hcc/animation/animation_host.cc
Audit Directions
- `DCHECK`-guarded downcastsSearch
cc/and other renderer/compositor code forDCHECK/DCHECK_EQimmediately preceding astatic_cast/reinterpret_castto a derived type; these are release-build type confusions waiting to happen and should use always-onCHECKhelpers. - `reinterpret_cast` between polymorphic typesFlag any
reinterpret_castfrom a base animation/timeline/curve pointer to a derived type (it does no type or offset adjustment), especially when only a non-null assertion follows; prefer type-checkedstatic_casthelpers. - Type-tag predicates not enforced at the cast siteAudit call sites of
Is*()/Type()predicates to confirm the predicate is actually checked (withCHECK) before the corresponding downcast is dereferenced, rather than assumed by an upstream invariant.
Patch
From deb41ddc5770bd74cc7d0e4d6a7d7b12e2c5ecec Mon Sep 17 00:00:00 2001 From: Robert Flack <[email protected]> Date: Fri, 28 Aug 2026 13:20:18 -0700 Subject: [PATCH] Promote DCHECK to CHECK in cc animation downcasts Promotes `DCHECK` downcast assertions to `CHECK` across `cc/animation/` downcasting helpers (`ToScrollTimeline`, `ToWorkletAnimation`, `ToScrollOffsetAnimationCurve`, and `ToFilterAnimationCurve`). Introduces `ToTimelineTrigger` helper functions with `CHECK` verification and uses them in `AnimationHost::UpdateTriggers`. This ensures downcasts between base and derived animation, timeline, trigger, and curve instances are always validated. Bug: 553449113 Change-Id: I06849cde7d144966b89ec2899eebef5f5a66fee0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8308916 Reviewed-by: David A <[email protected]> Commit-Queue: Robert Flack <[email protected]> Cr-Commit-Position: refs/heads/main@{#1688263} --- diff --git a/cc/animation/animation_host.cc b/cc/animation/animation_host.cc index 0a5e0d6..8514063 100644 --- a/cc/animation/animation_host.cc +++ b/cc/animation/animation_host.cc @@ -1091,9 +1091,7 @@ for (const auto& kv : id_to_trigger_map_.Read(*this)) { AnimationTrigger* trigger = kv.second.get(); // NOTE(crbug.com/451238244): Only timeline triggers are supported for now. - DCHECK(trigger->IsTimelineTrigger()); - static_cast<TimelineTrigger*>(trigger)->Update(scroll_tree, events, - monotonic_time); + ToTimelineTrigger(trigger)->Update(scroll_tree, events, monotonic_time); } } diff --git a/cc/animation/filter_animation_curve.cc b/cc/animation/filter_animation_curve.cc index 8a01901f..003a1f43a 100644 --- a/cc/animation/filter_animation_curve.cc +++ b/cc/animation/filter_animation_curve.cc @@ -4,6 +4,7 @@ #include "cc/animation/filter_animation_curve.h" +#include "base/check_op.h" #include "base/memory/ptr_util.h" #include "ui/gfx/animation/keyframe/keyframed_animation_curve-inl.h" @@ -30,13 +31,13 @@ const FilterAnimationCurve* FilterAnimationCurve::ToFilterAnimationCurve( const gfx::AnimationCurve* c) { - DCHECK_EQ(gfx::AnimationCurve::FILTER, c->Type()); + CHECK_EQ(gfx::AnimationCurve::FILTER, c->Type()); return static_cast<const FilterAnimationCurve*>(c); } FilterAnimationCurve* FilterAnimationCurve::ToFilterAnimationCurve( gfx::AnimationCurve* c) { - DCHECK_EQ(AnimationCurve::FILTER, c->Type()); + CHECK_EQ(AnimationCurve::FILTER, c->Type()); return static_cast<FilterAnimationCurve*>(c); } diff --git a/cc/animation/scroll_offset_animation_curve.cc b/cc/animation/scroll_offset_animation_curve.cc index f2d5475..3e57ae3 100644 --- a/cc/animation/scroll_offset_animation_curve.cc +++ b/cc/animation/scroll_offset_animation_curve.cc @@ -412,13 +412,13 @@ const ScrollOffsetAnimationCurve* ScrollOffsetAnimationCurve::ToScrollOffsetAnimationCurve( const AnimationCurve* c) { - DCHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type()); + CHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type()); return static_cast<const ScrollOffsetAnimationCurve*>(c); } ScrollOffsetAnimationCurve* ScrollOffsetAnimationCurve::ToScrollOffsetAnimationCurve(AnimationCurve* c) { - DCHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type()); + CHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type()); return static_cast<ScrollOffsetAnimationCurve*>(c); } diff --git a/cc/animation/scroll_timeline.h b/cc/animation/scroll_timeline.h index 8d350fc..d6218ad 100644 --- a/cc/animation/scroll_timeline.h +++ b/cc/animation/scroll_timeline.h @@ -8,6 +8,7 @@ #include <optional> #include <vector> +#include "base/check.h" #include "base/time/time.h" #include "cc/animation/animation_export.h" #include "cc/animation/animation_timeline.h" @@ -199,13 +200,13 @@ }; inline ScrollTimeline* ToScrollTimeline(AnimationTimeline* timeline) { - DCHECK(timeline->IsScrollTimeline()); + CHECK(timeline->IsScrollTimeline()); return static_cast<ScrollTimeline*>(timeline); } inline const ScrollTimeline* ToScrollTimeline( const AnimationTimeline* timeline) { - DCHECK(timeline->IsScrollTimeline()); + CHECK(timeline->IsScrollTimeline()); return static_cast<const ScrollTimeline*>(timeline); } diff --git a/cc/animation/timeline_trigger.cc b/cc/animation/timeline_trigger.cc index c1c915ba..bc770f2 100644 --- a/cc/animation/timeline_trigger.cc +++ b/cc/animation/timeline_trigger.cc @@ -68,8 +68,7 @@ AnimationEvents* events, base::TimeTicks monotonic_time) { ScrollTimeline* scroll_timeline = - reinterpret_cast<ScrollTimeline*>(timeline_.Read(*this).get()); - DCHECK(scroll_timeline); + ToScrollTimeline(timeline_.Read(*this).get()); // Triggers only function based on the active tree. if (!scroll_timeline->IsActive(scroll_tree, /*is_active_tree=*/true)) { diff --git a/cc/animation/timeline_trigger.h b/cc/animation/timeline_trigger.h index 13a0203..5674704 100644 --- a/cc/animation/timeline_trigger.h +++ b/cc/animation/timeline_trigger.h @@ -5,6 +5,7 @@ #ifndef CC_ANIMATION_TIMELINE_TRIGGER_H_ #define CC_ANIMATION_TIMELINE_TRIGGER_H_ +#include "base/check.h" #include "cc/animation/animation_trigger.h" namespace cc { @@ -71,6 +72,17 @@ State state_ = State::kIdle; }; +inline TimelineTrigger* ToTimelineTrigger(AnimationTrigger* trigger) { + CHECK(trigger->IsTimelineTrigger()); + return static_cast<TimelineTrigger*>(trigger); +} + +inline const TimelineTrigger* ToTimelineTrigger( + const AnimationTrigger* trigger) { + CHECK(trigger->IsTimelineTrigger()); + return static_cast<const TimelineTrigger*>(trigger); +} + } // namespace cc #endif // CC_ANIMATION_TIMELINE_TRIGGER_H_ diff --git a/cc/animation/worklet_animation.h b/cc/animation/worklet_animation.h index e4323e7..1b4c5e4 100644 --- a/cc/animation/worklet_animation.h +++ b/cc/animation/worklet_animation.h @@ -9,6 +9,7 @@ #include <optional> #include <string> +#include "base/check.h" #include "base/time/time.h" #include "cc/animation/animation.h" #include "cc/animation/animation_export.h" @@ -154,10 +155,15 @@ }; inline WorkletAnimation* ToWorkletAnimation(Animation* animation) { - DCHECK(animation->IsWorkletAnimation()); + CHECK(animation->IsWorkletAnimation()); return static_cast<WorkletAnimation*>(animation); } +inline const WorkletAnimation* ToWorkletAnimation(const Animation* animation) { + CHECK(animation->IsWorkletAnimation()); + return static_cast<const WorkletAnimation*>(animation); +} + } // namespace cc #endif // CC_ANIMATION_WORKLET_ANIMATION_H_ diff --git a/cc/animation/scroll_timeline_unittest.cc b/cc/animation/scroll_timeline_unittest.cc index 9824011f..d06aad59 100644 --- a/cc/animation/scroll_timeline_unittest.cc +++ b/cc/animation/scroll_timeline_unittest.cc @@ -437,4 +437,22 @@ active_timeline->IsActive(scroll_tree(), true /*is_active_tree*/)); } +TEST_F(ScrollTimelineTest, ToScrollTimeline) { + ScrollTimeline::ScrollOffsets scroll_offsets(0, 100); + scoped_refptr<ScrollTimeline> scroll_timeline = ScrollTimeline::Create( + scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets); + scoped_refptr<AnimationTimeline> plain_timeline = + AnimationTimeline::Create(1); + + EXPECT_EQ(ToScrollTimeline(scroll_timeline.get()), scroll_timeline.get()); + const AnimationTimeline* const_scroll_timeline = scroll_timeline.get(); + EXPECT_EQ(ToScrollTimeline(const_scroll_timeline), scroll_timeline.get()); + +#if GTEST_HAS_DEATH_TEST + EXPECT_DEATH_IF_SUPPORTED(ToScrollTimeline(plain_timeline.get()), ""); + const AnimationTimeline* const_plain_timeline = plain_timeline.get(); + EXPECT_DEATH_IF_SUPPORTED(ToScrollTimeline(const_plain_timeline), ""); +#endif +} + } // namespace cc diff --git a/cc/animation/worklet_animation_unittest.cc b/cc/animation/worklet_animation_unittest.cc index 61ec54e5..bdd8c16 100644 --- a/cc/animation/worklet_animation_unittest.cc +++ b/cc/animation/worklet_animation_unittest.cc @@ -548,6 +548,26 @@ EXPECT_EQ(input->updated_animations.size(), 1u); } +TEST_F(WorkletAnimationTest, ToWorkletAnimation) { + scoped_refptr<WorkletAnimation> worklet_animation = + WorkletAnimation::Create(worklet_animation_id_, "test_name", 1.0, + /* options */ nullptr, + /* effect_timings */ nullptr); + scoped_refptr<Animation> plain_animation = Animation::Create(1); + + EXPECT_EQ(ToWorkletAnimation(worklet_animation.get()), + worklet_animation.get()); + const Animation* const_worklet_animation = worklet_animation.get(); + EXPECT_EQ(ToWorkletAnimation(const_worklet_animation), + worklet_animation.get()); + +#if GTEST_HAS_DEATH_TEST + EXPECT_DEATH_IF_SUPPORTED(ToWorkletAnimation(plain_animation.get()), ""); + const Animation* const_plain_animation = plain_animation.get(); + EXPECT_DEATH_IF_SUPPORTED(ToWorkletAnimation(const_plain_animation), ""); +#endif +} + } // namespace } // namespace cc
Regression Test / PoC
diff --git a/cc/animation/scroll_timeline_unittest.cc b/cc/animation/scroll_timeline_unittest.cc
index 9824011f..d06aad59 100644
--- a/cc/animation/scroll_timeline_unittest.cc
+++ b/cc/animation/scroll_timeline_unittest.cc
@@ -437,4 +437,22 @@
active_timeline->IsActive(scroll_tree(), true /*is_active_tree*/));
}
+TEST_F(ScrollTimelineTest, ToScrollTimeline) {
+ ScrollTimeline::ScrollOffsets scroll_offsets(0, 100);
+ scoped_refptr<ScrollTimeline> scroll_timeline = ScrollTimeline::Create(
+ scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
+ scoped_refptr<AnimationTimeline> plain_timeline =
+ AnimationTimeline::Create(1);
+
+ EXPECT_EQ(ToScrollTimeline(scroll_timeline.get()), scroll_timeline.get());
+ const AnimationTimeline* const_scroll_timeline = scroll_timeline.get();
+ EXPECT_EQ(ToScrollTimeline(const_scroll_timeline), scroll_timeline.get());
+
+#if GTEST_HAS_DEATH_TEST
+ EXPECT_DEATH_IF_SUPPORTED(ToScrollTimeline(plain_timeline.get()), "");
+ const AnimationTimeline* const_plain_timeline = plain_timeline.get();
+ EXPECT_DEATH_IF_SUPPORTED(ToScrollTimeline(const_plain_timeline), "");
+#endif
+}
+
} // namespace cc
diff --git a/cc/animation/worklet_animation_unittest.cc b/cc/animation/worklet_animation_unittest.cc
index 61ec54e5..bdd8c16 100644
--- a/cc/animation/worklet_animation_unittest.cc
+++ b/cc/animation/worklet_animation_unittest.cc
@@ -548,6 +548,26 @@
EXPECT_EQ(input->updated_animations.size(), 1u);
}
+TEST_F(WorkletAnimationTest, ToWorkletAnimation) {
+ scoped_refptr<WorkletAnimation> worklet_animation =
+ WorkletAnimation::Create(worklet_animation_id_, "test_name", 1.0,
+ /* options */ nullptr,
+ /* effect_timings */ nullptr);
+ scoped_refptr<Animation> plain_animation = Animation::Create(1);
+
+ EXPECT_EQ(ToWorkletAnimation(worklet_animation.get()),
+ worklet_animation.get());
+ const Animation* const_worklet_animation = worklet_animation.get();
+ EXPECT_EQ(ToWorkletAnimation(const_worklet_animation),
+ worklet_animation.get());
+
+#if GTEST_HAS_DEATH_TEST
+ EXPECT_DEATH_IF_SUPPORTED(ToWorkletAnimation(plain_animation.get()), "");
+ const Animation* const_plain_animation = plain_animation.get();
+ EXPECT_DEATH_IF_SUPPORTED(ToWorkletAnimation(const_plain_animation), "");
+#endif
+}
+
} // namespace
} // namespace cc
Original Bug Report
OOB heap write in ScrollTimeline::PushPropertiesTo via timeline ID wraparound
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A 32-bit integer wraparound in AnimationIdProvider::NextTimelineId() can lead to ID collisions between a main-thread cc::ScrollTimeline and a stale impl-thread cc::AnimationTimeline. In release builds, ScrollTimeline::PushPropertiesTo performs an unchecked downcast of the smaller base class instance, writing derived member fields past the bounds of the allocation. This leads to a potential out-of-bounds heap write into adjacent PartitionAlloc slots on the renderer compositor thread.
Affected files:
cc/animation/scroll_timeline.cccc/animation/scroll_timeline.hcc/animation/animation_host.cccc/animation/animation_id_provider.cc
Estimated timestamp from git blame: 2020-01-21
Summary
A 32-bit integer wraparound in cc::AnimationIdProvider can result in type confusion between cc::ScrollTimeline and cc::AnimationTimeline during main-to-impl property synchronization, leading to an out-of-bounds (OOB) heap write in the renderer process.
Root Cause Analysis
In cc/animation/animation_id_provider.cc:12, 26-28, timeline IDs are generated from a process-global 32-bit atomic counter without wraparound protection:
base::AtomicSequenceNumber g_next_timeline_id;
int AnimationIdProvider::NextTimelineId() {
return g_next_timeline_id.GetNext() + 1;
}
Because base::AtomicSequenceNumber wraps on 32-bit integer overflow (base/atomic_sequence_num.h:23-26), repeated increments can cause timeline IDs to repeat.
In cc/animation/animation_host.cc, synchronization between the main thread and compositor (impl) thread relies entirely on matching integer IDs without validating timeline types:
PushTimelinesToImplThread(cc/animation/animation_host.cc:458-468) skips instantiating a new impl instance if any timeline with the matching ID already exists onhost_impl:
const AnimationTimeline* timeline_impl = host_impl->GetTimelineById(timeline->id());
if (timeline_impl) continue;
RemoveTimelinesFromImplThread(cc/animation/animation_host.cc:483-501) preserves existing impl instances as long as the ID exists in the main-thread map:
if (timeline_impl->is_impl_only() || GetTimelineById(timeline_impl->id())) {
++it;
}
PushPropertiesToImplThread(cc/animation/animation_host.cc:516-528) retrievestimeline_implsolely by ID and uses virtual dispatch on the main thread object:
if (AnimationTimeline* timeline_impl = host_impl->GetTimelineById(timeline->id())) {
timeline->PushPropertiesTo(timeline_impl);
}
When a main-thread cc::ScrollTimeline collides with a stale plain cc::AnimationTimeline on the impl thread, ScrollTimeline::PushPropertiesTo is called (cc/animation/scroll_timeline.cc:152-161):
void ScrollTimeline::PushPropertiesTo(AnimationTimeline* impl_timeline) {
AnimationTimeline::PushPropertiesTo(impl_timeline);
DCHECK(impl_timeline);
ScrollTimeline* scroll_timeline = ToScrollTimeline(impl_timeline);
scroll_timeline->pending_id_.Write(*this) = pending_id_.Read(*this);
scroll_timeline->pending_direction_.Write(*this) = pending_direction_.Read(*this);
scroll_timeline->pending_offsets_.Write(*this) = pending_offsets_.Read(*this);
scroll_timeline->last_tick_time_.Write(*scroll_timeline) = std::nullopt;
}
In release builds, ToScrollTimeline (cc/animation/scroll_timeline.h:201-204) executes only a DCHECK(timeline->IsScrollTimeline()), which is stripped. The subsequent static_cast treats the ~80-byte cc::AnimationTimeline allocation as a ~192-byte cc::ScrollTimeline. The four member assignments (pending_id_, pending_direction_, pending_offsets_, and last_tick_time_) write to offsets ~96–192, causing an out-of-bounds write past the base object’s PartitionAlloc slot into adjacent heap memory.
Potential Attack / Reproduction Scenario
Note: These are suggested/potential steps derived from source code analysis; our automated tooling does not currently have the capability to execute live code.
- Plant Plain Impl Timelines: The attacker page loads same-origin iframes running composited animations (e.g. opacity animation on a
will-change: transformelement). This causes Blink to constructDocumentTimelineinstances (third_party/blink/renderer/core/animation/document_timeline.cc:221-228) that register plaincc::AnimationTimelineinstances (IDs $N \dots N+k$) onto the sharedcc::AnimationHost. A commit pushes these intohost_implas plaincc::AnimationTimelineallocations (~80–96 bytes). - Pump Sequence Counter: The attacker runs a loop calling
new DocumentTimeline()in JavaScript. Each call invokesEnsureCompositorTimeline()and consumes one ID fromAnimationIdProvider::NextTimelineId(). Because unreferencedDocumentTimelineobjects are only weakly referenced inDocumentAnimations::timelines_(third_party/blink/renderer/core/animation/document_animations.h:168), they are reclaimed by Oilpan GC with minimal memory overhead until the 32-bit counter wraps around back to range $[N, N+k]$. - Induce Collision: In a single JavaScript task, the attacker removes the planted iframes (removing IDs $N \dots N+k$ from the main-thread
id_to_timeline_map_viaDocument::Shutdown) and creates CSS scroll-driven animations (animation-timeline: scroll()). Each scroll timeline creates acc::ScrollTimelineassigned a colliding ID in $[N, N+k]$. - Trigger OOB Write: On the next commit,
AnimationHost::PushPropertiesToruns on the compositor thread.PushTimelinesToImplThreadskips recreating the impl timeline due to the ID match,RemoveTimelinesFromImplThreadretains the stale plain timeline, andPushPropertiesToImplThreadinvokesScrollTimeline::PushPropertiesToon the stale plainAnimationTimeline*. This writes derived member fields out of bounds into adjacent PartitionAlloc slots.
Suggested Fix
- Use 64-bit IDs: Change
g_next_timeline_idand timeline IDs from 32-bit signed integers to 64-bit types (e.g.,uint64_torbase::IdType64<...>) to make ID wraparound practically impossible. - Validate Subtype on Sync: In
AnimationHost::PushTimelinesToImplThreadandAnimationHost::RemoveTimelinesFromImplThread, check whethertimeline->IsScrollTimeline() == timeline_impl->IsScrollTimeline(). If a type mismatch occurs for the same ID, recreate the impl instance. - Harden Downcast: Replace
DCHECK(timeline->IsScrollTimeline())inToScrollTimeline(cc/animation/scroll_timeline.h:202, 208) with a productionCHECKor use dynamic type querying.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.