Samuel Groß, V8 Security

The Basics

Disclosure or Patch Date: 2 December 2022

Product: Google Chrome

Advisory: https://chromereleases.googleblog.com/2022/12/stable-channel-update-for-desktop.html

Affected Versions: 108.0.5359.71 and previous

First Patched Version: 108.0.5359.94

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

Patch CL: https://chromium.googlesource.com/v8/v8/+/27fa951ae4a3801126e84bc94d5c82dd2370d18b

Bug-Introducing CL: N/A

Reporter(s): Clement Lecigne of Google's Threat Analysis Group

The Code

Proof-of-concept:

let alloc = function() {
  let tt = new ArrayBuffer(31 * 1024 * 1024 * 1024);
  tt = new ArrayBuffer(31 * 1024 * 1024 * 1024);
  tt = new ArrayBuffer(31 * 1024 * 1024 * 1024);
  tt = new ArrayBuffer(31 * 1024 * 1024 * 1024);
  tt = new ArrayBuffer(31 * 1024 * 1024 * 1024);
  tt = new ArrayBuffer(31 * 1024 * 1024 * 1024);
};
for (let j = 0; j < 9999999; j++) {
  (
    (a = class b3 {
      [
        {
          c: eval()
        } ? 0 : (xy = 1)
      ]
    }) => {}
  )();
  if (j == 8) {
    alloc();
  }
}


Exploit sample: N/A

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

The Vulnerability

Bug class: Different bytecode produced by JavaScript parser during reparsing of a function.

Vulnerability details: Prerequisites:

  • V8 may end up parsing a given JavaScript source string multiple times: due to bytecode aging, existing bytecode may be evicted after not being used for some time. In that case, if the function is later used again, it will be recompiled again.
  • When such a recompilation happens, the parsing logic can take some shortcuts. For example, when parsing the code (a = 42) => a + 1; for the first time, when reaching the “=”, the parse cannot yet know whether it will parse a variable assignment or an arrow function with a default argument. However, during the second (and third, etc.) parsing, it knows that it is parsing an arrow function. As such, the logic for the initial parsing differs slightly from the logic for any subsequent parsing.
  • The bytecodes generated from the first and second compilation are expected to be equal, and any mismatch between the two can lead to memory corruption as other data structures, such as feedback vectors, will not be re-created during reparsing and are specific to the (first) bytecode.

The following JavaScript code leads to different parsing outcomes between initial and second parsing:

​​  (
    (a = class b3 {
      [
        {
          c: eval()
        } ? 0 : (xy = 1)
      ]
    }) => {}
  )();

This is a JS arrow function with a default argument (for a) that is a class expression which has a computed property that uses direct eval. Prior to the fix, the two parsing passes would handle the eval call differently, which would lead to an (exploitable) mismatch in the generated bytecode. To trigger memory corruption, bytecode aging has to be triggered through garbage collection, as shown above.

Patch analysis: Commit db83e72034 fixed the bug in the parser by ensuring that both parsing passes use the correct strictness attributes and therefore handle the call to eval in the same way.

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.): Fuzzing on an AST-level (or even below that) could've eventually found this bug since it mostly only requires somewhat odd (but valid) syntactical constructs. In particular, once a researcher notices that bytecode mismatch potentially leads to security issues, targeted fuzzing for these issues becomes much simpler, see below.

(Historical/present/future) context of bug: Unknown.

The Exploit

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

Exploit strategy (or strategies): Bytecode mismatch can lead to type confusions as some data structures associated with bytecode (such as feedback vectors) will have been created for the initial bytecode but will then be used with the re-parsed bytecode. In this case, the bug for example allowed an attacker to confuse a PropertyCell with a FeedbackCell object, leading to memory corruption inside the V8 heap.

Exploit flow: Type confusions between V8 objects are easily exploitable. A typical exploit will first construct an arbitrary read/write primitive, then use that to gain shellcode execution.

Known cases of the same exploit flow: Most other V8 exploits.

Part of an exploit chain? According to the TAG blog post, CVE-2022-4262 was used against Samsung devices as the renderer RCE. It was used in the chain with CVE-2022-3038, CVE-2022-22706, and CVE-2023-0266.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why): Two v8 flags can make finding this bug significantly easier: --no-lazy and --stress-lazy-source-positions. These will cause every function to be compiled twice right away and will furthermore cause a CHECK failure if the generated bytecode is different. With these flags, triggering the bug is possible with a much simpler testcase (compared with the test case above):

for (let j = 0; j < 10; j++) {  // Or any other block
    (a = class b3 { [eval()] }) => {}
}

With crrev.com/c/4096480, these two flags are now automatically enabled when --fuzzing is enabled, which should help v8 fuzzers find similar bugs much easier in the future.

Found variants: Since enabling the two flags when --fuzzing, fuzzers have found some other cases of bytecode mismatches, which can be found using this crbug query. However, not all such issues had security impact, such as this one where due to lucky circumstances, the bytecode mismatch was harmless.

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: With the new flags, discovering these bugs through fuzzing should be significantly easier. On top of that, the parse could be hardened, for example by recreating associated data-structures during re-parsing of the bytecode. However, that would be a non-trivial change with potential for introducing bugs on its own and would come with a performance cost.

Ideas to mitigate the exploit flow: The V8 Sandbox project is designed to break this exploit flow for the vast majority of V8 vulnerabilities, including this one.

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