CINXE.COM
CVE-2021-4102: Chrome incorrect node elision in Turbofan leads to unexpected WriteBarrier elision | 0-days In-the-Wild
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"><!-- Begin Jekyll SEO tag v2.8.0 --> <title>CVE-2021-4102: Chrome incorrect node elision in Turbofan leads to unexpected WriteBarrier elision | 0-days In-the-Wild</title> <meta name="generator" content="Jekyll v3.10.0" /> <meta property="og:title" content="CVE-2021-4102: Chrome incorrect node elision in Turbofan leads to unexpected WriteBarrier elision" /> <meta name="author" content="Google Project Zero" /> <meta property="og:locale" content="en_US" /> <meta name="description" content="Information about 0-days exploited in-the-wild!" /> <meta property="og:description" content="Information about 0-days exploited in-the-wild!" /> <link rel="canonical" href="https://googleprojectzero.github.io/0days-in-the-wild/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102.html" /> <meta property="og:url" content="https://googleprojectzero.github.io/0days-in-the-wild/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102.html" /> <meta property="og:site_name" content="0-days In-the-Wild" /> <meta property="og:type" content="website" /> <meta name="twitter:card" content="summary" /> <meta property="twitter:title" content="CVE-2021-4102: Chrome incorrect node elision in Turbofan leads to unexpected WriteBarrier elision" /> <script type="application/ld+json"> {"@context":"https://schema.org","@type":"WebPage","author":{"@type":"Person","name":"Google Project Zero"},"description":"Information about 0-days exploited in-the-wild!","headline":"CVE-2021-4102: Chrome incorrect node elision in Turbofan leads to unexpected WriteBarrier elision","url":"https://googleprojectzero.github.io/0days-in-the-wild/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102.html"}</script> <!-- End Jekyll SEO tag --> <link rel="stylesheet" href="/0days-in-the-wild/assets/main.css"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto|Source+Code+Pro"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.7/css/all.css"><link type="application/atom+xml" rel="alternate" href="https://googleprojectzero.github.io/0days-in-the-wild/0days-in-the-wild/feed.xml" title="0-days In-the-Wild" /></head> <body><header class="site-header"> <div class="wrapper"> <a class="site-title" rel="author" href="/0days-in-the-wild/">0-days In-the-Wild</a> <nav class="site-nav"> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger"> <span class="menu-icon"> <svg viewBox="0 0 18 15" width="18px" height="15px"> <path d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.032C17.335,0,18,0.665,18,1.484L18,1.484z M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.032C17.335,6.031,18,6.696,18,7.516L18,7.516z M18,13.516C18,14.335,17.335,15,16.516,15H1.484 C0.665,15,0,14.335,0,13.516l0,0c0-0.82,0.665-1.483,1.484-1.483h15.032C17.335,12.031,18,12.695,18,13.516L18,13.516z"/> </svg> </span> </label> <div class="trigger"> <a href="https://googleprojectzero.github.io/0days-in-the-wild/rca.html" class="page-link">Root Cause Analyses</a> <a href="https://docs.google.com/spreadsheets/d/1lkNJ0uQwbeC1ZTRrxdtuPLCIl7mlUreoKfSIgajnSyY" class="page-link">Tracking Sheet</a> <a href="https://googleprojectzero.github.io/0days-in-the-wild/contributing.html" class="page-link">Contributing</a> <a href="https://googleprojectzero.github.io/0days-in-the-wild/about.html" class="page-link">About</a> <a href="https://googleprojectzero.blogspot.com/" class="menu-link" target="_blank"><i class="fab fa-blogger"></i></a> <a href="https://bugs.chromium.org/p/project-zero/issues/list" class="menu-link" target="_blank"><i class="fas fa-bug"></i></a> <a href="https://github.com/googleprojectzero/0days-in-the-wild" class="menu-link" target="_blank"><i class="fab fa-github"></i></a> <a href="mailto:0day-in-the-wild@google.com" class="menu-link" target="_blank"><i class="fas fa-envelope"></i></a> </div> </nav> </div> </header> <main class="page-content" aria-label="Content"> <div class="wrapper"> <article class="post"> <header class="post-header"> <h1 class="post-title">CVE-2021-4102: Chrome incorrect node elision in Turbofan leads to unexpected WriteBarrier elision</h1> </header> <div class="post-content"> <p>Brendon Tiszka, Chrome</p> <h2 id="the-basics">The Basics</h2> <p><strong>Disclosure or Patch Date:</strong> December 13, 2021</p> <p><strong>Product:</strong> Google Chrome</p> <p><strong>Advisory:</strong> <a href="https://chromereleases.googleblog.com/2021/12/stable-channel-update-for-desktop_13.html">https://chromereleases.googleblog.com/2021/12/stable-channel-update-for-desktop_13.html</a></p> <p><strong>Affected Versions:</strong> pre 96.0.4664.110</p> <p><strong>First Patched Version:</strong> 96.0.4664.110</p> <p><strong>Issue/Bug Report:</strong> <a href="https://crbug.com/1278387">crbug.com/1278387</a></p> <p><strong>Patch CL:</strong> <a href="https://chromium-review.googlesource.com/c/v8/v8/+/3329790">https://chromium-review.googlesource.com/c/v8/v8/+/3329790</a></p> <p><strong>Bug-Introducing CL:</strong> <a href="https://codereview.chromium.org/1908093002">https://codereview.chromium.org/1908093002</a></p> <p><strong>Reporter(s):</strong> Anonymous</p> <h2 id="the-code">The Code</h2> <p><strong>Proof-of-concepts:</strong></p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">mark_sweep</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">2</span><span class="o">**</span><span class="mi">34</span><span class="p">);</span> <span class="p">}</span> <span class="kd">let</span> <span class="nx">scavenge</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span> <span class="kd">let</span> <span class="nx">ref</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">16777216</span><span class="p">);</span> <span class="nx">ref</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="nx">ref</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="nx">ref</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span> <span class="p">}</span> <span class="kd">function</span> <span class="nx">noop</span><span class="p">()</span> <span class="p">{}</span> <span class="p">{</span> <span class="c1">// Ensure `o` is a non-const signed field</span> <span class="kd">var</span> <span class="nx">o</span> <span class="o">=</span> <span class="p">{</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">:</span><span class="mi">5</span><span class="p">};</span> <span class="nx">o</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span> <span class="c1">// Move `o` to OldSpace</span> <span class="nx">mark_sweep</span><span class="p">();</span> <span class="kd">var</span> <span class="nx">b</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">opt</span><span class="p">()</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">a1</span><span class="p">;</span> <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="mi">2</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">b</span> <span class="o">=</span> <span class="nx">i</span> <span class="o">+</span> <span class="o">-</span><span class="mi">0</span><span class="p">;</span> <span class="nx">a1</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">abs</span><span class="p">(</span><span class="nx">b</span><span class="p">);</span> <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o"><</span> <span class="mi">2</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{}</span> <span class="k">for</span> <span class="p">(</span><span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="mi">2</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{}</span> <span class="nx">o</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">abs</span><span class="p">(</span><span class="nx">a1</span><span class="p">);</span> <span class="nx">noop</span><span class="p">();</span> <span class="p">}</span> <span class="p">}</span> <span class="o">%</span><span class="nx">PrepareFunctionForOptimization</span><span class="p">(</span><span class="nx">opt</span><span class="p">);</span> <span class="nx">opt</span><span class="p">();</span> <span class="nx">opt</span><span class="p">();</span> <span class="o">%</span><span class="nx">OptimizeFunctionOnNextCall</span><span class="p">(</span><span class="nx">opt</span><span class="p">);</span> <span class="nx">opt</span><span class="p">();</span> <span class="c1">// Trigger Scavenger, use-after-free</span> <span class="nx">scavenge</span><span class="p">();</span> <span class="nx">mark_sweep</span><span class="p">();</span> <span class="kd">var</span> <span class="nx">ret</span> <span class="o">=</span> <span class="nx">o</span><span class="p">.</span><span class="nx">a</span><span class="p">;</span> <span class="nx">ret</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="p">}</span> </code></pre></div></div> <p><strong>Exploit sample:</strong> No</p> <p><strong>Did you have access to the exploit sample when doing the analysis?</strong> No</p> <h2 id="the-vulnerability">The Vulnerability</h2> <p><strong>Bug class:</strong> WriteBarrier elision (use-after-free)</p> <p><strong>Vulnerability details:</strong></p> <p>Prerequisites:</p> <ul> <li>V8 has two garbage collection cycles: Scavenger (minor) and Mark-Compact (major). For the purposes of this RCA, the scavenger only collects garbage in the young-generation <code>NewSpace</code> and mark-compact collects garbage in all generations <code>OldSpace</code> and <code>NewSpace</code>.</li> <li>Objects in <code>OldSpace</code> can hold references to objects in <code>NewSpace</code>. WriteBarriers are used to maintain a list of old-to-new references which are <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/heap/scavenger.cc;l=684-715;drc=475c8cdf9a951bb06da3084794a0f659f8ef36c2">iterated during Scavenge (minor-gc)</a>.</li> <li>WriteBarriers are only required for pointers to other <code>HeapObjects</code>. V8 will ignore WriteBarrier slots that point to <code>Smis</code>.</li> <li>Turbofan's <code>SimplifiedLoweringPhase</code> determines if a WriteBarrier is needed when visiting a <code>StoreField</code> node by checking the object's field representation. If the field representation is not a tagged or a tagged pointer, then the <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/simplified-lowering.cc;l=1528;drc=8f5c47fd8d80208c191fe575f0817b26a9093837">WriteBarrier will be elided</a>.</li> </ul> <p>An optimization in <code>EarlyOptimizationPhase</code> that elides <code>ChangeTaggedToInt32</code> -> <code>ChangeInt31ToTaggedSigned</code> caused a <code>HeapNumber</code> to be stored to an object in a field with a <code>TaggedSigned</code> representation. A check before <code>EarlyOptimizationPhase</code> in <code>SimplifiedLoweringPhase</code> makes the <em>correct</em> assumption that write barriers can be elided when storing anything to a field in an object with the <code>TaggedSigned</code> representation. This leads to a use-after-free if the <code>HeapNumber</code> being mistakenly stored to the object lives in <code>NewSpace</code>, and the object being stored to lives in <code>OldSpace</code>.</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kChangeInt31ToTaggedSigned</span><span class="p">:</span> <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kChangeInt32ToTagged</span><span class="p">:</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">IsChangeTaggedToInt32</span><span class="p">()</span> <span class="o">||</span> <span class="p">...)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">Replace</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Parts 1 through 4 of the vulnerability details go deep into how to reach this vulnerability.</p> <p><strong><em>(Part 1): Eliding Write Barriers in Turbofan</em></strong></p> <p>The bug itself lives in <code>SimplifiedOperatorReducer</code>, which runs during the <code>EarlyOptimizationPhase</code>. <code>EarlyOptimizationPhase</code> is notably after <code>SimplifiedLoweringPhase</code> (where the <code>WriteBarrier</code> calculation for <code>StoreField</code> happens):</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kChangeInt31ToTaggedSigned</span><span class="p">:</span> <span class="cm">/*** 1 ***/</span> <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kChangeInt32ToTagged</span><span class="p">:</span> <span class="p">{</span> <span class="n">Int32Matcher</span> <span class="n">m</span><span class="p">(</span><span class="n">node</span><span class="o">-></span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span> <span class="k">if</span> <span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">HasResolvedValue</span><span class="p">())</span> <span class="k">return</span> <span class="n">ReplaceNumber</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">ResolvedValue</span><span class="p">());</span> <span class="k">if</span> <span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">IsChangeTaggedToInt32</span><span class="p">()</span> <span class="o">||</span> <span class="n">m</span><span class="p">.</span><span class="n">IsChangeTaggedSignedToInt32</span><span class="p">())</span> <span class="p">{</span> <span class="cm">/*** 2 ***/</span> <span class="k">return</span> <span class="n">Replace</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span> <span class="cm">/*** 3 ***/</span> <span class="p">}</span> <span class="k">break</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>The bug is simple. When the <code>SimplifiedOperatorReducer</code> visits a <code>ChangeInt31ToTaggedSigned</code> [1] node, it will elide [3] <code>ChangeInt32ToTagged</code> -> <code>ChangeInt31ToTaggedSigned</code> [2]. Consider this theoretical sequence of nodes after the <code>SimplifiedLoweringPhase</code> which can be emitted when storing to a <code>TaggedSigned</code> field:</p> <p><img src="/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102-turbolizer-a.png" alt="" /></p> <p>The input to <code>ChangeTaggedToInt32</code> can be either a <code>Smi</code> or a <code>TaggedPointer</code>. If the input to <code>ChangeTaggedToInt32</code> is a <code>TaggedPointer</code>, and <code>ChangeTaggedToInt32</code> -> <code>ChangeInt31ToTaggedSigned</code> is elided, then it will result in an object with a <code>TaggedPointer</code> type being stored to an object field whose representation is <code>TaggedSigned</code> during <code>StoreField</code>.</p> <p>While the bug is simple, producing the correct sequence of nodes after <code>SimplifiedLoweringPhase</code> and coercing <code>SimplifiedOperatorReducer</code> into visiting <code>ChangeInt31ToTaggedSigned</code> before <code>ChangeTaggedToInt32</code> is difficult.</p> <p><strong><em>(Part 2): Arranging Nodes Pre-Simplified-Lowering</em></strong></p> <p>Let's take a look at a simplified poc that can be used to create the arrangement of nodes mentioned above after <code>SimplifiedLoweringPhase</code>:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">o</span> <span class="o">=</span> <span class="p">{</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">:</span> <span class="mi">5</span><span class="p">};</span> <span class="nx">o</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="mi">6</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">b</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">opt</span><span class="p">()</span> <span class="p">{</span> <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="mi">2</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">b</span> <span class="o">=</span> <span class="nx">i</span> <span class="o">+</span> <span class="o">-</span><span class="mi">0</span><span class="p">;</span> <span class="nx">o</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">abs</span><span class="p">(</span><span class="nx">b</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p><strong>Step 1</strong></p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">o</span> <span class="o">=</span> <span class="p">{</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">:</span> <span class="mi">5</span><span class="p">};</span> <span class="nx">o</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="mi">6</span><span class="p">;</span> </code></pre></div></div> <p>We start by creating a <code>JSObject</code> with a field that has a mutable <code>TaggedSigned</code> representation.</p> <p><strong>Step 2</strong></p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">b</span><span class="p">;</span> <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="mi">2</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">b</span> <span class="o">=</span> <span class="nx">i</span> <span class="o">+</span> <span class="o">-</span><span class="mi">0</span><span class="p">;</span> <span class="p">[...]</span> <span class="p">}</span> </code></pre></div></div> <p><code>JSNativeContextSpecialization</code> will emit a <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/js-native-context-specialization.cc;l=1257;drc=445f8cfdbb63b29ce44f728f6b5015cf18df5534">CheckHeapObject</a> node as input to <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/js-native-context-specialization.cc;l=1274;drc=445f8cfdbb63b29ce44f728f6b5015cf18df5534">StoreField</a> because we are storing a <code>HeapNumber</code> to the global property <code>b</code>.</p> <p><strong>Step 3</strong></p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="mi">2</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="p">[...]</span> <span class="nx">o</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">abs</span><span class="p">(</span><span class="nx">b</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Passing <code>b</code> to <code>Math.abs</code> causes <code>LoadEliminationPhase</code> to pass the <code>CheckHeapObject</code> node as input to both the <code>NumberAbs</code> and the <strong>global</strong> <code>StoreField</code> above. <code>Math.abs</code> will be reduced to <code>NumberAbs</code> and then <code>JSSetNamedProperty</code> will be reduced to a <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/js-native-context-specialization.cc;l=3022;drc=be8f6de811592171739ea8ea326255df6065ce1d">CheckSmi</a> node as input to <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/js-native-context-specialization.cc;l=3105;drc=be8f6de811592171739ea8ea326255df6065ce1d">StoreField</a> because the field that's being stored to has the field representation <code>TaggedSmi</code>.</p> <p>Putting all of this together this results in this graph after <code>LoadEliminationPhase</code>:</p> <p><img src="/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102-turbolizer-c.png" alt="" /></p> <p><strong><em>(Part 3): Arranging Nodes during SimplifiedLowering</em></strong></p> <p>These nodes are lowered as follows in the <code>SimplifiedLoweringPhase</code>:</p> <ol> <li><code>NumberAdd</code> is lowered <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/simplified-lowering.cc;l=2502;drc=c7feb2cc1656bc5ecaacf082bb1651da5959077c">to <code>Int32Add</code></a> because its type is <code>Range(0, 2)</code>.</li> <li><code>CheckHeapObject</code> is lowered <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/simplified-lowering.cc;l=3647;drc=c7feb2cc1656bc5ecaacf082bb1651da5959077c">to <code>ChangeUint32ToFloat64</code> -> <code>ChangeFloat64ToTaggedPointer</code></a> because the type of <code>NumberAdd</code> is <code>Range(0, 2)</code>.</li> <li><code>NumberAbs</code> is lowered <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/simplified-lowering.cc;l=2649;drc=7a8373f18e2327d7dc52600fc9e52cc2f5b6abf6">to <code>ChangeTaggedToInt32</code></a> because the type of <code>CheckHeapObject</code> is <code>Range(0, 2)</code>.</li> <li><code>CheckSmi</code> is lowered <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/simplified-lowering.cc;l=3230;drc=7a8373f18e2327d7dc52600fc9e52cc2f5b6abf6">to <code>ChangeInt32ToTaggedSigned</code></a> because the type of the <code>NumberAbs</code> node is <code>Range(0, 2)</code>.</li> </ol> <p><img src="/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102-turbolizer-b.png" alt="" /></p> <p>This results in the above graph which <strong>appears</strong> to satisfy all of the conditions that we need to trigger this vulnerability and store a <code>TaggedPointer</code> to a field with a <code>smi</code> representation without a write barrier because <code>ChangeUint64ToFloat64</code> -> <code>ChangeFloat64ToTaggedPointer</code> -> <code>ChangeTaggedToInt32</code> -> <code>ChangeInt31ToTaggedSigned</code> -> <code>StoreField</code> should be elided directly to <code>ChangeUint64ToFloat64</code> -> <code>ChangeFloat64ToTaggedPointer</code> -> <code>StoreField</code>.</p> <p><strong><em>(Part 4): Traversing the Graph</em></strong></p> <p>Turbofan's <a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/graph-reducer.cc;l=148;drc=8f5c47fd8d80208c191fe575f0817b26a9093837">GraphReducer algorithm</a> is a modified version of depth-first-search. It starts from the <code>kEnd</code> node and recurses up node inputs from left to right until it reaches a node that has either seen before or until all of its inputs have been visited. The order in which nodes are visited is important triggering this bug. For example, the simple proof-of-conept from Part2/Part3 does not reach the vulnerability because <code>ChangeTaggedToInt32</code> is visited before the <code>ChangeInt31ToTaggedSigned</code>.</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kChangeTaggedSignedToInt32</span><span class="p">:</span> <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kChangeTaggedToInt32</span><span class="p">:</span> <span class="p">{</span> <span class="n">NumberMatcher</span> <span class="n">m</span><span class="p">(</span><span class="n">node</span><span class="o">-></span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span> <span class="k">if</span> <span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">HasResolvedValue</span><span class="p">())</span> <span class="k">return</span> <span class="n">ReplaceInt32</span><span class="p">(</span><span class="n">DoubleToInt32</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">ResolvedValue</span><span class="p">()));</span> <span class="k">if</span> <span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">IsChangeFloat64ToTagged</span><span class="p">()</span> <span class="o">||</span> <span class="n">m</span><span class="p">.</span><span class="n">IsChangeFloat64ToTaggedPointer</span><span class="p">())</span> <span class="p">{</span> <span class="k">return</span> <span class="n">Change</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">machine</span><span class="p">()</span><span class="o">-></span><span class="n">ChangeFloat64ToInt32</span><span class="p">(),</span> <span class="n">m</span><span class="p">.</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span> <span class="cm">/*** A ***/</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">IsChangeInt31ToTaggedSigned</span><span class="p">()</span> <span class="o">||</span> <span class="n">m</span><span class="p">.</span><span class="n">IsChangeInt32ToTagged</span><span class="p">())</span> <span class="p">{</span> <span class="k">return</span> <span class="n">Replace</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span> <span class="p">}</span> <span class="k">break</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>This happens because the graph is traversed with DFS starting from <code>End</code>, we will always visit <code>ChangeTaggedToInt32</code> before <code>ChangeInt31ToTaggedSigned</code> with that graph because <code>ChanteTaggedToInt32</code> only has one output and <code>ChanteInt32ToTaggedSigned</code> only has one input. <code>ChangeTaggedToInt32</code> is guaranteed to be reduced before <code>ChangeInt31ToTaggedSigned</code>:</p> <p><img src="/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102-turbolizer-b.png" alt="" /></p> <p>When this happens, <code>SimplifedOperatorReducer</code> elides <code>ChangeUint64ToFloat64</code> -> <code>ChangeFloat64ToTaggedPointer</code> -> <code>ChangeTaggedToInt32</code> -> <code>ChangeInt31ToTaggedSigned</code> -> <code>StoreField</code> to <code>ChangeUint64ToFloat64</code> -> <code>ChangeFloat64ToInt32</code> -> <code>ChangeInt31ToTaggedSigned</code> -> <code>StoreField</code> [A] which is semantically valid and does not introduce a security issue.</p> <p>To get around this, the final proof of concept intricately and cleverly creates multiple <code>FrameState</code>, <code>StateValue</code>, and <code>CheckPoint</code> nodes that take <code>ChangeTaggedToInt32</code> as an input, causing the GraphTraversal to push <code>ChangeTaggedToInt32</code> on the stack before reaching <code>ChangeInt31ToTaggedSigned</code>. This causes the GraphTraversal for <code>SimplifiedOperatorReducer</code> to stop at <code>ChangeInt31ToTaggedSigned</code> because <code>ChangeTaggedToInt32</code> is already on the stack. Finally, <code>ChanteTaggedToInt32</code> -> <code>ChanteInt31ToTaggedSigned</code> is elided.</p> <p><img src="/0days-in-the-wild/0day-RCAs/2021/CVE-2021-4102-turbolizer-d.png" alt="" /></p> <p><strong>Patch analysis:</strong> Commit <a href="https://chromium-review.googlesource.com/c/v8/v8/+/3329790">4fae8b1</a> fixed the bug in <code>SimplifiedOperatorReducer</code> that could cause <code>ChangeTaggedToInt32</code> -> <code>ChangeInt31ToTaggedSigned</code> to be elided.</p> <p><strong>Thoughts on how this vuln might have been found <em>(fuzzing, code auditing, variant analysis, etc.)</em>:</strong></p> <p>The bug was probably discovered through manual analysis.</p> <p><strong>(Historical/present/future) context of bug:</strong></p> <p>There was a bug in Turbofan's handling of WriteBarriers in the past during <code>SimplfiedLoweringPhase</code> <a href="crbug.com/791245">crbug.com/791245</a>. However, CVE-2021-4102 was the first bug of its kind that bypassed that check in a later optimization pass.</p> <h2 id="the-exploit">The Exploit</h2> <p>(The terms <em>exploit primitive</em>, <em>exploit strategy</em>, <em>exploit technique</em>, and <em>exploit flow</em> are <a href="https://googleprojectzero.blogspot.com/2020/06/a-survey-of-recent-ios-kernel-exploits.html">defined here</a>.)</p> <p><strong>Exploit strategy (or strategies):</strong> We did not have an exploit sample while analyzing this vulnerability, but it is reasonable to assume that the use-after-free was used to either leak a sentinel value like <code>TheHole</code> or <code>UninitializedOddball</code> or to directly materialize a fake object similar to <a href="https://crbug.com/1307610">crbug.com/1307610</a>.</p> <p><strong>Exploit flow:</strong> A typical exploit will first construct an arbitrary read/write primitive, then use that to gain shellcode execution.</p> <p><strong>Known cases of the same exploit flow:</strong> Most other V8 exploits.</p> <p><strong>Part of an exploit chain?</strong> N/A</p> <h2 id="the-next-steps">The Next Steps</h2> <h3 id="variant-analysis">Variant analysis</h3> <p><strong>Areas/approach for variant analysis (and why):</strong></p> <ul> <li>Manual Variant Analysis: Look at other reduction passes after <code>SimplifiedLoweringPhase</code> that could be abused to cause unexpected WriteBarrier elision.</li> </ul> <p><strong>Found variants:</strong></p> <ul> <li><a href="https://crbug.com/1307610">crbug.com/1307610</a> discovered by Brendon Tiszka is not a direct variant of this vulnerability, however it is another WriteBarrier elision bug and was reported after root-causing this vulnerability.</li> <li><a href="https://crbug.com/1382434">crbug.com/1382434</a> discovered by Sergei Glazunov of Google Project Zero is not a variant of this vulnerability, however it uses WriteBarrier elision to exploit a class of vulnerabilty.</li> <li><a href="https://crbug.com/1423610">crbug.com/1423610</a> discovered by Nan Wang and Zhenghang Xiao of Qihoo360 is another WriteBarrier elision bug in V8's Maglev JIT compiler.</li> </ul> <h3 id="structural-improvements">Structural improvements</h3> <p>What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?</p> <p><strong>Ideas to kill the bug class:</strong> N/A</p> <p><strong>Ideas to mitigate the exploit flow:</strong> The V8 Sandbox project is designed to break this exploit flow for the vast majority of V8 vulnerabilities, including this one.</p> <p><strong>Other potential improvements:</strong> N/A</p> <h3 id="day-detection-methods">0-day detection methods</h3> <p>What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected <strong>as a 0-day</strong>? N/A</p> <h2 id="other-references">Other References</h2> <ul> <li>A WriteBarrier elision <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1307610#c5">hole leaking technique</a> and a WriteBarrier elision fake object technique - <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1307610#c43">exploit</a> - are publicly documented.</li> </ul> </div> </article> </div> </main><footer class="site-footer h-card"> <data class="u-url" href="/0days-in-the-wild/"></data> <div class="wrapper"> <h2 class="footer-heading">0-days In-the-Wild</h2> <div class="footer-col-wrapper"> <div class="footer-col footer-col-1"> <ul class="contact-list"> <li class="p-name">Google Project Zero</li><li><a class="u-email" href="mailto:0day-in-the-wild@google.com">0day-in-the-wild@google.com</a></li></ul> </div> <div class="footer-col footer-col-2"><ul class="social-media-list"><li><a href="https://github.com/googleprojectzero"><svg class="svg-icon"><use xlink:href="/0days-in-the-wild/assets/minima-social-icons.svg#github"></use></svg> <span class="username">googleprojectzero</span></a></li></ul> </div> <div class="footer-col footer-col-3"> <p>Information about 0-days exploited in-the-wild!</p> </div> </div> </div> </footer> </body> </html>