issue(https://bugs.chromium.org/p/chromium/issues/detail?id=1195650),这是去年公布的一个1day,我们来看一下详细情况。
漏洞分析
经过调试poc以及观察turbolizer,我发现主要是在不合适的地方插入了unreachable节点,导致运行时中断。
而这里
(https://docs.google.com/presentation/d/1sOEF4MlF7LeO7uq-uThJSulJlTh–wgLeaVibsbb3tc/edit#slide=id.g549957988_0383)有说到。
也就是说这个unreachable是和dead node相关的,我们把目光锁定在SimplifedLowing阶段,因为通过查看turbolizer以及看issue界面得到的信息发现错误出现在此阶段。
所以我先把目光放在SimplifiedLowering中在SpeculativeToNumber之后插入Unreachable的部分。
template <Phase T>
void VisitNode(Node* node, Truncation truncation,
SimplifiedLowering* lowering) {
tick_counter_->TickAndMaybeEnterSafepoint();
// Unconditionally eliminate unused pure nodes (only relevant if there's
// a pure operation in between two effectful ones, where the last one
// is unused).
// Note: We must not do this for constants, as they are cached and we
// would thus kill the cached {node} during lowering (i.e. replace all
// uses with Dead), but at that point some node lowering might have
// already taken the constant {node} from the cache (while it was not
// yet killed) and we would afterwards replace that use with Dead as well.
if (node->op()->ValueInputCount() > 0 &&
node->op()->HasProperty(Operator::kPure) && truncation.IsUnused()) {
return VisitUnused<T>(node); //调用的这个函数里面打了patch
}
if (lower<T>()) InsertUnreachableIfNecessary<T>(node); //这里打了patch,且InsertUnreachableIfNecessary里也打了patch,从SpeculativeToNumber变为unreachable就是在这个函数内
switch (node->opcode()) {
[ ... ]
case IrOpcode::kSpeculativeToNumber: {
NumberOperationParameters const& p =
NumberOperationParametersOf(node->op());
switch (p.hint()) {
case NumberOperationHint::kSignedSmall:
case NumberOperationHint::kSignedSmallInputs:
VisitUnop<T>(node,
CheckedUseInfoAsWord32FromHint(
p.hint(), kDistinguishZeros, p.feedback()),
MachineRepresentation::kWord32, Type::Signed32());
break;
case NumberOperationHint::kNumber:
case NumberOperationHint::kNumberOrBoolean:
case NumberOperationHint::kNumberOrOddball: //这里会将其换为float64
VisitUnop<T>(
node, CheckedUseInfoAsFloat64FromHint(p.hint(), p.feedback()),
MachineRepresentation::kFloat64);
break;
}
if (lower<T>()) DeferReplacement(node, node->InputAt(0));
return;
}
另通过观察patch发现和DeferReplacement也有关系,有patch。
之所以比较麻烦是因为在运行时直接–trace-representation看不到我想要的过程,所以只能一点点调试。
从调试结果来看,是先插入的unreachable,后经下面转的checkedTaggedToFloat64,从节点顺序来看也是如此,不过这就无法作为插入unreachable的原因了,所以要看patch的其他部分。
经调试发现也不会先进入VisitUnused,此外,在插入unreachable之前,不会到达patch的任何其他段代码处,所以目前锁定问题出现在。
diff --git a/src/compiler/simplified-lowering.cc b/src/compiler/simplified-lowering.cc
index 23e0006..a71e627 100644
--- a/src/compiler/simplified-lowering.cc
+++ b/src/compiler/simplified-lowering.cc
@@ -1960,7 +1975,32 @@
return VisitUnused<T>(node);
}
- if (lower<T>()) InsertUnreachableIfNecessary<T>(node);
+ if (lower<T>()) {
+ // Kill non-effectful operations that have a None-type input and are thus
+ // dead code. Otherwise we might end up lowering the operation in a way,
+ // e.g. by replacing it with a constant, that cuts the dependency on a
+ // deopting operation (the producer of the None type), possibly resulting
+ // in a nonsense schedule.
+ if (node->op()->EffectOutputCount() == 0 &&
+ node->op()->ControlOutputCount() == 0 &&
+ node->opcode() != IrOpcode::kDeadValue &&
+ node->opcode() != IrOpcode::kStateValues &&
+ node->opcode() != IrOpcode::kFrameState &&
+ node->opcode() != IrOpcode::kPhi) {
+ for (int i = 0; i < node->op()->ValueInputCount(); i++) {
+ Node* input = node->InputAt(i);
+ if (TypeOf(input).IsNone()) {
+ MachineRepresentation rep = GetInfo(node)->representation();
+ DeferReplacement(
+ node,
+ graph()->NewNode(jsgraph_->common()->DeadValue(rep), input));
+ return;
+ }
+ }
+ } else {
+ InsertUnreachableIfNecessary<T>(node);
+ }
+ }
可以看到是多了一个判断条件,也就是说,原本不该进入InsertUnreachableIfNecessary的情况进入了,所以也就是在不该插入的时候插入了,这个特殊的情况,可以用各种技巧构造出来,作为比较,拿InsertUnreachableIfNecessary来看。
template <>
void RepresentationSelector::InsertUnreachableIfNecessary<LOWER>(Node* node) {
// If the node is effectful and it produces an impossible value, then we
// insert Unreachable node after it.
if (node->op()->ValueOutputCount() > 0 &&
node->op()->EffectOutputCount() > 0 &&
node->opcode() != IrOpcode::kUnreachable && TypeOf(node).IsNone()) {
Node* control = (node->op()->ControlOutputCount() == 0)
? NodeProperties::GetControlInput(node, 0)
: NodeProperties::FindSuccessfulControlProjection(node);
Node* unreachable =
graph()->NewNode(common()->Unreachable(), node, control);
// Insert unreachable node and replace all the effect uses of the {node}
// with the new unreachable node.
for (Edge edge : node->use_edges()) {
if (!NodeProperties::IsEffectEdge(edge)) continue;
// Make sure to not overwrite the unreachable node's input. That would
// create a cycle.
if (edge.from() == unreachable) continue;
// Avoid messing up the exceptional path.
if (edge.from()->opcode() == IrOpcode::kIfException) {
DCHECK(!node->op()->HasProperty(Operator::kNoThrow));
DCHECK_EQ(NodeProperties::GetControlInput(edge.from()), node);
continue;
}
edge.UpdateTo(unreachable);
}
}
}
可以看到,曾经能走入InsertUnreachableIfNecessary并成功插入unreach节点的node,不满足diff中新加的判断,也就是,原本能向内完成插入的节点还会往后走,那么再仔细看下patch,可以看到,是对另外的一些节点,主动将其变为DeadValue,所以猜测是有些节点未及时变为deadvalue而导致的问题,是哪些节点呢,是pure dead operation,除了kDeadValue,kStateValues,kFrameState,kPhi之外的operation。
关于dead code,其实含义很广泛,可以说是不可能执行的代码,或者更恰当的是不可能执行的分支,都是代表这段代码可以被消除的含义,我们可以看下dead-code-elimination.h中如何消除dead value的注释。
// Propagates {Dead} control and {DeadValue} values through the graph and
// thereby removes dead code.
// We detect dead values based on types, replacing uses of nodes with
// {Type::None()} with {DeadValue}. A pure node (other than a phi) using
// {DeadValue} is replaced by {DeadValue}. When {DeadValue} hits the effect
// chain, a crashing {Unreachable} node is inserted and the rest of the effect
// chain is collapsed. We wait for the {EffectControlLinearizer} to connect
// {Unreachable} nodes to the graph end, since this is much easier if there is
// no floating control.
// {DeadValue} has an input, which has to have {Type::None()}. This input is
// important to maintain the dependency on the cause of the unreachable code.
// {Unreachable} has a value output and {Type::None()} so it can be used by
// {DeadValue}.
// {DeadValue} nodes track a {MachineRepresentation} so they can be lowered to a
// value-producing node. {DeadValue} has the runtime semantics of crashing and
// behaves like a constant of its representation so it can be used in gap moves.
// Since phi nodes are the only remaining use of {DeadValue}, this
// representation is only adjusted for uses by phi nodes.
// In contrast to {DeadValue}, {Dead} can never remain in th
可以看到,dead value是不该在effect chain中的,因为如此会导致unreachable节点的插入effect chain中,从而导致crash,effect chain可以理解为对于执行流中的有先后顺序操作的限制顺序。
这个漏洞patch方法是主动将一些节点先行转为dead value,所以可以预料到只要能先把非effect chain上的一些节点转为dead value,就不会造成运行到unreachable的情况,说到这里,我们先要看为什么在这会插入unreachable。
这是在InsertUnreachableIfNecessary内部,而调用这个函数可以说没什么判断,但是在函数内有许多判断需要满足,直观来说就是。
// If the node is effectful and it produces an impossible value, then we
// insert Unreachable node after it.
if (node->op()->ValueOutputCount() > 0 &&
node->op()->EffectOutputCount() > 0 &&
node->opcode() != IrOpcode::kUnreachable && TypeOf(node).IsNone()) {
再结合turbolizer图(TFEscapeAnalysis)。
那么满足判断之后就会走入下面插入unreachable的部分。
再看poc
(function() {
function foo(a) {
let y = Math.min(Infinity ? [] : Infinity, -0) / 0;
if (a) y = 1.1;
return y ? 1 : 0;
}
%PrepareFunctionForOptimization(foo);
print(foo(false));
%OptimizeFunctionOnNextCall(foo);
print(foo(false));
})();
其中[]、”之类的是SpeculativeToNumber节点的必须,#74的heapconstant就是这个。
我们看修复之后的版本:
很明显的是虽然还是会生成unreachable以及checkedTaggedToFloat64,但是也可以看到生成了很多的DeadValue,也就是虽然生成Unreachable的分支判断依然可以运行,但是在此之外多出了一些生成dead value的过程,patch也正是改动的这一点,在遍历节点时,主动检查是否应转为dead value。
我们看一下最终生成代码的差别。
右边是patch后的版本。
为了辅助定位触发的int3是哪条代码里的,我修改了一下。
(function() {
function foo(a) {
let y = Math.min(Infinity ? [] : Infinity, -0) / 0;
console.log("hi");
if (a) y = 1.1;
return y ? 1 : 0;
}
%PrepareFunctionForOptimization(foo);
print(foo(false));
%OptimizeFunctionOnNextCall(foo);
print(foo(false));
})();
显然是在let y = Math.min(Infinity ? [] : Infinity, -0) / 0;中直接break,也就是说确实是插入的那个unreachable起了作用,后发现是其旁边的一个unreachable起了作用。
另外经过一些尝试,我发现patch达到的效果和poc中将除操作删去效果一样,都是在后面加了几个dead value,(min操作没有必要,转为dead value)然后后面的操作就比较正常了。
最终经过阅读最后形成的代码,我发现对于不成功触发的情况,(因为显然是return全变为throw了),最后的结果要么是走到unreachable,要么是deopt,因为unreachable走不到(正常情况下),所以每次都会deopt,从而走正确的流程,下面是patch后的v8运行poc,加了输出deopt的参数。
而未patch的运行poc。
看完未patch版本最终生成的代码后,其整体流程其实也是要么走向unreachbale,要么走向deopt,但是大部分情况都会走向unreachable,且看运行结果也发现是直接走到unreachable了,所以最终导致的结果应该是,本该走向deopt的情况,走向了unreachable。
基本上二者从EffectLinearization开始出现差别。
是因为,左侧patch前的是在和##104节点差距离较远的一个dead value生成的##190 unreachable,右侧patch后的是由和##104算是有点关系的几个dead value生成的unreachable。
生成unreachable在这里。
Node* EffectControlLinearizer::LowerDeadValue(Node* node) {
Node* input = NodeProperties::GetValueInput(node, 0);
if (input->opcode() != IrOpcode::kUnreachable) {
// There is no fundamental reason not to connect to end here, except it
// integrates into the way the graph is constructed in a simpler way at
// this point.
// TODO(jgruber): Connect to end here as well.
Node* unreachable = __ UnreachableWithoutConnectToEnd();
NodeProperties::ReplaceValueInput(node, unreachable, 0); //dead value上会插入unreachable
}
return gasm()->AddNode(node);
}
造成的结果就是在最后,##104旁的unreachable有无与effectPhi有直接联系。
而这个effectPhi又是与一个DeoptimizeUnless节点直接关联的,也就是本来会把unreachable安排在deopt后面的,然而因为那些dead avlue没有在正确的地方生成,导致代码组织出了错误,使得从dead value衍生出来的unreachable节点与上面的一个DeoptmizeUnless断了联系,从而越过Deopt直接到达Unreachable,然后crash。
在未patch版中虽然simplifiedLowering阶段也把除0操作变为了dead value,但是还是保留的float64Min节点(原NumberMin)。
导致此处dead value(div0)早一步被消除。
可以看到左侧(patch前),#107 dead value已经没了,右侧倒是还有div0转的dead value。
对应的numberMin变Float64Min逻辑在。
case IrOpcode::kNumberMin: {
// It is safe to use the feedback types for left and right hand side
// here, since we can only narrow those types and thus we can only
// promise a more specific truncation.
// For NumberMin we generally propagate whether the truncation
// identifies zeros to the inputs, and we choose to ignore minus
// zero in those cases.
Type const lhs_type = TypeOf(node->InputAt(0));
Type const rhs_type = TypeOf(node->InputAt(1));
[ ... ]
} else {
VisitBinop<T>(node,
UseInfo::TruncatingFloat64(truncation.identify_zeros()),
MachineRepresentation::kFloat64);
if (lower<T>()) {
// If the left hand side is not NaN, and the right hand side
// is not NaN (or -0 if the difference between the zeros is
// observed), we can do a simple floating point comparison here.
if (lhs_type.Is(Type::OrderedNumber()) &&
rhs_type.Is(truncation.IdentifiesZeroAndMinusZero()
? Type::OrderedNumber()
: Type::PlainNumber())) {
lowering->DoMin(node,
lowering->machine()->Float64LessThanOrEqual(),
MachineRepresentation::kFloat64);
} else {
ChangeOp(node, Float64Op(node));
}
}
}
return;
}
对于这次情况就是NumberMin的左是SpeculativeToNumber,右是-0,所以会变为Float64Min,然而patch后的版本会在走到这里之前,先对其进行判断,转为Dead Value。
# 对应这里的判断
+ if (node->op()->EffectOutputCount() == 0 &&
+ node->op()->ControlOutputCount() == 0 &&
+ node->opcode() != IrOpcode::kDeadValue &&
+ node->opcode() != IrOpcode::kStateValues &&
+ node->opcode() != IrOpcode::kFrameState &&
+ node->opcode() != IrOpcode::kPhi) {
从而导致没有在靠近#104 unreachable位置衍生出unreachable(因为这里没有一个dead value),倒是在上面有一个原本应当放在div0化成的dead value后面的一个#110 dead value衍生了unreachable,在右侧对应#109,也就是原本。
这里的#109,但是这个节点因为前面的关系原因,没有联系到下面#104 unreachable这里,所以造成了上面说的最终结果。
虽然没有找到合适的利用链但是不排除能成功利用的情况,在这里(https://bugs.chromium.org/p/chromium/issues/detail?id=1195650#c31),有半任意地址读的poc,但是离构造出越界数组,仍有一段路要走,另外还有一个poc能把本该返回false的改成true,稍加改变就能使得本该true的被turbofan优化成了false,但是此版本消除check bound已经不再能使用(也不一定),并且那个利用参杂着另外一个漏洞点,在作者用的这个版本(https://crrev.com/b2ae9951d4a12b996532022959f44a0cd10184ec)上才能成功。
但是也不是完全没有思路,我们可以看到他是越过DeoptimizeUnless直接走到本该在DeoptimizeUnless后面的unreachable,所以我们如果可以把此处的unreachable换为别的类型混淆利用方式,比如用其他类型对象实现越界读写,那么我们就能造出一个越界数组来,但是这只是理论方面的想法。