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 the LayoutObject 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 the PaintPropertyTree. The PaintPropertyTree is a specialized tree used for painting. Each LayoutObject has one or more FragmentData which holds information about a portion of the LayoutObject and every FragmentData has an ObjectPaintProperties if any paint property nodes are induced on it (e.g. if the fragment has a transform then its ObjectPaintProperties::Transform() points to the TransformPaintPropertyNode representing that transform). These property nodes are stored to the PaintPropertyTree during PrePaint. Notably, the PainPropertyTrees hold raw pointers to the ObjectPaintProperties 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, &current_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:

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 with DumpWithoutCrashing. Surround fragile areas of the codebase like Paint, PrePaint, and other potentially fragile areas like any HeapVector iteration with this assert-scope.

Other References