Chrome v8 CVE-2020-6418의 Root cause에 대해 소개해보겠습니다.



환경설정





# install depot_tools
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=$HOME/depot_tools:$PATH
echo 'export PATH=$HOME/depot_tools:$PATH' >> ~/.zshrc

# get V8
cd ~
mkdir v8
cd v8
fetch v8
cd v8
git checkout bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07


# build V8

gclient sync -D
sudo apt install -y python ninja-build
build/install-build-deps.sh
./tools/dev/gm.py x64.release





사전지식





해당 Root cause를 설명하기 위해서 먼저 필요한 개념들에 대해 먼저 소개해보겠습니다.





Map Inference





Map Inference는 V8에서 최적화 과정에서 object의 map, 즉 type을 추론하는 작업을 합니다.

이러한 작업을 하는 이유는 v8은 동적언어인 JS를 사용하기 때문에 Runtime에서 Map이 변경될수도 있습니다.

이때 계속하여 Map 정보를 업데이트 해줘야 최적화를 진행 또는 유지할 수 있습니다.



하나의 예시를 살펴보겠습니다.

2020_1.png

(v8에서 Debug 명령들을 사용하기 위해선 --allow-natives-syntax 옵션을 사용해줘야 합니다.

Debug 명령어 예시 : %DebugPrint() , %SystemBreak() 등등)

현재 해당 배열은 SMI 타입인 걸 확인할 수 있습니다.

2020_2.png

이때 해당 배열의 0번 인덱스의 값을 1.1로 바꾸어 보면 SMI 타입이던 Map이 DOUBLE로 변경되는 걸 확인할 수 있습니다.

(V8에선 더 큰 개념으로 타입이 변경됩니다.)

Map Inference가 추론한 결과는 두 가지가 나올 수 있는데 다음과 같습니다.

/* src/compiler/map-inference.h */

// The MapInference class provides access to the "inferred" maps of an
// {object}. This information can be either "reliable", meaning that the object
// is guaranteed to have one of these maps at runtime, or "unreliable", meaning
// that the object is guaranteed to have HAD one of these maps.
//
// The MapInference class does not expose whether or not the information is
// reliable. A client is expected to eventually make the information reliable by
// calling one of several methods that will either insert map checks, or record
// stability dependencies (or do nothing if the information was already
// reliable).



  1. reliable -> Runtime에서 Map이 변경될 가능성이 없다면 reliable로 반환합니다
  2. unreliable -> Runtime에 Map이 변경될 가능성이 존재한다면 unreliable로 반환합니다.

v8에는 Mapchecks라는 노드가 존재하는데 해당 노드의 역할은 Runtime에서 Mapchecks노드를 가지고 있는 객체의 Map을 확인해주는 역할을 합니다.

reliable의 경우 위에서 이야기한대로 Map이 변경될 가능성이 없기 떄문에 다시 Map을 체크하는 루틴을 거치게 된다면 성능이 저하가 됩니다.
그러므로 reliable로 설정된 객체는 Mapchecks 노드를 삽입하지 않아 최적화를 합니다.

unreliable은 최적화를 위해선 map이 항상 확인이 되야하기 때문에 Runtime에 Map이 변경될 가능성이 있는 unreliable한 객체는 Mapchecks 노드를 삽입하여 Runtime에 Map을 확인함으로써 최적화를 할 수 있게끔 합니다.



Receiver





JS에서 receiver는 함수 호출의 대상이 되는 객체입니다. 예를 들어, 배열 arr에 대해 arr.pop()을 호출할 경우, pop()함수의 receiver는 arr입니다.

Receiver

ex) arr.pop() 에선 arr이 receiver



Sea of nodes





Sea of nodes는 V8에서 TurboFan이 최적화하는데 사용하는 기법으로 프로그램의 모든 연산과 데이터를 노드(node)로 표현하는 그래프 기반의 IR 방식입니다.

예를 들어 JS코드가 아래와 같다면

let a = 10;
let b = 20;
let c = a + b;
console.log(c);



Sea of nodes로 표현하면 다음과 같습니다.

a = 10 -> node 1
b = 20 -> node 2
c = a + b -> node 3 (노드 1과 노드2에 의존)
console.log(c) -> node 4(노드 3에 의존)

