Samuel Groß, V8 Security

The Basics

Disclosure or Patch Date: 14 April 2022

Product: Google Chrome

Advisory: https://chromereleases.googleblog.com/2022/04/stable-channel-update-for-desktop_14.html

Affected Versions: 100.0.4896.79 and previous

First Patched Version: 100.0.4896.127

Issue/Bug Report: https://bugs.chromium.org/p/chromium/issues/detail?id=1315901

Patch CL: https://chromium.googlesource.com/v8/v8/+/8081a5ffa7ebdb0e5b35cf63aa0490ad3578b940

Bug-Introducing CL: N/A

Reporter(s): Clément Lecigne of Google's Threat Analysis Group

The Code

Proof-of-concept:

function foo(bug) {
  function C(z) {
    Error.prepareStackTrace = function(t, B) {
      return B[z].getThis();
    };
    let p = Error().stack;
    Error.prepareStackTrace = null;
    return p;
  }
  function J() {}
  var optim = false;
  var opt = new Function(
      'a', 'b', 'c',
      'if(typeof a===\'number\'){if(a>2){for(var i=0;i<100;i++);return;}b.d(a,b,1);return}' +
          'g++;'.repeat(70));
  var e = null;
  J.prototype.d = new Function(
      'a', 'b', '"use strict";b.a.call(arguments,b);return arguments[a];');
  J.prototype.a = new Function('a', 'a.b(0,a)');
  J.prototype.b = new Function(
      'a', 'b',
      'b.c();if(a){' +
          'g++;'.repeat(70) + '}');
  J.prototype.c = function() {
    if (optim) {
      var z = C(3);
      var p = C(3);
      z[0] = 0;
      e = {M: z, C: p};
    }
  };
  var a = new J();
  // jit optim
  if (bug) {
    for (var V = 0; 1E4 > V; V++) {
      opt(0 == V % 4 ? 1 : 4, a, 1);
    }
  }
  optim = true;
  opt(1, a, 1);
  return e;
}

e1 = foo(false);
console.log(e1.M === e1.C); // prints true.
e2 = foo(true);
console.log(e2.M === e2.C); // should be true as above but prints false.

Exploit sample: N/A

Did you have access to the exploit sample when doing the analysis? Yes

The Vulnerability

Bug class: Inconsistent object materialization.

Vulnerability details: Prerequisite: escape analysis and materialization

  • When escape analysis removes an allocation (because it doesn’t escape), it still needs to keep the information necessary for reconstructing (“materializing”) the object at runtime.
  • There are currently two reasons for materialization: (1) Deoptimization, in which case all objects must be materialized as they are needed by the interpreter, and (2) the OptimizedFrame::Summarize function for collecting stackframe information, in which case the function and the this object must be materialized as they are exposed to JS (e.g. through the getThis method). Case (2) can be triggered for example through the Error constructor.
  • In case (2), the optimized function is not deoptimized. As such, when collecting the stacktrace multiple times for the same activation, the same object (e.g. this) is materialized multiple times, which is observable. This is a documented correctness bug.
  • There are other, related correctness bugs due to this logic. For example, if the materialized this is modified, the changes are not reflected in the optimized code. See Appendix 2 for an example of this issue

The security bug:

  • In certain scenarios, when the escape analysis dematerializes an ArgumentsObject, it keeps the backing store (which contains the function’s arguments in a FixedArray) of the object alive. With the PoC in Appendix 1, this happens because the backing store allocation is used by a LoadElement operation with dynamic index due to the return arguments[a] statement. As such, escape analysis concludes that the ArgumentsObject can be dematerialized, but its backing store cannot.
  • This in combination with the correctness bug described above results in a security bug: by using an arguments object as this value for a call, then materializing it multiple times using the stack capturing mechanism, multiple ArgumentsObjects are created which all share the same backing store. This violates the V8 invariant that backing stores are never shared, except if they are copy-on-write.
  • This can then be used to leak the internal “hole” value: by deleting an entry from one arguments object (which will then transition from PACKED to HOLEY elements type), then reading it from the other (which still has PACKED elements type), the “hole” is returned to JS code
  • The “hole” can then be exploited as described in crbug.com/1263462

Patch analysis: The patch marks the this object as escaping, thus preventing it from being dematerialized in the first place. A follow-up patch (https://chromium.googlesource.com/v8/v8/+/add8811019a1f2015c4214c20df8e6e8d4a864bb) forbids materialization during OptimizedFrame::Summarize.

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.): Could've been found as a variant of CVE-2021-21195 or through (differential?) fuzzing.

(Historical/present/future) context of bug: See above.

The Exploit

(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)

Exploit strategy (or strategies): The bug can be used to create two or more JSArgumentsObjects that all point to the same backing store.

Exploit flow: A situation in which two array-like objects point to the same backing store can be exploited to leak the "hole" value as described above. From there, memory corruption can be achieved as described in crbug.com/1263462

Known cases of the same exploit flow: CVE-2021-38003 had exploited access to the "hole" in the same way.

Part of an exploit chain? Unknown but likely.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why): In general issues where escape analysis incorrectly marks an object as not-escaping. More specifically, other cases where objects are materialized multiple times which may lead to inconsistent state.

Found variants: See https://bugs.chromium.org/p/chromium/issues/detail?id=1315901#c65

Structural improvements

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.?

Ideas to kill the bug class: Forbid double-materialization of objects (the known correctness bug), which ultimately allows this class of issues to occurr in the first place. This is effectively what the follow-up patch did.

Ideas to mitigate the exploit flow: It's likely not realistic to prevent leakage of the "hole" when backing stores are accidentally shared as array access is very performance sensitive. However, exploiting access to the "hole" can be mitigated by explicitly checking for it, as done in https://chromium.googlesource.com/v8/v8/+/66c8de2cdac10cad9e622ecededda411b44ac5b3

Other potential improvements:

0-day detection methods

What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected as a 0-day?

Other References