漏洞分析
也就是说这个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看不到我想要的过程,所以只能一点点调试。
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 the graph.
这个漏洞patch方法是主动将一些节点先行转为dead value,所以可以预料到只要能先把非effect chain上的一些节点转为dead value,就不会造成运行到unreachable的情况,说到这里,我们先要看为什么在这会插入unreachable。
// 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)。
(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));
})();
(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));
})();
最终经过阅读最后形成的代码,我发现对于不成功触发的情况,(因为显然是return全变为throw了),最后的结果要么是走到unreachable,要么是deopt,因为unreachable走不到(正常情况下),所以每次都会deopt,从而走正确的流程,下面是patch后的v8运行poc,加了输出deopt的参数。
而未patch的运行poc。
看完未patch版本最终生成的代码后,其整体流程其实也是要么走向unreachbale,要么走向deopt,但是大部分情况都会走向unreachable,且看运行结果也发现是直接走到unreachable了,所以最终导致的结果应该是,本该走向deopt的情况,走向了unreachable。
基本上二者从EffectLinearization开始出现差别。
生成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)。
可以看到左侧(patch前),#107 dead value已经没了,右侧倒是还有div0转的dead value。
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) {
虽然没有找到合适的利用链但是不排除能成功利用的情况,在这里(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换为别的类型混淆利用方式,比如用其他类型对象实现越界读写,那么我们就能造出一个越界数组来,但是这只是理论方面的想法。
往期 · 推荐
原文始发于微信公众号(星阑科技):【技术干货】Chrome-V8 CVE-2021-30588