위와 같은 결과를 노드들간의 의존성으로 표현하는 방식입니다.

Sea of nodes의 간단한 동작순서는 이러합니다.

1.프론트 엔드:JavaScript 소스 코드를 바이트코드로 변환한 뒤, 이를 TurboFan에서 이해할 수 있는 IR로 변환

2.Sea of nodes 그래프 생성: 모든 연산과 값을 노드로 표현하여 그래프를 생성 이 그래프는 순서가 없으며, 단순히 노드들의 의존성 관계를 나타냄

3.최적화:상수 폴딩 , 공통 부분식 제거, 죽은 코드 제거, 루프 최적화 등

4.백엔드: 최적화된 노드 그래프를 기반으로 머신코드 생성





Effect chain





Effect chain은은 sea of nodes에서 어떤 함수의 변화 과정에 해당하는 부분을 연결 시킨 걸 함수의 Effect Chain이라고 합니다.
여기서 말하는 변화과정은 런타임 중 객체나 배열의 구조적 변경 또는 상태 변화로 인해 Map이 변할 수 있는 모든 작업을 포함합니다.

변화과정의 예시: 배열의 요소 추가 or 제거 , 배열 크기 조정 , 요소 타입 변경 , 속성 추가 or 제거 , 속성 타입 변경 , 프로토 타입 변경 , 변수 선언 및 최가화 등등


※더 많이 존재합니다



patch1





2020_3.png



해당 취약점의 패치 내용은 그림의 빨간색 줄이 추가된 것이 전부입니다.
(위치는 InferReceiverMapsUnsafe() 함수에서 JSCreate 노드를 처리하는 부분입니다.) 지금은 확인만 하고 넘어가고 설명은 Root cause 부분 설명이 끝난 후 다시 언급하겠습니다.





Root cause





