CVE-2021-21206: Chrome Use-After-Free in Animations
Brendon Tiszka, Chrome
The Basics
Disclosure or Patch Date: Apr 7, 2021
Product: Google Chrome
Advisory: https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop.html
Affected Versions: pre 89.0.4389.114 (likely to M59)
First Patched Version: 89.0.4389.128
Issue/Bug Report: 1196781
Patch CL: https://chromium-review.googlesource.com/c/chromium/src/+/2812000
Bug-Introducing CL: N/A, likely https://codereview.chromium.org/2808013002
Reporter(s): Anonymous
The Code
Proof-of-concept:
<html>
<head>
<script>
function run() {
let div = document.createElement('div');
document.body.appendChild(div);
let animation = div.animate([ {opacity: 0.1} ], 1);
Object.defineProperty(Object.prototype, 'then', {
get: function () {
div.remove();
}
});
animation.ready.then((_)=>{});
animation.pause();
}
</script>
</head>
<body onload="run()"></body>
</html>
Exploit sample: Yes
Did you have access to the exploit sample when doing the analysis? Yes
The Vulnerability
Bug class: use-after-free and unexpected JavaScript callback triggered by a thennable
object
Vulnerability details:
Prerequisites:
- Each DOM node owns a
LayoutObject
.LayoutObjects
form a tree structure that is a close mapping of the DOM tree. LayoutObjects
store information needed for painting and are created by LayoutTreeBuilderForElement::CreateLayoutTree.- The code within
renderer/core/paint
converts theLayoutObject
tree into a rendering format for the compositor. The process is broken up into two parts: PrePaint and Paint. - PrePaint walks the
LayoutObject
tree and builds thePaintPropertyTree
. ThePaintPropertyTree
is a specialized tree used for painting. EachLayoutObject
has one or moreFragmentData
which holds information about a portion of theLayoutObject
and everyFragmentData
has anObjectPaintProperties
if any paint property nodes are induced on it (e.g. if the fragment has a transform then its ObjectPaintProperties::Transform() points to theTransformPaintPropertyNode
representing that transform). These property nodes are stored to thePaintPropertyTree
during PrePaint. Notably, thePainPropertyTrees
hold raw pointers to theObjectPaintProperties
nodes. - Tangentially,
DocumentAnimations::UpdateAnimations
can trigger a synchronous JavaScript callback if there is a queued microtask and the animation has a pending_pause_ (NotifyRead -> CommitPendingPause -> ResolvePromiseMaybeAsync -> PromiseResolve). PromiseResolve has a documented way to trigger synchronous callbacks that has caused many issues.
void LocalFrameView::RunPaintLifecyclePhase(PaintBenchmarkMode benchmark_mode) {
…
ForAllNonThrottledLocalFrameViews(
[this, &total_animations_count, ¤t_frame_had_raf,
&next_frame_has_pending_raf](LocalFrameView& frame_view) {
...
frame_view.GetLayoutView()
->GetDocument()
.GetDocumentAnimations()
.UpdateAnimations(DocumentLifecycle::kPaintClean,
paint_artifact_compositor_.get()); /*** 1 ***/
Document& document = frame_view.GetLayoutView()->GetDocument();
total_animations_count +=
document.GetDocumentAnimations().GetAnimationsCount();
current_frame_had_raf |= document.CurrentFrameHadRAF();
next_frame_has_pending_raf |= document.NextFrameHasPendingRAF();
});
...
if (paint_artifact_compositor_)
paint_artifact_compositor_->ClearPropertyTreeChangedState(); /*** 2 ***/
if (GetPage())
GetPage()->Animator().ReportFrameAnimations(GetCompositorAnimationHost());
}
The bug lies within Paint. UpdateAnimations
[1] can trigger a callback, and within that callback a LayoutObject
that Paint is currently painting can be destroyed by removing the DOM node that owns it, leading to the ObjectDataProperties
and its property nodes being freed. Then within ClearPropertyTreeChangedState
[2] the dangling pointer to ObjectDataProperties::Transform()
is dereferenced leading to a use-after-free.
Note: Synchronous JavaScript execution is not expected within PrePaint nor Paint. There are likely many ways to trigger memory corruption bug beyond this specific use-after-free.
Patch analysis:
The patch wraps UpdateAnimations
with a ScriptForbiddenScope
assert-scope which will cause the JavaScript callback to be executed asynchronously.
Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):
It seems reasonable that it could have been found through variant analysis given that unexpected JavaScript callback triggered by a thennable
object is a fairly well-known bug class within Chrome that commonly causes use-after-frees and iterator-invalidation issues. A vulnerability researcher could have also come across crbug.com/678706 and crbug.com/708887 dug deeper into this area of code.
(Historical/present/future) context of bug:
- crbug.com/678706 and crbug.com/708887
Promise.then
publicly documented in https://bugs.chromium.org/p/chromium/issues/detail?id=663476#c10
The Exploit
(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)
Exploit strategy (or strategies): The exploit modifies the PartitionPage metadata to exploit the UAF and get arbitrary read/write.
Exploit flow: The exploit uses the arbitrary read/write to leak the address of a WASM RWX page and writes to it to get code execution.
Known cases of the same exploit flow: The exploit follows a similar pattern as many other Blink use-after-frees
Part of an exploit chain? N/A
The Next Steps
Variant analysis
Areas/approach for variant analysis (and why):
- Look for other ways to trigger a synchronous callback within PrePaint and Paint.
- Wrap PrePaint and Paint in an psuedo script forbidden assert-scope that crashes on any resolve while fuzzing.
Found variants: N/A
Structural improvements
Ideas to kill the bug class:
Narrowing in on memory corruptions caused by JavaScript callbacks within PrePaint and Paint: wrap RunPrePaintLifecyclePhase
and RunPaintLifecyclePhase
with ScriptForbiddenScope
.
Ideas to mitigate the exploit flow: N/A
Other potential improvements: N/A
0-day detection methods
- Create an assert-scope similar to
ScriptForbiddenScope
that logs withDumpWithoutCrashing
. Surround fragile areas of the codebase like Paint, PrePaint, and other potentially fragile areas like anyHeapVector
iteration with this assert-scope.