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.LayoutObjectsform a tree structure that is a close mapping of the DOM tree.
- LayoutObjectsstore information needed for painting and are created by LayoutTreeBuilderForElement::CreateLayoutTree.
- The code within renderer/core/paintconverts theLayoutObjecttree into a rendering format for the compositor. The process is broken up into two parts: PrePaint and Paint.
- PrePaint walks the LayoutObjecttree and builds thePaintPropertyTree. ThePaintPropertyTreeis a specialized tree used for painting. EachLayoutObjecthas one or moreFragmentDatawhich holds information about a portion of theLayoutObjectand everyFragmentDatahas anObjectPaintPropertiesif any paint property nodes are induced on it (e.g. if the fragment has a transform then its ObjectPaintProperties::Transform() points to theTransformPaintPropertyNoderepresenting that transform). These property nodes are stored to thePaintPropertyTreeduring PrePaint. Notably, thePainPropertyTreeshold raw pointers to theObjectPaintPropertiesnodes.
- Tangentially, DocumentAnimations::UpdateAnimationscan 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.thenpublicly 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 ScriptForbiddenScopethat logs withDumpWithoutCrashing. Surround fragile areas of the codebase like Paint, PrePaint, and other potentially fragile areas like anyHeapVectoriteration with this assert-scope.