// static
NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe(
    JSHeapBroker* broker, Node* receiver, Node* effect,
    ZoneHandleSet<Map>* maps_return) {
  HeapObjectMatcher m(receiver);
  if (m.HasValue()) {
    HeapObjectRef receiver = m.Ref(broker);
    // We don't use ICs for the Array.prototype and the Object.prototype
    // because the runtime has to be able to intercept them properly, so
    // we better make sure that TurboFan doesn't outsmart the system here
    // by storing to elements of either prototype directly.
    //
    // TODO(bmeurer): This can be removed once the Array.prototype and
    // Object.prototype have NO_ELEMENTS elements kind.
    if (!receiver.IsJSObject() ||
        !broker->IsArrayOrObjectPrototype(receiver.AsJSObject())) {
      if (receiver.map().is_stable()) {
        // The {receiver_map} is only reliable when we install a stability
        // code dependency.
        *maps_return = ZoneHandleSet<Map>(receiver.map().object());
        return kUnreliableReceiverMaps;
      }
    }
  }
  InferReceiverMapsResult result = kReliableReceiverMaps;
  while (true) {
    switch (effect->opcode()) {
      case IrOpcode::kMapGuard: {
        Node* const object = GetValueInput(effect, 0);
        if (IsSame(receiver, object)) {
          *maps_return = MapGuardMapsOf(effect->op());
          return result;
        }
        break;
      }
      case IrOpcode::kCheckMaps: {
        Node* const object = GetValueInput(effect, 0);
        if (IsSame(receiver, object)) {
          *maps_return = CheckMapsParametersOf(effect->op()).maps();
          return result;
        }
        break;
      }
      case IrOpcode::kJSCreate: {
        if (IsSame(receiver, effect)) {
          base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver);
          if (initial_map.has_value()) {
            *maps_return = ZoneHandleSet<Map>(initial_map->object());
            return result;
          }
          // We reached the allocation of the {receiver}.
          return kNoReceiverMaps;
        }
        break;
      }



해당 부분은 patch 내용이 있던 InferReceiverMapsUnsafe() 함수의 내용입니다.

switch (effect->opcode()) switch로 effect가 어떠한 effect인지를 분류하여 나눠서 코드를 진행하는 모습을 볼 수 있습니다.
하지만 patch 부분은 Jscreate를 처리하는 부분에 존재했기 때문에 해당 부분만 확인하여 진행하도록 하겠습니다.

NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe(
    JSHeapBroker* broker, Node* receiver, Node* effect,
    ZoneHandleSet<Map>* maps_return) {
...
  InferReceiverMapsResult result = kReliableReceiverMaps;
  while (true) {
    switch (effect->opcode()) {
...
    }

    // Stop walking the effect chain once we hit the definition of
    // the {receiver} along the {effect}s.
    if (IsSame(receiver, effect)) return kNoReceiverMaps;

    // Continue with the next {effect}.
    DCHECK_EQ(1, effect->op()->EffectInputCount());
    effect = NodeProperties::GetEffectInput(effect);
  }
}



함수 내용에 대해 설명해보자면 InferReceiverMapsUnsafe()함수의 역할은 effect chain을 거슬러 올라가면서 receiver의 map을 추론하는 함수입니다.
InferReceiverMapsUnsafe()함수의 인자를 확인해보면 사전지식에서 공부했던 receiver와 effect를 확인할 수 있습니다.
해당 함수의 인자에 들어가는 receiver는 map을 추론할 receiver에 해당하는 node를 이야기하고 effect는 현재 탐색을 시작할 node입니다.

해당 함수의 결과로 반환되는 값은 총 3가지가 있는데 아래와 같습니다.

/* src/compiler/node-properties.h */

  // Walks up the {effect} chain to find a witness that provides map
  // information about the {receiver}. Can look through potentially
  // side effecting nodes.
  enum InferReceiverMapsResult {
    kNoReceiverMaps,         // No receiver maps inferred.
    kReliableReceiverMaps,   // Receiver maps can be trusted.
    kUnreliableReceiverMaps  // Receiver maps might have changed (side-effect).
  };



kNoReceiverMaps는 receiver의 map을 추론하지 못한 경우에 반환됩니다.
kReliableReceiverMaps는 receiver의 map이 안정적이며, 변경되지 않을 것으로 신뢰 할 수 있는 경우에 반환됩니다.
kUnreliableReceiverMaps는 receiver의 map이 변경되었거나 변경될 가능성이 있을 경우에 반환하게 됩니다.

아래는 취약점이 존재하는 부분인 Jscreate를 처리하는 부분입니다.

NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe(
    JSHeapBroker* broker, Node* receiver, Node* effect,
    ZoneHandleSet<Map>* maps_return) {
...
  InferReceiverMapsResult result = kReliableReceiverMaps; 
  while (true) {
    switch (effect->opcode()) { //effect가 무엇인지 확인 
...
      case IrOpcode::kJSCreate: { //Jscreate노드라면 아래 코드를 실행 
        if (IsSame(receiver, effect)) { //만약 receiver = effect라면 해당 receiver의 초기맵을 가져오려고 함
          base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver);
          if (initial_map.has_value()) {
            *maps_return = ZoneHandleSet<Map>(initial_map->object());
            return result; // 결과를 반환 
          }
          // We reached the allocation of the {receiver}.
          return kNoReceiverMaps; // 만약 초기맵을 가져오지 못했다면 KNoReceiverMaps를 반환함. 
        }
        break;
      }
...
    }
...
  }
}



(IsSame(receiver, effect) 부분에서 receiver = effect라면 해당 receiver의 초기맵을 가져오는 루틴을 수행하고 만약 같지 않다면 아래 코드를 실행합니다.

            // Continue search for receiver map outside the loop. Since operations
            // inside the loop may change the map, the result is unreliable.
   	        effect = GetEffectInput(effect, 0);
  	        result = kUnreliableReceiverMaps;
            continue;



해당 코드는 Effect chain 루프 부분이며 위치는 /src/compiler/node-properties.cc-443줄부터 존재합니다.
이 코드는 receiver와 effect가 같지 않을 때 실행이 되며 같아질때까지 실행되게 됩니다.
그 이유를 설명하자면 receiver와 effect가 같지 않다는 이야기는 아직 effect chain을 끝까지 거슬러 올라간 상태가 아닌 더 거슬러 올라갈 수 있다는 걸 의미하게 됩니다.
이 의미는 다른 effect 중에 map이 변경될수 있는 가능성이 존재할 수 있다는 이야기가 되어 모든 effect를 확인해야 합니다.
그래서 만약 receiver와 effect가 일치하지 않는다면 이 루프를 수행하게 됩니다.

InferReceiverMapsUnsafe() 함수는 MapInference 클래스의 생성자에서 호출되는데 해당 코드는 아래와 같습니다.

MapInference::MapInference(JSHeapBroker* broker, Node* object, Node* effect)
    : broker_(broker), object_(object) {
  ZoneHandleSet<Map> maps;
  auto result =
      NodeProperties::InferReceiverMapsUnsafe(broker_, object_, effect, &maps); //InferReceiverMapsUnsafe()함수의 결과를 반환받음.
  maps_.insert(maps_.end(), maps.begin(), maps.end()); // 반환받은 map을 저장함.
  maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) // 반환받은 map의 상태를 확인하여 kUnreliableDontNeedGuard 또는 kReliableOrGuarded로 처리함.
                    ? kUnreliableDontNeedGuard
                    : kReliableOrGuarded;
  DCHECK_EQ(maps_.empty(), result == NodeProperties::kNoReceiverMaps);
}



map_state 부분을 보면 InferReceiverMapsUnsafe함수의 반환 값에 따라 처리되는 값이 나눠지는 것을 볼 수 있습니다.
만약 결과가 kUnreliableReceiverMaps라면 kUnreliableDontNeedGuard 또는 kReliableOrGuarded로 나누어지는데 kUnreliableDontNeedGuard와 kReliableOrGuarded가 뭔지 설명하고 그 이유에 대해 설명해보겠습니다.

kUnreliableDontNeedGuard는 Map이 변경될 가능성이 있지만, 런타임 검사(Guard)를 추가하지 않아도 안전하다고 판단되는 상태를 이야기 합니다.
즉, map이 변경될 가능성은 있지만, 그 변경이 연산 결과에 영향을 미치지 않는다고 판단합니다.
이런 결과를 사용하는 이유는 Map이 변경될 가능성은 있지만, 이 변경이 최적화 코드의 동작에 영향을 미치지 않는다고 판단하여 허용 가능한 위험으로 간주하고 Guard를 생략하여 성능을 극대화하기 위함입니다.

kReliableOrGuarded는 Map이 안정적이거나, 런타임 검사(Guard)를 통해 안정성을 보장할 수 있는 상태를 이야기합니다.
즉, map이 변경되지 않을 것으로 신뢰할 수 있거나, 변경 가능성을 감지할 수 있는 Guard가 추가될 수 있음을 의미합니다.

kUnreliableDontNeedGuard는 Map이 변경될 가능성이 있지만, 런타임 검사(Guard)를 추가하지 않아도 안전하다고 판단되는 상태를 이야기 합니다.


라고 위에서 이야기 했었는데 이해하기 쉽게 예시를 들어 어떤 상황인지 보겠습니다.



Map 변경이 무해한 경우

1.

let arr = [1, 2, 3];
Object.defineProperty(arr, 'custom', { value: 42 }); // Map이 변경됨
arr.pop(); // 배열의 마지막 요소 제거 (map 변경과 무관하게 정상 작동)



두번째 줄이 있거나 없거나 arr 배열에 마지막 요소가 제거되는 건 같습니다. 즉 map이 변경되더라도 결과는 같습니다. 그러므로 map 변경이 무해하다고 판단하게 됩니다.

2.

let obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', { value: 3, writable: false }); // Map 변경
console.log(obj.a + obj.b); // 3 + 2 = 5 (map 변경과 무관하게 정상 작동)



a와 b를 더하고 출력하는데 있어 c가 추가되거나 추가되지 않아도 결과는 상관없습니다. 그러므로 map변경이 무해하다고 판단합니다.

※ 다른 예시들도 있지만 이 부분이 주가 아니기 때문에 생략하겠습니다.

위와 같은 경우일떄

 maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps)
                    ? kUnreliableDontNeedGuard
                    : kReliableOrGuarded;



