CVE-2022-3723: Logic Issue in Turbofan JIT Compiler
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
, andobj.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 forobj.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?