Samuel Groß, V8 Security

The Basics

Disclosure or Patch Date: 27 October 2022

Product: Google Chrome

Advisory: https://chromereleases.googleblog.com/2022/10/stable-channel-update-for-desktop_27.html

Affected Versions: 107.0.5304.62 and previous

First Patched Version: 107.0.5304.87

Issue/Bug Report: https://bugs.chromium.org/p/chromium/issues/detail?id=1378239 (Embargoed)

Patch CL: https://chromium.googlesource.com/v8/v8/+/db83e72034c0d431ff2f73e3c4ae3130c0f3e4e1

Bug-Introducing CL: N/A

Reporter(s): Jan Vojtěšek, Milánek, and Przemek Gmerek of Avast

The Code

Proof-of-concept:

// --expose-gc --allow-natives-syntax

function setInnerProperty(o) {
  o.inner.foo = {};
}

function makeObject() {
    // [‘foo’] syntax causes kStoreInLiteral operation
    var o = {
        inner: {
            ['foo']: 0
        }
    };
    // Spreading arguments prevents inlining
    setInnerProperty(o, ...arguments);
    %OptimizeFunctionOnNextCall(setInnerProperty);
    setInnerProperty(o, ...arguments);
    return o;
}

%PrepareFunctionForOptimization(makeObject);
%PrepareFunctionForOptimization(setInnerProperty);

makeObject();
gc();
makeObject();
gc();

%OptimizeFunctionOnNextCall(makeObject);
let o = makeObject();
%HeapObjectVerify(o.inner);

Exploit sample: N/A

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

The Vulnerability

Bug class: Logic Issue in JIT Compiler.

Vulnerability details: Prerequisites:

  • When Turbofan compiles code such as obj.foo.bar.baz, it must generally emit a CheckMap operation for every object, i.e. obj,obj.foo, and obj.foo.bar
  • The exception is when precise FieldType information for a field is available. For example, in the example above obj may be an object of Map A where the FieldType for .foo states that it will always be an object of Map B. In that case, the CheckMap for obj.foo can be elided iff Map B is stable and a CodeDependency for that field type is installed (so that the compiled code will be discarded if the FieldType ever changes, e.g. because the property has been changed to another value)
  • The FieldType holds on to the Map through a weak reference. If the Map is collected (because no more objects are using it), the reference is cleared and the FieldType now implicitly becomes "None". Crucially, this does not trigger deoptimization of any optimized functions. This is safe because there cannot currently be an outer object (obj in the above example) as there cannot be an inner object (obj.foo). Further, the next time an outer object is created, the FieldType of its property (.foo) will be generalized from “None” to “Any”, which will then trigger the deoptimization.
  • When the FieldType is “None”, Turbofan must be very careful not to optimize any stores to such fields. For example, it must not optimize obj.foo.bar = {} into a StoreField operation (a direct store to memory, basically) if the FieldType of .bar is “None” as that would not perform the FieldType generalization. Instead, it must rely on the runtime to perform the property store and do the generalization so that CodeDependencies are correctly handled and optimized functions discarded.

The Bug:

  • When optimizing property stores, turbofan correctly handled the “None” FieldType case only for the “kStore” AccessMode but not for the “kStoreInLiteral” case. That made it possible to change the value of a FieldType “None” field without generalizing the FieldType, and so without deoptimizing any code that still relies on the previous FieldType.

Patch analysis: Commit db83e72034 fixed Turbofan’s handling of the “None” FieldType by forbidding any stores to fields of that type. With that, any optimized code that still relies on the old Map after it has been collected is now properly discarded when an object of an outer type is created the next time.

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.): Triggering the bug requires a fairly complex testcase, so a fuzzer will likely struggle to find it without additional help. As such, the bug was probably discovered through manual analysis or targeted fuzzing, for example of the handling of "none" FieldTypes in the JIT compiler.

(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):

  • Build a function “makeObject” that creates a new object of type OUTER with a property .inner of type INNER, which itself has a property .foo of type Y (e.g Smi). The FieldType of .inner is now INNER
  • Build a function “setInnerProperty” that takes an OUTER object as parameter and sets outer.inner.foo to some value X (e.g. Object). Optimize the function in turbofan, which will now be able to elide a CheckMaps for .inner
  • Trigger garbage collection to collect all instances of OUTER and INNER and free the INNER map (but keep the OUTER map alive!). This will set the FieldType of .inner to “None” but will not deoptimize setInnerProperty()
  • Optimize makeObject(). This will use the “kStoreInLiteral” AccessMode when creating OUTER and will therefore fail to generalize the FieldType of .inner
  • Call makeObject() to create a new OUTER object with a new INNER object with a ‘.foo property of type X (e.g. Smi)
  • Call setInnerProperty() to corrupt outer.inner.bar with the value X, which is now incompatible with the current FieldType of .foo

At that point, type confusions can be constructed in optimized code by accessing the .foo field which now has a different type than what its FieldType states.

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? Unknown but likely.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why): Investigate other store operations, or stores on other types of objects, and ensure that they leave the object in a consistent state. This can be done manually or through fuzzing. For example, Fuzzilli could not previously emit code such as { ['foo']: 42 } (which was necessary to trigger this bug), but now supports these.

Found variants:

  • Sergei Glazunov from Project Zero found a similar bug that involves copy-on-write arrays: CVE-2022-4906.

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: Unknown. This is essentially a logic bug in a JIT compiler that can then be exploited to cause memory corruption. One approach would be to entirely remove field type tracking in the engine, but that comes with a fairly significant performance cost and the benefit is somewhat unclear: while it would have prevented this specific bug, it wouldn't have prevented CVE-2022-4906 (see above) which is otherwise very similar.

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