map_state를 kUnreliableDontNeedGuard로 받을 수 있게 됩니다.

Map inference는 주로 최적화 과정 중 Inlining phase에서 JSCallReducer에 의해 수행이 되는데 JSCallReducer는 함수 호출을 최적화하는 역할을 합니다.
예를 들어, Array.prototype.pop()을 최적화하는 ReduceArrayPrototypePop() 함수에서 map inference를 수행하는 코드는 다음과 같습니다.

Reduction JSCallReducer::ReduceArrayPrototypePop(Node* node) {
...
  Node* receiver = NodeProperties::GetValueInput(node, 1);
  Node* effect = NodeProperties::GetEffectInput(node);
  Node* control = NodeProperties::GetControlInput(node);

  MapInference inference(broker(), receiver, effect); //inference 생성
  if (!inference.HaveMaps()) return NoChange();
  MapHandles const& receiver_maps = inference.GetMaps();

  std::vector<ElementsKind> kinds;
  if (!CanInlineArrayResizingBuiltin(broker(), receiver_maps, &kinds)) {
    return inference.NoChange();
  }
  if (!dependencies()->DependOnNoElementsProtector()) UNREACHABLE();
  inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &effect, //RelyOnMapsPreferStability()호출 
                                      control, p.feedback());
...
}



RelyOnMapsPreferStability()라는 inference의 함수를 호출하는 모습입니다.
해당 함수의 내용은 아래와 같습니다.

bool MapInference::RelyOnMapsPreferStability(
    CompilationDependencies* dependencies, JSGraph* jsgraph, Node** effect,
    Node* control, const FeedbackSource& feedback) {
  CHECK(HaveMaps());
  if (Safe()) return false; // safe() 함수의 결과에 따라 check 노드를 삽입하거나 삽입하지 않게 됨
  if (RelyOnMapsViaStability(dependencies)) return true;
  CHECK(RelyOnMapsHelper(nullptr, jsgraph, effect, control, feedback)); //check노드를 삽입하는 부분
  return false;
}



safe()함수가 반환하는 값에 따라서 아래의 RelyOnMapsHelper() 함수 부분에서 Mapchecks 노드를 삽입할지 안할지 여부가 결정됩니다.

safe()함수의 내용은 아래와 같습니다.

bool MapInference::Safe() const { return maps_state_ != kUnreliableNeedGuard; }



함수를 살펴보면 maps_state가 kUnreliableNeedGuard이 아니라면 true를 맞다면 false를 반환합니다.
RelyOnMapsPreferStability() 함수부분까지 풀어서 이야기를 해보면 maps_state = kUnreliableNeedGuard라면 false if (Safe()) return false; 부분에 의해 아래 Mapchecks 부분이 실행되게 되어 Runtime에 Map을 검사받게 되고 maps_state != kUnreliableNeedGuard이라면 safe()가 true가 되어 if (Safe()) return false; 부분이 false를 반환하게 되어 아래 Mapchecks 부분을 실행하지 않게 됩니다.
즉 Mapchecks노드를 삽입하지 않게 됩니다.

이때 중요한 점은 map_state가 2가지로 분류가 되었습니다.

 maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps)
                    ? kUnreliableDontNeedGuard
                    : kReliableOrGuarded;



즉 kReliableOrGuarded 같은 경우엔 Mapchecks 노드가 삽입이 되지 않는다는 이야기입니다.
Mapchecks노드가 삽입이 되지 않았다는 건 Runtime에서 해당 객체의 Map이 변경되어도 알 수 없다는 걸 뜻합니다.

이걸 조금 더 풀어 설명해보면
v8은 객체를 읽을 떄 map의 타입에 따라서 읽어오는 방법이 다릅니다.
아래는 v8 Map의 레이아웃입니다.

// Map layout:
// +---------------+------------------------------------------------+
// |   _ Type _    | _ Description _                                |
// +---------------+------------------------------------------------+
// | TaggedPointer | map - Always a pointer to the MetaMap root     |
// +---------------+------------------------------------------------+
// | Int           | The first int field                            |
//  `---+----------+------------------------------------------------+
//      | Byte     | [instance_size]                                |
//      +----------+------------------------------------------------+
//      | Byte     | If Map for a primitive type:                   |
//      |          |   native context index for constructor fn      |
//      |          | If Map for an Object type:                     |
//      |          |   inobject properties start offset in words    |
//      +----------+------------------------------------------------+
//      | Byte     | [used_or_unused_instance_size_in_words]        |
//      |          | For JSObject in fast mode this byte encodes    |
//      |          | the size of the object that includes only      |
//      |          | the used property fields or the slack size     |
//      |          | in properties backing store.                   |
//      +----------+------------------------------------------------+
//      | Byte     | [visitor_id]                                   |
// +----+----------+------------------------------------------------+
// | Int           | The second int field                           |
//  `---+----------+------------------------------------------------+
//      | Short    | [instance_type]                                |
//      +----------+------------------------------------------------+
//      | Byte     | [bit_field]                                    |
//      |          |   - has_non_instance_prototype (bit 0)         |
//      |          |   - is_callable (bit 1)                        |
//      |          |   - has_named_interceptor (bit 2)              |
//      |          |   - has_indexed_interceptor (bit 3)            |
//      |          |   - is_undetectable (bit 4)                    |
//      |          |   - is_access_check_needed (bit 5)             |
//      |          |   - is_constructor (bit 6)                     |
//      |          |   - has_prototype_slot (bit 7)                 |
//      +----------+------------------------------------------------+
//      | Byte     | [bit_field2]                                   |
//      |          |   - new_target_is_base (bit 0)                 |
//      |          |   - is_immutable_proto (bit 1)                 |
//      |          |   - unused bit (bit 2)                         |
//      |          |   - elements_kind (bits 3..7)                  |
// +----+----------+------------------------------------------------+
// | Int           | [bit_field3]                                   |
// |               |   - enum_length (bit 0..9)                     |
// |               |   - number_of_own_descriptors (bit 10..19)     |
// |               |   - is_prototype_map (bit 20)                  |
// |               |   - is_dictionary_map (bit 21)                 |
// |               |   - owns_descriptors (bit 22)                  |
// |               |   - is_in_retained_map_list (bit 23)           |
// |               |   - is_deprecated (bit 24)                     |
// |               |   - is_unstable (bit 25)                       |
// |               |   - is_migration_target (bit 26)               |
// |               |   - is_extensible (bit 28)                     |
// |               |   - may_have_interesting_symbols (bit 28)      |
// |               |   - construction_counter (bit 29..31)          |
// |               |                                                |
// +****************************************************************+
// | Int           | On systems with 64bit pointer types, there     |
// |               | is an unused 32bits after bit_field3           |
// +****************************************************************+
// | TaggedPointer | [prototype]                                    |
// +---------------+------------------------------------------------+
// | TaggedPointer | [constructor_or_backpointer_or_native_context] |
// +---------------+------------------------------------------------+
// | TaggedPointer | [instance_descriptors]                         |
// +****************************************************************+
// ! TaggedPointer ! [layout_descriptors]                           !
// !               ! Field is only present if compile-time flag     !
// !               ! FLAG_unbox_double_fields is enabled            !
// !               ! (basically on 64 bit architectures)            !
// +****************************************************************+
// | TaggedPointer | [dependent_code]                               |
// +---------------+------------------------------------------------+
// | TaggedPointer | [prototype_validity_cell]                      |
// +---------------+------------------------------------------------+
// | TaggedPointer | If Map is a prototype map:                     |
// |               |   [prototype_info]                             |
// |               | Else:                                          |
// |               |   [raw_transitions]                            |
// +---------------+------------------------------------------------+



그렇기 때문에 타입이 SMI일 떄와 Double일떄 필드를 읽어오는 방법이 다릅니다.
이 뜻은 처음에 SMI의 Map을 가지고 있던 객체가 Runtime에 Jscreate를 통해 Map이 Double로 변경되게 되었지만 TurboFan은 Mapchecks노드가 없기 때문에 Map이 Double로 변경된걸 모르는 상태로 Double로 설정되어 있는 값을 SMI 형태로 접근하게 되어 Type confusion이 발생할 수 있게 됩니다.





patch2





2020_3.png



패치부분에서 result = kUnreliableReceiverMaps; 를 추가하여 다시 한번 Map을 Check할 수 있게끔 하였습니다.





Reference


https://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreate