CVE-2022-3723 分析
终于有时间了,继续分析 v8 漏洞,还是从 google p0 分析的漏洞里选一个 https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2022/CVE-2022-3723.html,从原 POST 可以拿到漏洞的简单分析和 POC.
分开来看 POC, 先看
var o = {
inner: {
['foo']: 0
}
};
./d8.exe --trace_turbo --expose-gc --allow-natives-syntax ./poc.js
可以拿到 trace log, 放到 https://v8.github.io/tools/head/turbolizer/
可以看到编译中各步骤的输出, 字节码如下(Debug 版 d8 的 trace 才有字节码)
1. CreateObjectLiteral [0], [0], #8
2. Star2
3. CreateObjectLiteral [1], [1], #41
4. Star3
5. LdaConstant [2]
6. Star4
7. LdaZero
8. DefineKeyedOwnPropertyInLiteral r3, r4, #0, [2]
9. Ldar r3
10. DefineNamedOwnProperty r2, [3], [4]
11. Mov r2, r0
...
Constant pool (size = 5)
0: 0x01fc00219c89 <ObjectBoilerplateDescription[3]>
1: 0x01fc000034d1 <ObjectBoilerplateDescription[1]>
2: 0x01fc00219c51 <String[3]: #foo>
3: 0x01fc002198f9 <String[5]: #inner>
4: 0x01fc00219855 <String[16]: #setInnerProperty>
在 interpreter\bytecodes.h 找到字节码的参数,结合 interpreter\interprete-generator.cc 中的字节码解释器代码, 可以大致读懂这些操作。
1 - 4 就是创建了两个 Object, 5-8 是设置一个对象的属性, 翻看 DefineKeyedOwnPropertyInLiteral 的实现以后,根据参数可以看到是在设置 foo 属性, 那肯定是在设置 inner 对象的属性, 后面 9-10 就是在设置 o 对象的 inner 这个属性。
V(CreateObjectLiteral, ImplicitRegisterUse::kWriteAccumulator, \
OperandType::kIdx, OperandType::kIdx, OperandType::kFlag8) \
...
V(DefineNamedOwnProperty, ImplicitRegisterUse::kReadWriteAccumulator, \
OperandType::kReg, OperandType::kIdx, OperandType::kIdx) \
...
V(DefineKeyedOwnPropertyInLiteral, ImplicitRegisterUse::kReadAccumulator, \
OperandType::kReg, OperandType::kReg, OperandType::kFlag8, \
OperandType::kIdx) \
// CreateObjectLiteral <element_idx> <literal_idx> <flags>
//
// Creates an object literal for literal index <literal_idx> with
// CreateObjectLiteralFlags <flags> and constant elements in <element_idx>.
IGNITION_HANDLER(CreateObjectLiteral, InterpreterAssembler) {
TNode<HeapObject> feedback_vector = LoadFeedbackVector();
TNode<TaggedIndex> slot = BytecodeOperandIdxTaggedIndex(1);
TNode<Uint32T> bytecode_flags = BytecodeOperandFlag8(2);
Label if_fast_clone(this), if_not_fast_clone(this, Label::kDeferred);
// No feedback, so handle it as a slow case.
GotoIf(IsUndefined(feedback_vector), &if_not_fast_clone);
// Check if we can do a fast clone or have to call the runtime.
Branch(IsSetWord32<CreateObjectLiteralFlags::FastCloneSupportedBit>(
bytecode_flags),
&if_fast_clone, &if_not_fast_clone);
BIND(&if_fast_clone);
{
// If we can do a fast clone do the fast-path in CreateShallowObjectLiteral.
ConstructorBuiltinsAssembler constructor_assembler(state());
TNode<HeapObject> result = constructor_assembler.CreateShallowObjectLiteral(
CAST(feedback_vector), slot, &if_not_fast_clone);
SetAccumulator(result);
Dispatch();
}
BIND(&if_not_fast_clone);
{
// If we can't do a fast clone, call into the runtime.
TNode<ObjectBoilerplateDescription> object_boilerplate_description =
CAST(LoadConstantPoolEntryAtOperandIndex(0));
TNode<Context> context = GetContext();
TNode<UintPtrT> flags_raw =
DecodeWordFromWord32<CreateObjectLiteralFlags::FlagsBits>(
bytecode_flags);
TNode<Smi> flags = SmiTag(Signed(flags_raw));
TNode<Object> result =
CallRuntime(Runtime::kCreateObjectLiteral, context, feedback_vector,
slot, object_boilerplate_description, flags);
SetAccumulator(result);
// TODO(klaasb) build a single dispatch once the call is inlined
Dispatch();
}
}
// DefineKeyedOwnPropertyInLiteral <object> <name> <flags> <slot>
//
// Define a property <name> with value from the accumulator in <object>.
// Property attributes and whether set_function_name are stored in
// DefineKeyedOwnPropertyInLiteralFlags <flags>.
//
// This definition is not observable and is used only for definitions
// in object or class literals.
IGNITION_HANDLER(DefineKeyedOwnPropertyInLiteral, InterpreterAssembler) {
TNode<Object> object = LoadRegisterAtOperandIndex(0);
TNode<Object> name = LoadRegisterAtOperandIndex(1);
TNode<Object> value = GetAccumulator();
TNode<Smi> flags =
SmiFromInt32(UncheckedCast<Int32T>(BytecodeOperandFlag8(2)));
TNode<TaggedIndex> slot = BytecodeOperandIdxTaggedIndex(3);
TNode<HeapObject> feedback_vector = LoadFeedbackVector();
TNode<Context> context = GetContext();
CallRuntime(Runtime::kDefineKeyedOwnPropertyInLiteral, context, object, name,
value, flags, feedback_vector, slot);
Dispatch();
}
class InterpreterSetNamedPropertyAssembler : public InterpreterAssembler {
public:
InterpreterSetNamedPropertyAssembler(CodeAssemblerState* state,
Bytecode bytecode,
OperandScale operand_scale)
: InterpreterAssembler(state, bytecode, operand_scale) {}
void SetNamedProperty(Callable ic, NamedPropertyType property_type) {
TNode<Object> object = LoadRegisterAtOperandIndex(0);
TNode<Name> name = CAST(LoadConstantPoolEntryAtOperandIndex(1));
TNode<Object> value = GetAccumulator();
TNode<TaggedIndex> slot = BytecodeOperandIdxTaggedIndex(2);
TNode<HeapObject> maybe_vector = LoadFeedbackVector();
TNode<Context> context = GetContext();
TNode<Object> result =
CallStub(ic, context, object, name, value, slot, maybe_vector);
// To avoid special logic in the deoptimizer to re-materialize the value in
// the accumulator, we overwrite the accumulator after the IC call. It
// doesn't really matter what we write to the accumulator here, since we
// restore to the correct value on the outside. Storing the result means we
// don't need to keep unnecessary state alive across the callstub.
SetAccumulator(result);
Dispatch();
}
};
// SetNamedProperty <object> <name_index> <slot>
//
// Calls the StoreIC at FeedBackVector slot <slot> for <object> and
// the name in constant pool entry <name_index> with the value in the
// accumulator.
IGNITION_HANDLER(SetNamedProperty, InterpreterSetNamedPropertyAssembler) {
// StoreIC is currently a base class for multiple property store operations
// and contains mixed logic for named and keyed, set and define operations,
// the paths are controlled by feedback.
// TODO(v8:12548): refactor SetNamedIC as a subclass of StoreIC, which can be
// called here.
Callable ic = Builtins::CallableFor(isolate(), Builtin::kStoreIC);
SetNamedProperty(ic, NamedPropertyType::kNotOwn);
}
// DefineNamedOwnProperty <object> <name_index> <slot>
//
// Calls the DefineNamedOwnIC at FeedBackVector slot <slot> for <object> and
// the name in constant pool entry <name_index> with the value in the
// accumulator.
IGNITION_HANDLER(DefineNamedOwnProperty, InterpreterSetNamedPropertyAssembler) {
Callable ic = Builtins::CallableFor(isolate(), Builtin::kDefineNamedOwnIC);
SetNamedProperty(ic, NamedPropertyType::kOwn);
}
在继续看 turbolizer 里的各个优化步骤, 找一些关键步骤,没找到什么有用的信息。再看一下 setInnerProperty 的 trace 日志。
1. GetNamedProperty a0, [0], [0]
2. Star0
3. CreateEmptyObjectLiteral
4. SetNamedProperty r0, [1], [2]
5. LdaUndefined
6. Return
Constant pool (size = 2)
0: 0x01fc002198f9 <String[5]: #inner>
1: 0x01fc00219c51 <String[3]: #foo
结合之前在 0-days in the wild blog 读到的分析,可以确定这个地方就是漏洞的直接触发点,给 inner 对象的 foo 属性赋值的地方,本来是有一个 CheckMap 保证 inner 对象的类型符合预期的,但是由于漏洞,这个 CheckMap 在 LoadElimination 阶段被错误的优化掉了。
其实这些编译优化阶段,不负责任的简化来说,就是对 IR 以各种顺序做遍历分析,然后在根据分析结果对 IR 进行修改。下面我们看一下 LoadElimination 这个阶段与漏洞相关的逻辑。

LoadElimination 阶段 32:ChekMaps 被认为是冗余的,所以给删除掉了。
// 所有结点都会按顺序调用这个 Reduce 分析处理
Reduction LoadElimination::Reduce(Node* node) {
if (v8_flags.trace_turbo_load_elimination) {
// code related to output trace log, we just ignore them
}
// 根据结点类型分发给不同的函数处理
switch (node->opcode()) {
case IrOpcode::kMapGuard:
return ReduceMapGuard(node);
case IrOpcode::kCheckMaps:
return ReduceCheckMaps(node); // 这个是处理 CheckMaps 结点的
case IrOpcode::kCompareMaps:
return ReduceCompareMaps(node);
case IrOpcode::kEnsureWritableFastElements:
return ReduceEnsureWritableFastElements(node);
case IrOpcode::kMaybeGrowFastElements:
return ReduceMaybeGrowFastElements(node);
case IrOpcode::kTransitionElementsKind:
return ReduceTransitionElementsKind(node);
case IrOpcode::kLoadField:
return ReduceLoadField(node, FieldAccessOf(node->op())); // 这个处理 CheckMaps 依赖的 LoadFiled 结点
case IrOpcode::kStoreField:
return ReduceStoreField(node, FieldAccessOf(node->op()));
case IrOpcode::kLoadElement:
return ReduceLoadElement(node);
case IrOpcode::kStoreElement:
return ReduceStoreElement(node);
case IrOpcode::kTransitionAndStoreElement:
return ReduceTransitionAndStoreElement(node);
case IrOpcode::kStoreTypedElement:
return ReduceStoreTypedElement(node);
case IrOpcode::kEffectPhi:
return ReduceEffectPhi(node);
case IrOpcode::kDead:
break;
case IrOpcode::kStart:
return ReduceStart(node);
default:
return ReduceOtherNode(node); // 还有其他很多普通结点是用这个函数处理
}
return NoChange();
}
先从 ReduceCheckMaps 开始分析,看下什么情况下 CheckMaps 会被删除。
Reduction LoadElimination::ReduceCheckMaps(Node* node) {
// 可以推测 op 里存放了要检查的预期 map
ZoneHandleSet<Map> const& maps = CheckMapsParametersOf(node->op()).maps();
// 看来 CheckMaps 有一个 value input 和 一个 effect input
Node* const object = NodeProperties::GetValueInput(node, 0);
Node* const effect = NodeProperties::GetEffectInput(node);
// 这个 node_states_ 应该是 LoadElimination 这个阶段分析出的状态的集合
// 这里可以看到是取 effect input 结点的状态
AbstractState const* state = node_states_.Get(effect);
if (state == nullptr) return NoChange();
ZoneHandleSet<Map> object_maps;
if (state->LookupMaps(object, &object_maps)) {
if (maps.contains(object_maps)) return Replace(effect);
// TODO(turbofan): Compute the intersection.
}
state = state->SetMaps(object, maps, zone());
return UpdateState(node, state);
}
node_states_ 基本上就是一个存放各结点 state 的容器
class AbstractStateForEffectNodes final : public ZoneObject {
public:
explicit AbstractStateForEffectNodes(Zone* zone) : info_for_node_(zone) {}
AbstractState const* Get(Node* node) const;
void Set(Node* node, AbstractState const* state);
Zone* zone() const { return info_for_node_.get_allocator().zone(); }
private:
ZoneVector<AbstractState const*> info_for_node_;
};
LoadElimination::AbstractState const*
LoadElimination::AbstractStateForEffectNodes::Get(Node* node) const {
size_t const id = node->id();
if (id < info_for_node_.size()) return info_for_node_[id];
return nullptr;
}
void LoadElimination::AbstractStateForEffectNodes::Set(
Node* node, AbstractState const* state) {
size_t const id = node->id();
if (id >= info_for_node_.size()) info_for_node_.resize(id + 1, nullptr);
info_for_node_[id] = state;
}
在 turbolizer 里可以看到 CheckMaps 的 effect input 是上面的 19:CheckPoint, CheckPoint 没有自己的分析函数,所以是用 ReduceOtherNode 来处理
Reduction LoadElimination::ReduceOtherNode(Node* node) {
if (node->op()->EffectInputCount() == 1) {
if (node->op()->EffectOutputCount() == 1) {
// effect input 和 output 都只有 1 个的结点
Node* const effect = NodeProperties::GetEffectInput(node);
AbstractState const* state = node_states_.Get(effect);
// If we do not know anything about the predecessor, do not propagate
// just yet because we will have to recompute anyway once we compute
// the predecessor.
if (state == nullptr) return NoChange();
// Check if this {node} has some uncontrolled side effects.
if (!node->op()->HasProperty(Operator::kNoWrite)) {
state = state->KillAll(zone());
}
// 这里就是把 state 跟当前结点关联起来
return UpdateState(node, state);
} else {
// Effect terminators should be handled specially.
return NoChange();
}
}
DCHECK_EQ(0, node->op()->EffectInputCount());
DCHECK_EQ(0, node->op()->EffectOutputCount());
return NoChange();
}
Reduction LoadElimination::UpdateState(Node* node, AbstractState const* state) {
AbstractState const* original = node_states_.Get(node);
// Only signal that the {node} has Changed, if the information about {state}
// has changed wrt. the {original}.
if (state != original) {
if (original == nullptr || !state->Equals(original)) {
node_states_.Set(node, state);
return Changed(node);
}
}
return NoChange();
}
可以看到如果结点的 effect input 的输入和输出数量都是 1,而且没有写的副作用的话,就可以把状态通过 effet chain 传递下去。那么 CheckPoint 结点是不是呢,推测应该是,但是我们要验证一下,看一下 CheckPoint 结点的属性,确实是这样的,可以在 common-operator.cc 找到。
// Name, properties, value_input_count, effect_input_count, ...
#define COMMON_CACHED_OP_LIST(V) \
V(Plug, Operator::kNoProperties, 0, 0, 0, 1, 0, 0) \
V(Dead, Operator::kFoldable, 0, 0, 0, 1, 1, 1) \
V(Unreachable, Operator::kFoldable, 0, 1, 1, 1, 1, 0) \
V(IfTrue, Operator::kKontrol, 0, 0, 1, 0, 0, 1) \
V(IfFalse, Operator::kKontrol, 0, 0, 1, 0, 0, 1) \
V(IfSuccess, Operator::kKontrol, 0, 0, 1, 0, 0, 1) \
V(IfException, Operator::kKontrol, 0, 1, 1, 1, 1, 1) \
V(Throw, Operator::kKontrol, 0, 1, 1, 0, 0, 1) \
V(Terminate, Operator::kKontrol, 0, 1, 1, 0, 0, 1) \
V(LoopExit, Operator::kKontrol, 0, 0, 2, 0, 0, 1) \
V(LoopExitEffect, Operator::kNoThrow, 0, 1, 1, 0, 1, 0) \
V(Checkpoint, Operator::kKontrol, 0, 1, 1, 0, 1, 0) \
V(FinishRegion, Operator::kKontrol, 1, 1, 0, 1, 1, 0) \
V(Retain, Operator::kKontrol, 1, 1, 0, 0, 1, 0)
struct CommonOperatorGlobalCache final {
#define CACHED(Name, properties, value_input_count, effect_input_count, \
control_input_count, value_output_count, effect_output_count, \
control_output_count) \
struct Name##Operator final : public Operator { \
Name##Operator() \
: Operator(IrOpcode::k##Name, properties, #Name, value_input_count, \
effect_input_count, control_input_count, \
value_output_count, effect_output_count, \
control_output_count) {} \
}; \
Name##Operator k##Name##Operator;
COMMON_CACHED_OP_LIST(CACHED)
// snippet from operator.h
class V8_EXPORT_PRIVATE Operator : public NON_EXPORTED_BASE(ZoneObject) {
public:
using Opcode = uint16_t;
// Properties inform the operator-independent optimizer about legal // transformations for nodes that have this operator.
enum Property {
kNoProperties = 0,
kCommutative = 1 << 0, // OP(a, b) == OP(b, a) for all inputs.
kAssociative = 1 << 1, // OP(a, OP(b,c)) == OP(OP(a,b), c) for all inputs.
kIdempotent = 1 << 2, // OP(a); OP(a) == OP(a).
kNoRead = 1 << 3, // Has no scheduling dependency on Effects
kNoWrite = 1 << 4, // Does not modify any Effects and thereby
// create new scheduling dependencies.
kNoThrow = 1 << 5, // Can never generate an exception.
kNoDeopt = 1 << 6, // Can never generate an eager deoptimization exit.
kFoldable = kNoRead | kNoWrite,
kEliminatable = kNoDeopt | kNoWrite | kNoThrow,
kKontrol = kNoDeopt | kFoldable | kNoThrow,
kPure = kKontrol | kIdempotent
};
...
也可以直接在 turbolizer 里看到,19:Chechpoint 的第二个输入边 和 32: CheckMpas 的第二个输入边的线条类型,以及光标移动到 Checkpoint 结点后弹出窗口中显示的结点描述,都可以看出。

再按照类似的方法,根据依赖关系一层一层往上分析。

看到 StoreField 结点,Allocate 节点, 分析 LoadElimination::ReduceStoreField 函数, 可以看到对 StoreField 结点的分析,主要影响他的操作数 Object 的状态,对于我们这个 case 来说,是一个用 Allocate 新申请的对象,上级结点的状态会被原样传递下去,
Reduction LoadElimination::ReduceStoreField(Node* node,
FieldAccess const& access) {
Node* const object = NodeProperties::GetValueInput(node, 0);
Node* const new_value = NodeProperties::GetValueInput(node, 1);
Node* const effect = NodeProperties::GetEffectInput(node);
AbstractState const* state = node_states_.Get(effect);
if (state == nullptr) return NoChange();
if (access.offset == HeapObject::kMapOffset &&
access.base_is_tagged == kTaggedBase) {
DCHECK(IsAnyTagged(access.machine_type.representation()));
// Kill all potential knowledge about the {object}s map.
state = state->KillMaps(object, zone());
Type const new_value_type = NodeProperties::GetType(new_value);
if (new_value_type.IsHeapConstant()) {
// Record the new {object} map information.
ZoneHandleSet<Map> object_maps(
new_value_type.AsHeapConstant()->Ref().AsMap().object());
state = state->SetMaps(object, object_maps, zone());
}
} else {
IndexRange field_index = FieldIndexOf(access);
if (field_index != IndexRange::Invalid()) {
bool is_const_store = access.const_field_info.IsConst();
MachineRepresentation representation =
access.machine_type.representation();
FieldInfo const* lookup_result =
state->LookupField(object, field_index, access.const_field_info);
if (lookup_result &&
(!is_const_store || V8_ENABLE_DOUBLE_CONST_STORE_CHECK_BOOL)) {
// At runtime, we should never encounter
// - any store replacing existing info with a different, incompatible
// representation, nor
// - two consecutive const stores, unless the latter is a store into
// a literal.
// However, we may see such code statically, so we guard against
// executing it by emitting Unreachable.
// TODO(gsps): Re-enable the double const store check even for
// non-debug builds once we have identified other FieldAccesses
// that should be marked mutable instead of const
// (cf. JSCreateLowering::AllocateFastLiteral).
bool incompatible_representation =
!lookup_result->name.is_null() &&
!IsCompatible(representation, lookup_result->representation);
bool illegal_double_const_store =
is_const_store && !access.is_store_in_literal;
if (incompatible_representation || illegal_double_const_store) {
Node* control = NodeProperties::GetControlInput(node);
Node* unreachable =
graph()->NewNode(common()->Unreachable(), effect, control);
return Replace(unreachable);
}
if (lookup_result->value == new_value) {
// This store is fully redundant.
return Replace(effect);
}
}
// Kill all potentially aliasing fields and record the new value.
FieldInfo new_info(new_value, representation, access.name,
access.const_field_info);
if (is_const_store && access.is_store_in_literal) {
// We only kill const information when there is a chance that we
// previously stored information about the given const field (namely,
// when we observe const stores to literals).
state = state->KillConstField(object, field_index, zone());
}
state = state->KillField(object, field_index, access.name, zone());
state = state->AddField(object, field_index, new_info, zone());
if (is_const_store) {
// For const stores, we track information in both the const and the
// mutable world to guard against field accesses that should have
// been marked const, but were not.
new_info.const_field_info = ConstFieldInfo::None();
state = state->AddField(object, field_index, new_info, zone());
}
} else {
// Unsupported StoreField operator.
state = state->KillFields(object, access.name, zone());
}
}
return UpdateState(node, state);
}
再往上来到 Allocate 和 BeginRegion 结点, 这两个结点也是由 ReduceOtherNodes 分析,他们也都符合 1 effect in, 1 effect out, no write 的条件,所以只是把上级结点的状态传递下去,就来到了 31:LoadFiled 结点,由 ReduceLoadFiled 函数分析
Reduction LoadElimination::ReduceLoadField(Node* node,
FieldAccess const& access) {
Node* object = NodeProperties::GetValueInput(node, 0);
Node* effect = NodeProperties::GetEffectInput(node);
Node* control = NodeProperties::GetControlInput(node);
AbstractState const* state = node_states_.Get(effect);
if (state == nullptr) return NoChange();
if (access.offset == HeapObject::kMapOffset &&
access.base_is_tagged == kTaggedBase) {
DCHECK(IsAnyTagged(access.machine_type.representation()));
ZoneHandleSet<Map> object_maps;
if (state->LookupMaps(object, &object_maps) && object_maps.size() == 1) {
Node* value = jsgraph()->HeapConstant(object_maps[0]);
NodeProperties::SetType(value, Type::OtherInternal());
ReplaceWithValue(node, value, effect);
return Replace(value);
}
} else {
IndexRange field_index = FieldIndexOf(access);
if (field_index != IndexRange::Invalid()) {
MachineRepresentation representation =
access.machine_type.representation();
FieldInfo const* lookup_result =
state->LookupField(object, field_index, access.const_field_info);
if (!lookup_result && access.const_field_info.IsConst()) {
// If the access is const and we didn't find anything, also try to look
// up information from mutable stores
lookup_result =
state->LookupField(object, field_index, ConstFieldInfo::None());
}
if (lookup_result) {
// Make sure we don't reuse values that were recorded with a different
// representation or resurrect dead {replacement} nodes.
Node* replacement = lookup_result->value;
if (IsCompatible(representation, lookup_result->representation) &&
!replacement->IsDead()) {
// Introduce a TypeGuard if the type of the {replacement} node is not
// a subtype of the original {node}'s type.
if (!NodeProperties::GetType(replacement)
.Is(NodeProperties::GetType(node))) {
Type replacement_type = Type::Intersect(
NodeProperties::GetType(node),
NodeProperties::GetType(replacement), graph()->zone());
replacement = effect =
graph()->NewNode(common()->TypeGuard(replacement_type),
replacement, effect, control);
NodeProperties::SetType(replacement, replacement_type);
}
ReplaceWithValue(node, replacement, effect);
return Replace(replacement);
}
}
FieldInfo info(node, representation, access.name,
access.const_field_info);
state = state->AddField(object, field_index, info, zone());
}
}
Handle<Map> field_map;
if (access.map.ToHandle(&field_map)) {
state = state->SetMaps(node, ZoneHandleSet<Map>(field_map), zone());
}
return UpdateState(node, state);
}
这个结点的 value output 刚好是 CheckMap 结点的 value input,结合源码我们能大致推测出这个 LoadFiled 是从 o 对象加载 inner 属性,access 应该是访问的属性信息,如果 access.map 存储着目标对象的 map 信息,则会被存储到 state 中供后面的结点分析使用。这个 access 应该是结点创建的时候写入的,我们看一下结点创建的函数,根据 turbolizer 中看到的信息,应该是在 JSNativeContextSpecialization::ReduceJSLoadNamed 函数内联处理 JSLoadNamed 结点时生成的。

Reduction JSNativeContextSpecialization::ReduceJSLoadNamed(Node* node) {
JSLoadNamedNode n(node);
NamedAccess const& p = n.Parameters();
Node* const receiver = n.object();
NameRef name = p.name(broker());
// Check if we have a constant receiver.
HeapObjectMatcher m(receiver);
if (m.HasResolvedValue()) {
ObjectRef object = m.Ref(broker());
if (object.IsJSFunction() &&
name.equals(MakeRef(broker(), factory()->prototype_string()))) {
// Optimize "prototype" property of functions.
JSFunctionRef function = object.AsJSFunction();
// TODO(neis): Remove the has_prototype_slot condition once the broker is
// always enabled.
if (!function.map().has_prototype_slot() ||
!function.has_instance_prototype(dependencies()) ||
function.PrototypeRequiresRuntimeLookup(dependencies())) {
return NoChange();
}
ObjectRef prototype = dependencies()->DependOnPrototypeProperty(function);
Node* value = jsgraph()->Constant(prototype);
ReplaceWithValue(node, value);
return Replace(value);
} else if (object.IsString() &&
name.equals(MakeRef(broker(), factory()->length_string()))) {
// Constant-fold "length" property on constant strings.
Node* value = jsgraph()->Constant(object.AsString().length());
ReplaceWithValue(node, value);
return Replace(value);
}
}
if (!p.feedback().IsValid()) return NoChange();
return ReducePropertyAccess(node, nullptr, name, jsgraph()->Dead(),
FeedbackSource(p.feedback()), AccessMode::kLoad);
}
Reduction JSNativeContextSpecialization::ReducePropertyAccess(
Node* node, Node* key, base::Optional<NameRef> static_name, Node* value,
FeedbackSource const& source, AccessMode access_mode) {
DCHECK_EQ(key == nullptr, static_name.has_value());
DCHECK(node->opcode() == IrOpcode::kJSLoadProperty ||
node->opcode() == IrOpcode::kJSSetKeyedProperty ||
node->opcode() == IrOpcode::kJSStoreInArrayLiteral ||
node->opcode() == IrOpcode::kJSDefineKeyedOwnPropertyInLiteral ||
node->opcode() == IrOpcode::kJSHasProperty ||
node->opcode() == IrOpcode::kJSLoadNamed ||
node->opcode() == IrOpcode::kJSSetNamedProperty ||
node->opcode() == IrOpcode::kJSDefineNamedOwnProperty ||
node->opcode() == IrOpcode::kJSLoadNamedFromSuper ||
node->opcode() == IrOpcode::kJSDefineKeyedOwnProperty);
DCHECK_GE(node->op()->ControlOutputCount(), 1);
ProcessedFeedback const& feedback =
broker()->GetFeedbackForPropertyAccess(source, access_mode, static_name);
switch (feedback.kind()) {
case ProcessedFeedback::kInsufficient:
return ReduceEagerDeoptimize(
node,
DeoptimizeReason::kInsufficientTypeFeedbackForGenericNamedAccess);
case ProcessedFeedback::kNamedAccess:
return ReduceNamedAccess(node, value, feedback.AsNamedAccess(),
access_mode, key);
case ProcessedFeedback::kMegaDOMPropertyAccess:
DCHECK_EQ(access_mode, AccessMode::kLoad);
DCHECK_NULL(key);
return ReduceMegaDOMPropertyAccess(
node, value, feedback.AsMegaDOMPropertyAccess(), source);
case ProcessedFeedback::kElementAccess:
DCHECK_EQ(feedback.AsElementAccess().keyed_mode().access_mode(),
access_mode);
DCHECK_NE(node->opcode(), IrOpcode::kJSLoadNamedFromSuper);
return ReduceElementAccess(node, key, value, feedback.AsElementAccess());
default:
UNREACHABLE();
}
}
JSNativeContextSpecialization::ReduceNamedAccess 函数是一个几百行的大函数,我们还是用调试的方法来大致理解他,经过几个小时的调试以后我发现,这个 access_info 是根据 feedback 中记录的运行时访问的目标对象和属性信息得到的,其中我们目前最关心的 access.map 是从目标对象的 Map 中存储的属性 Descriptor 中的 FieldType。
Reduction JSNativeContextSpecialization::ReduceNamedAccess(
Node* node, Node* value, NamedAccessFeedback const& feedback,
AccessMode access_mode, Node* key) {
DCHECK(node->opcode() == IrOpcode::kJSLoadNamed ||
node->opcode() == IrOpcode::kJSSetNamedProperty ||
node->opcode() == IrOpcode::kJSLoadProperty ||
node->opcode() == IrOpcode::kJSSetKeyedProperty ||
node->opcode() == IrOpcode::kJSDefineNamedOwnProperty ||
node->opcode() == IrOpcode::kJSDefineKeyedOwnPropertyInLiteral ||
node->opcode() == IrOpcode::kJSHasProperty ||
node->opcode() == IrOpcode::kJSLoadNamedFromSuper ||
node->opcode() == IrOpcode::kJSDefineKeyedOwnProperty);
static_assert(JSLoadNamedNode::ObjectIndex() == 0 &&
JSSetNamedPropertyNode::ObjectIndex() == 0 &&
JSLoadPropertyNode::ObjectIndex() == 0 &&
JSSetKeyedPropertyNode::ObjectIndex() == 0 &&
JSDefineNamedOwnPropertyNode::ObjectIndex() == 0 &&
JSSetNamedPropertyNode::ObjectIndex() == 0 &&
JSDefineKeyedOwnPropertyInLiteralNode::ObjectIndex() == 0 &&
JSHasPropertyNode::ObjectIndex() == 0 &&
JSDefineKeyedOwnPropertyNode::ObjectIndex() == 0);
static_assert(JSLoadNamedFromSuperNode::ReceiverIndex() == 0);
Node* context = NodeProperties::GetContextInput(node);
FrameState frame_state{NodeProperties::GetFrameStateInput(node)};
Effect effect{NodeProperties::GetEffectInput(node)};
Control control{NodeProperties::GetControlInput(node)};
// receiver = the object we pass to the accessor (if any) as the "this" value.
Node* receiver = NodeProperties::GetValueInput(node, 0);
// lookup_start_object = the object where we start looking for the property.
Node* lookup_start_object;
if (node->opcode() == IrOpcode::kJSLoadNamedFromSuper) {
DCHECK(v8_flags.super_ic);
JSLoadNamedFromSuperNode n(node);
// Lookup start object is the __proto__ of the home object.
lookup_start_object = effect =
BuildLoadPrototypeFromObject(n.home_object(), effect, control);
} else {
// 我们遇到的是 JsLoadNamed 所以是这里
lookup_start_object = receiver;
}
// Either infer maps from the graph or use the feedback.
ZoneVector<MapRef> inferred_maps(zone());
if (!InferMaps(lookup_start_object, effect, &inferred_maps)) {
// 不能通过图结点推断出, 所以我们要用 feedback 信息中的 maps
// 而且我们的 feedback 只记录一种 map
for (const MapRef& map : feedback.maps()) {
inferred_maps.push_back(map);
}
}
RemoveImpossibleMaps(lookup_start_object, &inferred_maps);
// Check if we have an access o.x or o.x=v where o is the target native
// contexts' global proxy, and turn that into a direct access to the
// corresponding global object instead.
if (inferred_maps.size() == 1) {
MapRef lookup_start_object_map = inferred_maps[0];
if (lookup_start_object_map.equals(
native_context().global_proxy_object().map())) {
// 这里是对 global 对象的优化,我们不满足条件
if (!native_context().GlobalIsDetached()) {
base::Optional<PropertyCellRef> cell =
native_context().global_object().GetPropertyCell(feedback.name());
if (!cell.has_value()) return NoChange();
// Note: The map check generated by ReduceGlobalAccesses ensures that we
// will deopt when/if GlobalIsDetached becomes true.
return ReduceGlobalAccess(node, lookup_start_object, receiver, value,
feedback.name(), access_mode, key, *cell,
effect);
}
}
}
ZoneVector<PropertyAccessInfo> access_infos(zone());
{
ZoneVector<PropertyAccessInfo> access_infos_for_feedback(zone());
for (const MapRef& map : inferred_maps) {
if (map.is_deprecated()) continue;
// TODO(v8:12547): Support writing to shared structs, which needs a write
// barrier that calls Object::Share to ensure the RHS is shared.
if (InstanceTypeChecker::IsJSSharedStruct(map.instance_type()) &&
access_mode == AccessMode::kStore) {
return NoChange();
}
// 根据对象的 map 和属性的名字, 找到对应的属性描述符等捏成,提取到 access_info 中
// 我们这个 case 只有一个 access_info
PropertyAccessInfo access_info = broker()->GetPropertyAccessInfo(
map, feedback.name(), access_mode, dependencies());
access_infos_for_feedback.push_back(access_info);
}
// 这里把所有的 access_info 合并一下,但是我们只有一个,所以不用合并
AccessInfoFactory access_info_factory(broker(), dependencies(),
graph()->zone());
if (!access_info_factory.FinalizePropertyAccessInfos(
access_infos_for_feedback, access_mode, &access_infos)) {
return NoChange();
}
}
// Ensure that {key} matches the specified name (if {key} is given).
if (key != nullptr) {
effect = BuildCheckEqualsName(feedback.name(), key, effect, control);
}
// Collect call nodes to rewire exception edges.
ZoneVector<Node*> if_exception_nodes(zone());
ZoneVector<Node*>* if_exceptions = nullptr;
Node* if_exception = nullptr;
if (NodeProperties::IsExceptionalCall(node, &if_exception)) {
if_exceptions = &if_exception_nodes;
}
PropertyAccessBuilder access_builder(jsgraph(), broker(), dependencies());
// Check for the monomorphic cases.
if (access_infos.size() == 1) {
// 只有一个,所以走到这里
PropertyAccessInfo access_info = access_infos.front();
if (receiver != lookup_start_object) {
// Super property access. lookup_start_object is a JSReceiver or
// null. It can't be a number, a string etc. So trying to build the
// checks in the "else if" branch doesn't make sense.
access_builder.BuildCheckMaps(lookup_start_object, &effect, control,
access_info.lookup_start_object_maps());
} else if (!access_builder.TryBuildStringCheck(
broker(), access_info.lookup_start_object_maps(), &receiver,
&effect, control) &&
!access_builder.TryBuildNumberCheck(
broker(), access_info.lookup_start_object_maps(), &receiver,
&effect, control)) {
// Try to build string check or number check if possible. Otherwise build
// a map check.
// TryBuildStringCheck and TryBuildNumberCheck don't update the receiver
// if they fail.
DCHECK_EQ(receiver, lookup_start_object);
if (HasNumberMaps(broker(), access_info.lookup_start_object_maps())) {
// We need to also let Smi {receiver}s through in this case, so
// we construct a diamond, guarded by the Sminess of the {receiver}
// and if {receiver} is not a Smi just emit a sequence of map checks.
Node* check = graph()->NewNode(simplified()->ObjectIsSmi(), receiver);
Node* branch = graph()->NewNode(common()->Branch(), check, control);
Node* if_true = graph()->NewNode(common()->IfTrue(), branch);
Node* etrue = effect;
Control if_false{graph()->NewNode(common()->IfFalse(), branch)};
Effect efalse = effect;
access_builder.BuildCheckMaps(receiver, &efalse, if_false,
access_info.lookup_start_object_maps());
control = graph()->NewNode(common()->Merge(2), if_true, if_false);
effect =
graph()->NewNode(common()->EffectPhi(2), etrue, efalse, control);
} else {
// 走到这里
access_builder.BuildCheckMaps(receiver, &effect, control,
access_info.lookup_start_object_maps());
}
} else {
// At least one of TryBuildStringCheck & TryBuildNumberCheck succeeded
// and updated the receiver. Update lookup_start_object to match (they
// should be the same).
lookup_start_object = receiver;
}
// Generate the actual property access.
base::Optional<ValueEffectControl> continuation = BuildPropertyAccess(
lookup_start_object, receiver, value, context, frame_state, effect,
control, feedback.name(), if_exceptions, access_info, access_mode);
if (!continuation) {
// At this point we maybe have added nodes into the graph (e.g. via
// NewNode or BuildCheckMaps) in some cases but we haven't connected them
// to End since we haven't called ReplaceWithValue. Since they are nodes
// which are not connected with End, they will be removed by graph
// trimming.
return NoChange();
}
value = continuation->value();
effect = continuation->effect();
control = continuation->control();
} else {
// 这里的大段代码,对于我们这个 case 都没用到,忽略了
}
// Properly rewire IfException edges if {node} is inside a try-block.
if (!if_exception_nodes.empty()) {
DCHECK_NOT_NULL(if_exception);
DCHECK_EQ(if_exceptions, &if_exception_nodes);
int const if_exception_count = static_cast<int>(if_exceptions->size());
Node* merge = graph()->NewNode(common()->Merge(if_exception_count),
if_exception_count, &if_exceptions->front());
if_exceptions->push_back(merge);
Node* ephi =
graph()->NewNode(common()->EffectPhi(if_exception_count),
if_exception_count + 1, &if_exceptions->front());
Node* phi = graph()->NewNode(
common()->Phi(MachineRepresentation::kTagged, if_exception_count),
if_exception_count + 1, &if_exceptions->front());
ReplaceWithValue(if_exception, phi, ephi, merge);
}
// 走到这里返回
ReplaceWithValue(node, value, effect, control);
return Replace(value);
}
分析完这些,我们就知道 LoadFiled 结点中的 access.map 就是在运行时捕获的 o.inner 属性的实际的 Map,这个 Map 就是会在 LoadElimination 分析过程中一点一点传递到 32:CheckMaps 这个结点,最终在分析过程中发现 32:CheckMaps 是冗余的,可以直接删除掉。
有了这些再来看 Google P0 的分析, 发现 FiledType 信息的维护是触发漏洞很重要的点,所以我们再仔细看一下 FieldType 的来源,以及他是怎么变成 access_info.map 的
在代码中搜索 fieldtype 可以在 DescriptorArray 定义处找到一段注释
// A DescriptorArray is a custom array that holds instance descriptors.
// It has the following layout:
// Header:
// [16:0 bits]: number_of_all_descriptors (including slack)
// [32:16 bits]: number_of_descriptors
// [48:32 bits]: raw_number_of_marked_descriptors (used by GC)
// [64:48 bits]: alignment filler
// [kEnumCacheOffset]: enum cache
// Elements:
// [kHeaderSize + 0]: first key (and internalized String)
// [kHeaderSize + 1]: first descriptor details (see PropertyDetails)
// [kHeaderSize + 2]: first value for constants / Smi(1) when not used
// Slack:
// [kHeaderSize + number of descriptors * 3]: start of slack
// The "value" fields store either values or field types. A field type is either
// FieldType::None(), FieldType::Any() or a weak reference to a Map. All other
// references are strong.
class DescriptorArray
还可以在 objects 目录下的 field-type.{h, cc} 看到 FieldType 的实现。在 map-updater.cc 看到 field-type 的更新过程,并且还有一个命令行 --trace-generalization 来开启相关的 trace 日志。
D:\v8\out.poc via v24.9.0
❯ ./d8.exe --expose-gc --trace-generalization --trace_turbo --allow-natives-syntax --trace-compilation-dependencies --trace-de
Concurrent recompilation has been disabled for tracing.
[generalizing]inner:v{None;const}->h{Class(0000005100219EED);const} (uninitialized field) [~makeObject+23 at ./poc.js:11]
[generalizing]inner:v{None;const}->h{Class(0000005100219EED);const} (field type generalization) [~makeObject+23 at ./poc.js:11
[generalizing]foo:s{Any;const}->t{Any;mutable} (uninitialized field) [~setInnerProperty+6 at ./poc.js:4]
[generalizing]foo:s{Any;const}->t{Any;mutable} (field type generalization) [~setInnerProperty+6 at ./poc.js:4]
---------------------------------------------------
Begin compiling method setInnerProperty using TurboFan
---------------------------------------------------
Finished compiling method setInnerProperty using TurboFan
Installing dependency of [0x7ffc80004001 <Code TURBOFAN>] on [0x005100219ec5 <Map[16](HOLEY_ELEMENTS)>] in groups [field-type,
Installing dependency of [0x7ffc80004001 <Code TURBOFAN>] on [0x005100219eed <Map[28](HOLEY_ELEMENTS)>] in groups [prototype-c
[marking dependent code 0x7ffc80004001 <Code TURBOFAN> (0x005100219989 <SharedFunctionInfo setInnerProperty>) (opt id 0) for deoptimization, reason: weak objects]
[deoptimize marked code in all contexts]
[generalizing]inner:v{None;const}->h{Class(000000510021BA71);const} (uninitialized field) [~makeObject+23 at ./poc.js:11]
[generalizing]inner:v{None;const}->h{Class(000000510021BA71);const} (field type generalization) [~makeObject+23 at ./poc.js:11]
[generalizing]foo:s{Any;const}->t{Any;mutable} (uninitialized field) [~setInnerProperty+6 at ./poc.js:4]
[generalizing]foo:s{Any;const}->t{Any;mutable} (field type generalization) [~setInnerProperty+6 at ./poc.js:4]
---------------------------------------------------
Begin compiling method setInnerProperty using TurboFan
---------------------------------------------------
Finished compiling method setInnerProperty using TurboFan
Installing dependency of [0x7ffc80004201 <Code TURBOFAN>] on [0x00510021b9f5 <Map[16](HOLEY_ELEMENTS)>] in groups [field-type,field-const,field-representation]
Installing dependency of [0x7ffc80004201 <Code TURBOFAN>] on [0x00510021ba71 <Map[28](HOLEY_ELEMENTS)>] in groups [prototype-check,field-type]
---------------------------------------------------
Begin compiling method makeObject using TurboFan
---------------------------------------------------
Finished compiling method makeObject using TurboFan
Installing dependency of [0x7ffc80004401 <Code TURBOFAN>] on [0x00510021b9f5 <Map[16](HOLEY_ELEMENTS)>] in groups [field-type,field-const,field-representation]
Installing dependency of [0x7ffc80004401 <Code TURBOFAN>] on [0x005100219c09 <PropertyCell name=0x005100219855 <String[16]: #setInnerProperty> value=0x005100219be9 <JSFunction setInnerProperty (sfi = 0000005100219989)>>] in groups [property-cell-changed]
Installing dependency of [0x7ffc80004401 <Code TURBOFAN>] on [0x0051001c4501 <PropertyCell name=0x005100002529 <String[0]: #> value=1>] in groups [property-cell-changed]
Installing dependency of [0x7ffc80004401 <Code TURBOFAN>] on [0x00510021ba1d <AllocationSite>] in groups [allocation-site-tenuring-changed]
Installing dependency of [0x7ffc80004401 <Code TURBOFAN>] on [0x00510021ba55 <AllocationSite>] in groups [allocation-site-tenuring-changed]
结合函数 PrintGeneralization 可以读懂日志输出的内容
void PrintGeneralization(
Isolate* isolate, Handle<Map> map, FILE* file, const char* reason,
InternalIndex modify_index, int split, int descriptors,
bool descriptor_to_field, Representation old_representation,
Representation new_representation, PropertyConstness old_constness,
PropertyConstness new_constness, MaybeHandle<FieldType> old_field_type,
MaybeHandle<Object> old_value, MaybeHandle<FieldType> new_field_type,
MaybeHandle<Object> new_value) {
OFStream os(file);
os << "[generalizing]";
Name name = map->instance_descriptors(isolate).GetKey(modify_index);
if (name.IsString()) {
String::cast(name).PrintOn(file);
} else {
os << "{symbol " << reinterpret_cast<void*>(name.ptr()) << "}";
}
os << ":";
if (descriptor_to_field) {
os << "c";
} else {
os << old_representation.Mnemonic() << "{";
if (old_field_type.is_null()) {
os << Brief(*(old_value.ToHandleChecked()));
} else {
old_field_type.ToHandleChecked()->PrintTo(os);
}
os << ";" << old_constness << "}";
}
os << "->" << new_representation.Mnemonic() << "{";
if (new_field_type.is_null()) {
os << Brief(*(new_value.ToHandleChecked()));
} else {
new_field_type.ToHandleChecked()->PrintTo(os);
}
os << ";" << new_constness << "} (";
if (strlen(reason) > 0) {
os << reason;
} else {
os << "+" << (descriptors - split) << " maps";
}
os << ") [";
JavaScriptFrame::PrintTop(isolate, file, false, true);
os << "]\n";
}
以 [generalizing]inner:v{None;const}->h{Class(0000005100219EED);const} (uninitialized field) [~makeObject+23 at ./poc.js:11] 为例分析一下, inner 是属性的名字, : 是固定分隔, 后面的 v 是 old_representation,在后面的 {None; const} 代表之前的是旧的 filed_type 和 constness, -> 是固定分隔,后面 h 是 new_representation, {Class(xxxxxx);const} 是新的 field_type 和 constness,后面就是 reason 和当前执行的函数信息
const char* Representation::Mnemonic() const {
switch (kind_) {
case kNone:
return "v";
case kTagged:
return "t";
case kSmi:
return "s";
case kDouble:
return "d";
case kHeapObject:
return "h";
case kWasmValue:
return "w";
}
UNREACHABLE();
}
// static
MaybeObjectHandle Map::WrapFieldType(Isolate* isolate, Handle<FieldType> type) {
if (type->IsClass()) {
return MaybeObjectHandle::Weak(type->AsClass(), isolate);
}
return MaybeObjectHandle(type);
}
Google P0 的分析里还说了一点很重要的就是在属性指向的对象被回收以后,Map 中存储的 FieldType 会变成 FieldType::None(), 这个是怎么改变的呢?在 gpt-5.5 的帮助下,我找到了关键函数 Map::UnwrapFieldType
// static
FieldType Map::UnwrapFieldType(MaybeObject wrapped_type) {
// 这里就是关键分支
if (wrapped_type->IsCleared()) {
return FieldType::None();
}
HeapObject heap_object;
if (wrapped_type->GetHeapObjectIfWeak(&heap_object)) {
return FieldType::cast(heap_object);
}
return wrapped_type->cast<FieldType>();
}
FieldType DescriptorArray::GetFieldType(PtrComprCageBase cage_base,
InternalIndex descriptor_number) {
DCHECK_EQ(GetDetails(descriptor_number).location(), PropertyLocation::kField);
MaybeObject wrapped_type = GetValue(cage_base, descriptor_number);
return Map::UnwrapFieldType(wrapped_type);
}
那 wrrapped_type 什么情况下会满足 IsCleared() 为 true 这个条件呢? 我们先来看一下 wrapped_type 代表什么,他的类型是 MaybeObject 可以在 objects\maybe-object.h 找到对应到定义。
// A MaybeObject is either a SMI, a strong reference to a HeapObject, a weak
// reference to a HeapObject, or a cleared weak reference. It's used for
// implementing in-place weak references (see design doc: goo.gl/j6SdcK )
class MaybeObject : public TaggedImpl<HeapObjectReferenceType::WEAK, Address> {
public:
constexpr MaybeObject() : TaggedImpl(kNullAddress) {}
constexpr explicit MaybeObject(Address ptr) : TaggedImpl(ptr) {}
// These operator->() overloads are required for handlified code.
constexpr const MaybeObject* operator->() const { return this; }
V8_INLINE static MaybeObject FromSmi(Smi smi);
V8_INLINE static MaybeObject FromObject(Object object);
V8_INLINE static MaybeObject MakeWeak(MaybeObject object);
V8_INLINE static MaybeObject Create(MaybeObject o);
V8_INLINE static MaybeObject Create(Object o);
V8_INLINE static MaybeObject Create(Smi smi);
#ifdef VERIFY_HEAP
static void VerifyMaybeObjectPointer(Isolate* isolate, MaybeObject p);
#endif
private:
template <typename TFieldType, int kFieldOffset, typename CompressionScheme>
friend class TaggedField;
};
可以看到其实类的注释已经写明了 MaybeObject 的用途,是用来实现弱引用的,并且附上了设计文档 goo.gl/j6SdcK, 结合代码和文档我们可以知道 MaybeObject 实际上就是一个将指针当成 SMI/Object/Weak/Cleared Weak 使用的辅助类:
SMI: ...xxxxxxx0 (like now)
Object: ...xxxxxx01 (like now)
Weak: ...xxxxxx11 (new) (where ...xxxxxx != nullptr)
Cleared weak: ...00000011 (new)
当存储的值是 0x3 的时候,MaybeObject 就是一个 Cleared Weak,代表之前弱引用指向的对象已经被 GC 回收了。
这样我有了个新问题,既然 CheckMap 这个条件还是满足的,那么 o 这个对象的 Map 应该还是不变的,然后既然触发了漏洞,那么 o.inner 这个对象的 Map 肯定是变化了,那为什么看起来生命周期差不多的两个 Map,GC 对他们进行了不同的操作呢?可以通过 d8.exe 的 --log-maps 命令记录 Map 的相关日志到 v8.log, 因为我还要用 windbg TTD 来录制一个 trace 后面反复的调试,所以我用下面的命令再执行一次 POC。
经过一段时间的调试特别是用数据写入端点追踪了 Map 对象的标记过程以后,我找到了 o 的 Map 存活过 gc() 的原因,关键点在 CreateLiteral 函数中。
template <typename LiteralHelper>
MaybeHandle<JSObject> CreateLiteral(Isolate* isolate,
MaybeHandle<FeedbackVector> maybe_vector,
int literals_index,
Handle<HeapObject> description, int flags) {
if (maybe_vector.is_null()) {
return CreateLiteralWithoutAllocationSite<LiteralHelper>(
isolate, description, flags);
}
Handle<FeedbackVector> vector = maybe_vector.ToHandleChecked();
FeedbackSlot literals_slot(FeedbackVector::ToSlot(literals_index));
CHECK(literals_slot.ToInt() < vector->length());
Handle<Object> literal_site(vector->Get(literals_slot)->cast<Object>(),
isolate);
Handle<AllocationSite> site;
Handle<JSObject> boilerplate;
if (HasBoilerplate(literal_site)) {
site = Handle<AllocationSite>::cast(literal_site);
boilerplate = Handle<JSObject>(site->boilerplate(), isolate);
} else {
// 关键点在这里
// Eagerly create AllocationSites for literals that contain an Array.
bool needs_initial_allocation_site =
(flags & AggregateLiteral::kNeedsInitialAllocationSite) != 0;
if (!needs_initial_allocation_site &&
IsUninitializedLiteralSite(*literal_site)) {
// 第一次创建 ObjectLiteral 走到这里, 不会修改 feedback
PreInitializeLiteralSite(vector, literals_slot);
return CreateLiteralWithoutAllocationSite<LiteralHelper>(
isolate, description, flags);
} else {
// 第二次创建走到这里, 之后会走到下面, 把 boilerplate 通过 allocationsite 设置到 feedback vector 中
boilerplate = LiteralHelper::Create(isolate, description, flags,
AllocationType::kOld);
}
// Install AllocationSite objects.
AllocationSiteCreationContext creation_context(isolate);
site = creation_context.EnterNewScope();
RETURN_ON_EXCEPTION(isolate, DeepWalk(boilerplate, &creation_context),
JSObject);
creation_context.ExitScope(site, boilerplate);
vector->SynchronizedSet(literals_slot, *site); //<< 这里会把 allocate site 和 feedback vector 关联
}
static_assert(static_cast<int>(ObjectLiteral::kDisableMementos) ==
static_cast<int>(ArrayLiteral::kDisableMementos));
bool enable_mementos = (flags & ObjectLiteral::kDisableMementos) == 0;
// Copy the existing boilerplate.
AllocationSiteUsageContext usage_context(isolate, site, enable_mementos);
usage_context.EnterNewScope();
MaybeHandle<JSObject> copy = DeepCopy(boilerplate, &usage_context);
usage_context.ExitScope(site, boilerplate);
return copy;
}
void PreInitializeLiteralSite(Handle<FeedbackVector> vector,
FeedbackSlot slot) {
vector->SynchronizedSet(slot, Smi::FromInt(1));
}
void AllocationSiteCreationContext::ExitScope(Handle<AllocationSite> scope_site, Handle<JSObject> object) {
if (object.is_null()) return;
scope_site->set_boilerplate(*object, kReleaseStore);
if (v8_flags.trace_creation_allocation_sites) {
bool top_level =
!scope_site.is_null() && top().is_identical_to(scope_site);
if (top_level) {
PrintF("*** Setting AllocationSite %p transition_info %p\n",
reinterpret_cast<void*>(scope_site->ptr()),
reinterpret_cast<void*>(object->ptr()));
} else {
PrintF("*** Setting AllocationSite (%p, %p) transition_info %p\n",
reinterpret_cast<void*>(top()->ptr()),
reinterpret_cast<void*>(scope_site->ptr()),
reinterpret_cast<void*>(object->ptr()));
}
}
}
调试发现第一次创建 o 对象的时候会走简单路径,但是第二次创建的时候,就会创建一个 bolerplate 对象用来作为以后创建 o 对象的模板,并且这个模板会被关联到当前函数的 feedback vector 中,以后再创建 o 对象,都是从这个 boilerplate 做拷贝,o 对象的 Map 和 boilerplate 的 Map 是同一个。另外要注意这个 boilerplate 对象的 inner 属性是 null, o 对象的实际 inner 属性是后面通过其他操作设置的。但是同样的 o.inner 这个对象的也是一个 ObjectLiteral,所以它也有一个 boilerplate 对象,为什么 o.inner 的 Map 又被回收了呢? 因为 inner 里面属性是用 ["foo"] 赋值的,对于[xxxx] 这种访问,由于 xxx 可能是需要动态运算的表达式,v8 认为这个属性名是动态计算的所以不会像 .inner 赋值那样静态存入 boolerplate 的 Map 中,而是动态触发了一次属性添加,导致 o.inner 的 Map 运行时经过了一次变换,而变换后的 Map 只有 TransitionArray 中的弱引用,不像 o 的 Map 有从 allocationsite 的强引用指向。所以 gc 的时候 o.inner 被回收后 o.inner 的 Map 也被回收了,但是 o 被回收后,由于 allocationsite 还有通过 boilerplate 对象指向 o 的 Map 的引用,所以 o 的 Map 存活了。
这也解释了为什么 poc.js 中有 3 次 MakeObject 调用,要调用两次才能触发这个 boilerplate 机制,之后第 3 次 MakeObject 是在 boilerplate 创建后,保证 Map 可以活过 gc, 而实际的对象 o, o.inner 又会被回收,才能触发到漏洞。这个可以通过调试两次 CreateObjectLiteral 调用发现,创建 o 的模板的那次 object_boilerplate_description 包含了一个属性,但是创建 o.inner 的模板那次 object_boilerplate_description 不包含任何属性。
导致 boilerplate 的 Map 不同的逻辑:
// 解析 ObjectLiteral
template <typename Impl>
typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseObjectLiteral() {
// ObjectLiteral ::
// '{' (PropertyDefinition (',' PropertyDefinition)* ','? )? '}'
int pos = peek_position();
ObjectPropertyListT properties(pointer_buffer());
int number_of_boilerplate_properties = 0;
bool has_computed_names = false;
bool has_rest_property = false;
bool has_seen_proto = false;
Consume(Token::LBRACE);
AccumulationScope accumulation_scope(expression_scope());
// If methods appear inside the object literal, we'll enter this scope.
Scope* block_scope = NewBlockScopeForObjectLiteral();
block_scope->set_start_position(pos);
BlockState object_literal_scope_state(&object_literal_scope_, block_scope);
while (!Check(Token::RBRACE)) {
FuncNameInferrerState fni_state(&fni_);
ParsePropertyInfo prop_info(this, &accumulation_scope);
prop_info.position = PropertyPosition::kObjectLiteral;
ObjectLiteralPropertyT property =
ParseObjectPropertyDefinition(&prop_info, &has_seen_proto);
if (impl()->IsNull(property)) return impl()->FailureExpression();
if (prop_info.is_computed_name) {
//["foo"] 走到这里
has_computed_names = true;
}
if (prop_info.is_rest) {
has_rest_property = true;
}
if (impl()->IsBoilerplateProperty(property) && !has_computed_names) {
// 对于 ["foo"] 程序因为 has_computed_name 为 true 所以程序不会走到这里
// Count CONSTANT or COMPUTED properties to maintain the enumeration
// order.
number_of_boilerplate_properties++;
}
properties.Add(property);
if (peek() != Token::RBRACE) {
Expect(Token::COMMA);
}
fni_.Infer();
}
// 解析 ObjectLiteral 的属性名
template <class Impl>
typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseProperty(
ParsePropertyInfo* prop_info) {
DCHECK_EQ(prop_info->kind, ParsePropertyKind::kNotSet);
DCHECK_EQ(prop_info->function_flags, ParseFunctionFlag::kIsNormal);
DCHECK(!prop_info->is_computed_name);
if (Check(Token::ASYNC)) {
Token::Value token = peek();
if ((token != Token::MUL && prop_info->ParsePropertyKindFromToken(token)) ||
scanner()->HasLineTerminatorBeforeNext()) {
prop_info->name = impl()->GetIdentifier();
impl()->PushLiteralName(prop_info->name);
return factory()->NewStringLiteral(prop_info->name, position());
}
if (V8_UNLIKELY(scanner()->literal_contains_escapes())) {
impl()->ReportUnexpectedToken(Token::ESCAPED_KEYWORD);
}
prop_info->function_flags = ParseFunctionFlag::kIsAsync;
prop_info->kind = ParsePropertyKind::kMethod;
}
if (Check(Token::MUL)) {
prop_info->function_flags |= ParseFunctionFlag::kIsGenerator;
prop_info->kind = ParsePropertyKind::kMethod;
}
if (prop_info->kind == ParsePropertyKind::kNotSet &&
base::IsInRange(peek(), Token::GET, Token::SET)) {
Token::Value token = Next();
if (prop_info->ParsePropertyKindFromToken(peek())) {
prop_info->name = impl()->GetIdentifier();
impl()->PushLiteralName(prop_info->name);
return factory()->NewStringLiteral(prop_info->name, position());
}
if (V8_UNLIKELY(scanner()->literal_contains_escapes())) {
impl()->ReportUnexpectedToken(Token::ESCAPED_KEYWORD);
}
if (token == Token::GET) {
prop_info->kind = ParsePropertyKind::kAccessorGetter;
} else if (token == Token::SET) {
prop_info->kind = ParsePropertyKind::kAccessorSetter;
}
}
int pos = peek_position();
// For non computed property names we normalize the name a bit:
//
// "12" -> 12
// 12.3 -> "12.3"
// 12.30 -> "12.3"
// identifier -> "identifier"
//
// This is important because we use the property name as a key in a hash
// table when we compute constant properties.
bool is_array_index;
uint32_t index;
switch (peek()) {
case Token::PRIVATE_NAME:
prop_info->is_private = true;
is_array_index = false;
Consume(Token::PRIVATE_NAME);
if (prop_info->kind == ParsePropertyKind::kNotSet) {
prop_info->ParsePropertyKindFromToken(peek());
}
prop_info->name = impl()->GetIdentifier();
if (V8_UNLIKELY(prop_info->position ==
PropertyPosition::kObjectLiteral)) {
ReportUnexpectedToken(Token::PRIVATE_NAME);
prop_info->kind = ParsePropertyKind::kNotSet;
return impl()->FailureExpression();
}
break;
case Token::STRING:
Consume(Token::STRING);
prop_info->name = peek() == Token::COLON ? impl()->GetSymbol()
: impl()->GetIdentifier();
is_array_index = impl()->IsArrayIndex(prop_info->name, &index);
break;
case Token::SMI:
Consume(Token::SMI);
index = scanner()->smi_value();
is_array_index = true;
// Token::SMI were scanned from their canonical representation.
prop_info->name = impl()->GetSymbol();
break;
case Token::NUMBER: {
Consume(Token::NUMBER);
prop_info->name = impl()->GetNumberAsSymbol();
is_array_index = impl()->IsArrayIndex(prop_info->name, &index);
break;
}
case Token::BIGINT: {
Consume(Token::BIGINT);
prop_info->name = impl()->GetBigIntAsSymbol();
is_array_index = impl()->IsArrayIndex(prop_info->name, &index);
break;
}
case Token::LBRACK: {
// ["foo"] 走入这个分支
prop_info->name = impl()->NullIdentifier();
prop_info->is_computed_name = true;
Consume(Token::LBRACK);
AcceptINScope scope(this, true);
ExpressionT expression = ParseAssignmentExpression();
Expect(Token::RBRACK);
if (prop_info->kind == ParsePropertyKind::kNotSet) {
prop_info->ParsePropertyKindFromToken(peek());
}
return expression;
}
case Token::ELLIPSIS:
if (prop_info->kind == ParsePropertyKind::kNotSet) {
prop_info->name = impl()->NullIdentifier();
Consume(Token::ELLIPSIS);
AcceptINScope scope(this, true);
int start_pos = peek_position();
ExpressionT expression =
ParsePossibleDestructuringSubPattern(prop_info->accumulation_scope);
prop_info->kind = ParsePropertyKind::kSpread;
if (!IsValidReferenceExpression(expression)) {
expression_scope()->RecordDeclarationError(
Scanner::Location(start_pos, end_position()),
MessageTemplate::kInvalidRestBindingPattern);
expression_scope()->RecordPatternError(
Scanner::Location(start_pos, end_position()),
MessageTemplate::kInvalidRestAssignmentPattern);
}
if (peek() != Token::RBRACE) {
expression_scope()->RecordPatternError(
scanner()->location(), MessageTemplate::kElementAfterRest);
}
return expression;
}
V8_FALLTHROUGH;
default:
prop_info->name = ParsePropertyName();
is_array_index = false;
break;
}
if (prop_info->kind == ParsePropertyKind::kNotSet) {
prop_info->ParsePropertyKindFromToken(peek());
}
impl()->PushLiteralName(prop_info->name);
return is_array_index ? factory()->NewNumberLiteral(index, pos)
: factory()->NewStringLiteral(prop_info->name, pos);
}
后面从两个 ObjectLiteral 表达式生成字节码的时候,使用了这些数据。
void BytecodeGenerator::VisitObjectLiteral(ObjectLiteral* expr) {
...
} else {
size_t entry;
// If constant properties is an empty fixed array, use a cached empty fixed
// array to ensure it's only added to the constant pool once.
if (expr->builder()->properties_count() == 0) {
entry = builder()->EmptyObjectBoilerplateDescriptionConstantPoolEntry();
} else {
entry = builder()->AllocateDeferredConstantPoolEntry();
object_literals_.push_back(std::make_pair(expr->builder(), entry));
}
BuildCreateObjectLiteral(literal, flags, entry);
}
...
// Store computed values into the literal.
AccessorTable<ObjectLiteral::Property> accessor_table(zone());
for (; property_index < expr->properties()->length(); property_index++) {
ObjectLiteral::Property* property = expr->properties()->at(property_index);
if (property->is_computed_name()) break;
...
}
其实找到这个关键点的过程还是有点曲折的,感兴趣的同学可以继续看,对这块不感兴趣的可以跳过下面的,直接看下一节,漏洞触发。后面追踪了 foo 和 inner 的赋值,foo 的赋值导致它所在的对象的 Map 发生了 Transition,一个新的 Map 被创建出, 而 foo 属性的赋值,只触发了 Map 中属性描述的泛化, 没有被设置为一个新 Map。泛化过程中 inner 对象的 Map 被设置进了 inner 属性的描述符的 FieldType。
Handle<FieldType> Object::OptimalType(Isolate* isolate,
Representation representation) {
if (representation.IsNone()) return FieldType::None(isolate);
if (v8_flags.track_field_types) {
if (representation.IsHeapObject() && IsHeapObject()) {
// We can track only JavaScript objects with stable maps.
Handle<Map> map(HeapObject::cast(*this).map(), isolate);
if (map->is_stable() && map->IsJSReceiverMap()) {
return FieldType::Class(map, isolate);
}
}
}
return FieldType::Any(isolate);
}
到这里基本的条件都已经具备了,就剩下漏洞的触发了,第三次的 MakeObject 调用时 setInnerProperty 函数已经被 JIT 编译了,通过 turborlizer 查看汇编代码可以看出,JIT 编译后的 setInnerProperty 只要检查了 o 的 Map 符合预期以后,就会认为 o.inner.foo 已经存储的是 JS 对象,直接创建一个空的 JS 对象赋值到对应的内存位置。但是实际这次 MakeObject 中第一次调用 setInnerProperty 时,o.inner.foo 指向的实际是 SMI 0

1:检查 o 的 Map 2: 创建一个空对象 3:赋值给 o.inner.foo, 要注意这里的 rcx 已经被 5d: movl rcx, [rcx+0xb] 赋值给了 o.inner
接下来我们找到拼图的最后一块,正常情况下 v8 是怎么处理这种情况的,我们可以看一下漏洞的 PATCH

在这个位置下端点,很容易就在 TTD 会话中发现只会断下一次,是在 JIT 编译 MakeObject 函数的过程中,在 inline 阶段,对 o.inner 的 foo 的属性赋值操作进行优化的时候。PATCH 后的代码返回 Invalid(), 这样的话这个 o.inner.foo 属性的赋值就不会进行 inline 优化了,根据代码我推测是会变为调用 Runtime 函数去走更通用的赋值流程。
我们新建一个调试会话,这次我们更改函数的返回值,让他返回 invalid。之后再在 Deoptimizer::DeoptimizeMarkedCode 这个函数下端点,就能在函数被取消优化的时候断下(不要问我为什么是这个函数,我也是经过反复假设、验证最终发现的)。
断下的时候,调用栈如下
[0x3] v8!v8::internal::Deoptimizer::DeoptimizeMarkedCode+0x1ff 0x1747dfda30 0x7ffca8402d20
[0x4] v8!v8::internal::DependentCode::DeoptimizeDependencyGroups+0x90 0x1747dfdb70 0x7ffca86a4201
[0x5] v8!v8::internal::DependentCode::DeoptimizeDependencyGroups+0x4b (Inline Function) (Inline Function)
[0x6] v8!v8::internal::MapUpdater::GeneralizeField+0x441 0x1747dfdbe0 0x7ffca86a3d63
[0x7] v8!v8::internal::MapUpdater::GeneralizeField+0x23 0x1747dfdd40 0x7ffca86a0eb4
[0x8] v8!v8::internal::MapUpdater::TryReconfigureToDataFieldInplace+0x1f4 0x1747dfdd80 0x7ffca86a08de
[0x9] v8!v8::internal::MapUpdater::ReconfigureToDataField+0x1fe 0x1747dfde80 0x7ffca86b8040
[0xa] v8!v8::internal::`anonymous namespace'::UpdateDescriptorForValue+0x240 0x1747dfdf10 0x7ffca86b7dc2
[0xb] v8!v8::internal::Map::PrepareForDataProperty+0xe2 0x1747dfe040 0x7ffca86962cb
[0xc] v8!v8::internal::LookupIterator::PrepareForDataProperty+0x44b 0x1747dfe150 0x7ffca82cde06
[0xd] v8!v8::internal::StoreIC::LookupForWrite+0x426 0x1747dfe210 0x7ffca82cfc13
[0xe] v8!v8::internal::StoreIC::UpdateCaches+0x43 0x1747dfe2e0 0x7ffca82cf41f
[0xf] v8!v8::internal::StoreIC::Store+0x61f 0x1747dfe360 0x7ffca82dbe9d
[0x10] v8!v8::internal::__RT_impl_Runtime_DefineNamedOwnIC_Miss+0x33d 0x1747dfe4d0 0x7ffca82db74d
[0x11] v8!v8::internal::Runtime_DefineNamedOwnIC_Miss+0xcd 0x1747dfe640 0x7ffc3f96a283
这个时候其实是在设置 o 的 inner 属性,设置属性检查 map 的时候由于 o.inner 属性中记录的之前的旧 map 已经被回收了标记为 Cleared,已经不适合直接存储现在的新创建的 o.inner 了,所以这个地方要对这个属性描述符进行泛化,泛化后 Map 变化了,就触发依赖 o 的 Map 的代码的取消优化。
但是如果是 PATCH 之前的代码,这个地方的属性设置是直接被优化掉了,不会有任何属性描述符的检查和泛化,就不会触发这个优化的取消,就会导致后面虽然 o.inner 对象已经变化了,但是由于无法触发相关代码的优化的取消,优化后的代码还是会认为 o.inner 还是之前的样子,直接去改变对应的内存。
下面我们看一下这个 kStoreLiteral 是哪里来的。
To Be Continue
一些弯路,先放这,以后整理
经过一些代码搜索和调试验证,我发现可以在函数 MarkCompactCollector::CollectGarbage() 下断点,这个是 Full GC 的主要流程,整个程序生命周期,这个函数执行了两次,我只要追踪第二次 MarkLiveObjects 的过程,如果猜想正确,在第二次的 MarkLiveObject 调用中 Map(0x02590021b9f5) 被标记为 live, 而 Map(0x02590021ba71) 没有被标记。
void MarkCompactCollector::CollectGarbage() {
// Make sure that Prepare() has been called. The individual steps below will
// update the state as they proceed.
DCHECK(state_ == PREPARE_GC);
MarkLiveObjects();
ClearNonLiveReferences();
VerifyMarking();
heap()->memory_measurement()->FinishProcessing(native_context_stats_);
RecordObjectStats();
Sweep();
Evacuate();
Finish();
}
v8 用了三色标记法,会先把第一次遇到的对象全部标记为灰色,然后再加入处理队列依次便利她内部指向的其他对象,遍历完成后把对象改成黑色,代表这个对象已经标记完成。我在把目标对象标记成灰色的地方加一个 trace point 输出对象的指针,看源码大概有几个位置,都加一下。
标记的方法就是在一个 bitmap 里把对应的位设位,所以我们可以给对应的位置下写入断点,来找到把 Map 标记成 live 的地方,那就要稍微了解一个位图的内存布局,可以从 src\heap\mark.h 中找到对应的代码,尤其是 Bitmap 这个类。
// Bitmap is a sequence of cells each containing fixed number of bits.
class V8_EXPORT_PRIVATE Bitmap {
public:
static const uint32_t kBitsPerCell = 32;
static const uint32_t kBitsPerCellLog2 = 5;
static const uint32_t kBitIndexMask = kBitsPerCell - 1;
static const uint32_t kBytesPerCell = kBitsPerCell / kBitsPerByte;
static const uint32_t kBytesPerCellLog2 = kBitsPerCellLog2 - kBitsPerByteLog2;
// The length is the number of bits in this bitmap. (+1) accounts for
// the case where the markbits are queried for a one-word filler at the
// end of the page.
static const size_t kLength = ((1 << kPageSizeBits) >> kTaggedSizeLog2) + 1;
// The size of the bitmap in bytes is CellsCount() * kBytesPerCell.
static const size_t kSize;
static constexpr size_t CellsForLength(int length) {
return (length + kBitsPerCell - 1) >> kBitsPerCellLog2;
}
static constexpr size_t CellsCount() { return CellsForLength(kLength); }
V8_INLINE static uint32_t IndexToCell(uint32_t index) {
return index >> kBitsPerCellLog2;
}
V8_INLINE static uint32_t IndexInCell(uint32_t index) {
return index & kBitIndexMask;
}
// Retrieves the cell containing the provided markbit index.
V8_INLINE static uint32_t CellAlignIndex(uint32_t index) {
return index & ~kBitIndexMask;
}
V8_INLINE MarkBit::CellType* cells() {
return reinterpret_cast<MarkBit::CellType*>(this);
}
V8_INLINE const MarkBit::CellType* cells() const {
return reinterpret_cast<const MarkBit::CellType*>(this);
}
V8_INLINE static Bitmap* FromAddress(Address addr) {
return reinterpret_cast<Bitmap*>(addr);
}
inline MarkBit MarkBitFromIndex(uint32_t index) {
MarkBit::CellType mask = 1u << IndexInCell(index);
MarkBit::CellType* cell = this->cells() + (index >> kBitsPerCellLog2);
return MarkBit(cell, mask);
}
};
这个就是一个位图管理的辅助类,可以看到它是把整个位图分割成多个 4 字节大小的 Cell 来管理,这样每个 Cell 就包括 32 个位,位图是堆中表示内存块的结构 MemoryChunk 的一部分,用来存储当前 MemoryChunk 的对象的存活情况,在 MemoryChunk 的 kMarkingBitmapOffset 便宜处开始,据我调试来看,我这个版本的 d8.exe 中偏移是 0x138, 然后每个位表示 4 字节的情况,因为堆内存储的都是 v8 对象,都是按 4 字节对齐的,其实应该说每个位表示对应的地址开头的对象的标记状态。
对于具体的某个对象来说,找到他对应的标记位,就是先找到它所在的 MemoryChunk 这个很方便,就是按 0x4000 对齐就好,因为每个 MemoryChunk 都是 256 K 大小的(不同平台可能不同)。
// 256K page size 0x40000, can vary
constexpr int kPageSizeBits = 18;
// 0x40000
static const intptr_t kAlignment =
(static_cast<uintptr_t>(1) << kPageSizeBits);
// 0x3ffff
static const intptr_t kAlignmentMask = kAlignment - 1;
class BasicMemoryChunk {
...
// Only works if the object is in the first kPageSize of the MemoryChunk.
static BasicMemoryChunk* FromHeapObject(HeapObject o) {
DCHECK(!V8_ENABLE_THIRD_PARTY_HEAP_BOOL);
return reinterpret_cast<BasicMemoryChunk*>(BaseAddress(o.ptr()));
}
// & 0xFFFFFFFF`FFFC0000
static Address BaseAddress(Address a) { return a & ~kAlignmentMask; }
static const intptr_t kMarkingBitmapOffset =
MemoryChunkLayout::kMarkingBitmapOffset;
template <AccessMode mode>
ConcurrentBitmap<mode>* marking_bitmap() const {
return static_cast<ConcurrentBitmap<mode>*>(
Bitmap::FromAddress(address() + kMarkingBitmapOffset));
}
ConcurrentBitmap<AccessMode::ATOMIC>* MarkingState::bitmap(
const BasicMemoryChunk* chunk) const {
return chunk->marking_bitmap<AccessMode::ATOMIC>();
}
...
};
然后再加上位图的偏移量 0x138, 就来到了位图的开头,对象在位图中的索引,就是用对象地址 - MemoryChunk 基地址 / 4 就可以了,但是因为是位运算,所以在具体操作的时候,又会把索引分成两部分,先找到对应的 Cell 再找到 Cell 中的第几位,这个 Cell 中的第几位最后是用掩码去访问。
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::WhiteToGrey(HeapObject obj) {
return Marking::WhiteToGrey<access_mode>(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
MarkBit MarkingStateBase<ConcreteState, access_mode>::MarkBitFrom(
const HeapObject obj) const {
return MarkBitFrom(BasicMemoryChunk::FromHeapObject(obj), obj.ptr());
}
template <typename ConcreteState, AccessMode access_mode>
MarkBit MarkingStateBase<ConcreteState, access_mode>::MarkBitFrom(
const BasicMemoryChunk* p, Address addr) const {
return static_cast<const ConcreteState*>(this)->bitmap(p)->MarkBitFromIndex(
p->AddressToMarkbitIndex(addr));
}
template <typename ConcreteState, AccessMode access_mode>
Marking::ObjectColor MarkingStateBase<ConcreteState, access_mode>::Color(
const HeapObject obj) const {
return Marking::Color(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::IsImpossible(
const HeapObject obj) const {
return Marking::IsImpossible<access_mode>(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::IsBlack(
const HeapObject obj) const {
return Marking::IsBlack<access_mode>(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::IsWhite(
const HeapObject obj) const {
return Marking::IsWhite<access_mode>(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::IsGrey(
const HeapObject obj) const {
return Marking::IsGrey<access_mode>(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::IsBlackOrGrey(
const HeapObject obj) const {
return Marking::IsBlackOrGrey<access_mode>(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::WhiteToGrey(HeapObject obj) {
return Marking::WhiteToGrey<access_mode>(MarkBitFrom(obj));
}
template <typename ConcreteState, AccessMode access_mode>
bool MarkingStateBase<ConcreteState, access_mode>::WhiteToBlack(
HeapObject obj) {
return WhiteToGrey(obj) && GreyToBlack(obj);
}
//from src/heap/memory-chunk-layout.h
class V8_EXPORT_PRIVATE MemoryChunkLayout {
public:
static constexpr int kNumSets = NUMBER_OF_REMEMBERED_SET_TYPES;
static constexpr int kNumTypes = ExternalBackingStoreType::kNumTypes;
#if V8_CC_MSVC && V8_TARGET_ARCH_IA32
static constexpr int kMemoryChunkAlignment = 8;
#else
static constexpr int kMemoryChunkAlignment = sizeof(size_t);
#endif // V8_CC_MSVC && V8_TARGET_ARCH_IA32
#define FIELD(Type, Name) \
k##Name##Offset, k##Name##End = k##Name##Offset + sizeof(Type) - 1
enum Header {
// BasicMemoryChunk fields:
FIELD(size_t, Size),
FIELD(uintptr_t, Flags),
FIELD(Heap*, Heap),
FIELD(Address, AreaStart),
FIELD(Address, AreaEnd),
FIELD(size_t, AllocatedBytes),
FIELD(size_t, WastedMemory),
FIELD(std::atomic<intptr_t>, HighWaterMark),
FIELD(Address, Owner),
FIELD(VirtualMemory, Reservation),
// MemoryChunk fields:
FIELD(SlotSet* [kNumSets], SlotSet),
FIELD(ProgressBar, ProgressBar),
FIELD(std::atomic<intptr_t>, LiveByteCount),
FIELD(TypedSlotsSet* [kNumSets], TypedSlotSet),
FIELD(void* [kNumSets], InvalidatedSlots),
FIELD(base::Mutex*, Mutex),
FIELD(std::atomic<intptr_t>, ConcurrentSweeping),
FIELD(base::Mutex*, PageProtectionChangeMutex),
FIELD(uintptr_t, WriteUnprotectCounter),
FIELD(std::atomic<size_t>[kNumTypes], ExternalBackingStoreBytes),
FIELD(heap::ListNode<MemoryChunk>, ListNode),
FIELD(FreeListCategory**, Categories),
FIELD(CodeObjectRegistry*, CodeObjectRegistry),
FIELD(PossiblyEmptyBuckets, PossiblyEmptyBuckets),
FIELD(ActiveSystemPages, ActiveSystemPages),
#ifdef V8_ENABLE_INNER_POINTER_RESOLUTION_OSB
FIELD(ObjectStartBitmap, ObjectStartBitmap),
#endif // V8_ENABLE_INNER_POINTER_RESOLUTION_OSB
FIELD(size_t, WasUsedForAllocation),
kMarkingBitmapOffset,
kMemoryChunkHeaderSize =
kMarkingBitmapOffset +
((kMarkingBitmapOffset % kMemoryChunkAlignment) == 0
? 0
: kMemoryChunkAlignment -
(kMarkingBitmapOffset % kMemoryChunkAlignment)),
kMemoryChunkHeaderStart = kSlotSetOffset,
kBasicMemoryChunkHeaderSize = kMemoryChunkHeaderStart,
kBasicMemoryChunkHeaderStart = 0,
};
#undef FIELD
static size_t CodePageGuardStartOffset();
static size_t CodePageGuardSize();
static intptr_t ObjectStartOffsetInCodePage();
static intptr_t ObjectEndOffsetInCodePage();
static size_t AllocatableMemoryInCodePage();
static intptr_t ObjectStartOffsetInDataPage();
static size_t AllocatableMemoryInDataPage();
static size_t ObjectStartOffsetInMemoryChunk(AllocationSpace space);
static size_t AllocatableMemoryInMemoryChunk(AllocationSpace space);
static int MaxRegularCodeObjectSize();
static_assert(kMemoryChunkHeaderSize % alignof(size_t) == 0);
};
void MarkCompactCollector::MarkObject(HeapObject host, HeapObject obj) {
if (marking_state()->WhiteToGrey(obj)) {
local_marking_worklists()->Push(obj);
if (V8_UNLIKELY(v8_flags.track_retaining_path)) {
heap_->AddRetainer(host, obj);
}
}
}
void MarkCompactCollector::MarkRootObject(Root root, HeapObject obj) {
if (marking_state()->WhiteToGrey(obj)) {
local_marking_worklists()->Push(obj);
if (V8_UNLIKELY(v8_flags.track_retaining_path)) {
heap_->AddRetainingRoot(root, obj);
}
}
}
# 函数被 inline 了 所以用 bm 设置断点
bm v8!v8::internal::MarkCompactCollector::MarkObject "dd r8 l4; gc;"
bm v8!v8::internal::MarkCompactCollector::MarkRootObject "? poi(r8); gc;"
调试以后发现,这个 Mark 的对象太多了,执行很慢,我们还是不输出了,改成条件断点,在 Mark 的目标是两个 Map 的时候断下来。
bm v8!v8::internal::MarkCompactCollector::MarkObject ".if (poi(r8) != 0x02590021b9f5) { gc;} .elsif (poi(r8) != 0x02370021b9f7) { gc; }"
bm v8!v8::internal::MarkCompactCollector::MarkRootObject ".if (poi(r8) != 0x02370021b9f5) { gc; } .elsif (poi(r8) != 0x02370021b9f7) { gc; }"
int LiteralBoilerplateBuilder::ComputeFlags(bool disable_mementos = false) const {
int flags = AggregateLiteral::kNoFlags;
if (is_shallow()) flags |= AggregateLiteral::kIsShallow;
if (disable_mementos) flags |= AggregateLiteral::kDisableMementos;
if (needs_initial_allocation_site())
flags |= AggregateLiteral::kNeedsInitialAllocationSite;
return flags;
}
bool needs_initial_allocation_site() const {
return NeedsInitialAllocationSiteField::decode(bit_field_);
}
// we actually only care three conditions for depth
// - depth == kUninitialized, DCHECK(!is_initialized())
// - depth == kShallow, which means depth = 1
// - depth == kNotShallow, which means depth > 1
using DepthField = base::BitField<DepthKind, 0, kDepthKindBits>;
using NeedsInitialAllocationSiteField = DepthField::Next<bool, 1>;
using IsSimpleField = NeedsInitialAllocationSiteField::Next<bool, 1>;
using BoilerplateDescriptorKindField =
IsSimpleField::Next<ElementsKind, kFastElementsKindBits>;
enum DepthKind { kUninitialized, kShallow, kNotShallow };
void set_needs_initial_allocation_site(bool required) {
bit_field_ = NeedsInitialAllocationSiteField::update(bit_field_, required);
}
void ObjectLiteralBoilerplateBuilder::InitDepthAndFlags() {
if (is_initialized()) return;
bool is_simple = true;
bool has_seen_prototype = false;
bool needs_initial_allocation_site = false;
DepthKind depth_acc = kShallow;
uint32_t nof_properties = 0;
uint32_t elements = 0;
uint32_t max_element_index = 0;
for (int i = 0; i < properties()->length(); i++) {
ObjectLiteral::Property* property = properties()->at(i);
if (property->IsPrototype()) {
has_seen_prototype = true;
// __proto__:null has no side-effects and is set directly on the
// boilerplate.
if (property->IsNullPrototype()) {
set_has_null_protoype(true);
continue;
}
DCHECK(!has_null_prototype());
is_simple = false;
continue;
}
if (nof_properties == boilerplate_properties_) {
DCHECK(property->is_computed_name());
is_simple = false;
if (!has_seen_prototype) InitFlagsForPendingNullPrototype(i);
break;
}
DCHECK(!property->is_computed_name());
MaterializedLiteral* literal = property->value()->AsMaterializedLiteral();
if (literal != nullptr) {
LiteralBoilerplateBuilder::InitDepthAndFlags(literal);
depth_acc = kNotShallow;
needs_initial_allocation_site |= literal->NeedsInitialAllocationSite();
}
Literal* key = property->key()->AsLiteral();
Expression* value = property->value();
bool is_compile_time_value = value->IsCompileTimeValue();
is_simple = is_simple && is_compile_time_value;
// Keep track of the number of elements in the object literal and
// the largest element index. If the largest element index is
// much larger than the number of elements, creating an object
// literal with fast elements will be a waste of space.
uint32_t element_index = 0;
if (key->AsArrayIndex(&element_index)) {
max_element_index = std::max(element_index, max_element_index);
elements++;
} else {
DCHECK(key->IsPropertyName());
}
nof_properties++;
}
set_depth(depth_acc);
set_is_simple(is_simple);
set_needs_initial_allocation_site(needs_initial_allocation_site);
set_has_elements(elements > 0);
set_fast_elements((max_element_index <= 32) ||
((2 * elements) >= max_element_index));
}
0:000> u
v8!v8::internal::MarkCompactCollector::MarkRootObject [F:\v8\v8\src\heap\mark-compact-inl.h @ 37] [inlined in v8!v8::internal::MarkCompactCollector::RootMarkingVisitor::MarkObjectByPointer+0x44 [F:\v8\v8\src\heap\mark-compact.cc @ 1098]]:
00007ffd`156557e4 4889fa mov rdx,rdi
00007ffd`156557e7 4881e20000fcff and rdx,0FFFFFFFFFFFC0000h
00007ffd`156557ee 89f9 mov ecx,edi
00007ffd`156557f0 c0e902 shr cl,2
00007ffd`156557f3 41b801000000 mov r8d,1
00007ffd`156557f9 41d3e0 shl r8d,cl
00007ffd`156557fc 89f9 mov ecx,edi
00007ffd`156557fe c1e907 shr ecx,7
> bp 00007ffd156557e4 "r rdi; gc;"
trace point 的输出里没有我们想看到的 Map 对象的指针,但是调试 MarkLiveObjects 过程中一个看起来跟 Map 回收有关的函数 RetaubMaps 进入了我们的视线
void MarkCompactCollector::RetainMaps() {
// Retaining maps increases the chances of reusing map transitions at some
// memory cost, hence disable it when trying to reduce memory footprint more
// aggressively.
const bool should_retain_maps =
!heap()->ShouldReduceMemory() && v8_flags.retain_maps_for_n_gc != 0;
for (WeakArrayList retained_maps : heap()->FindAllRetainedMaps()) {
DCHECK_EQ(0, retained_maps.length() % 2);
for (int i = 0; i < retained_maps.length(); i += 2) {
MaybeObject value = retained_maps.Get(i);
HeapObject map_heap_object;
if (!value->GetHeapObjectIfWeak(&map_heap_object)) {
continue;
}
int age = retained_maps.Get(i + 1).ToSmi().value();
int new_age;
Map map = Map::cast(map_heap_object);
if (should_retain_maps && marking_state()->IsWhite(map)) {
if (ShouldRetainMap(marking_state(), map, age)) {
if (marking_state()->WhiteToGrey(map)) {
local_marking_worklists()->Push(map);
}
if (V8_UNLIKELY(v8_flags.track_retaining_path)) {
heap_->AddRetainingRoot(Root::kRetainMaps, map);
}
}
Object prototype = map.prototype();
if (age > 0 && prototype.IsHeapObject() &&
marking_state()->IsWhite(HeapObject::cast(prototype))) {
// The prototype is not marked, age the map.
new_age = age - 1;
} else {
// The prototype and the constructor are marked, this map keeps only
// transition tree alive, not JSObjects. Do not age the map.
new_age = age;
}
} else {
new_age = v8_flags.retain_maps_for_n_gc;
}
// Compact the array and update the age.
if (new_age != age) {
retained_maps.Set(i + 1, MaybeObject::FromSmi(Smi::FromInt(new_age)));
}
}
}
}
调试这个函数发现,0x000001fc00204524 这个 Map 被标记为 live 了,但是另一个 Map 并没有,用内存写入断点查看哪个 Map 被 Sweep ,以及后面的一组 CreateLiteralObject 查看使用的 Map 指针,也都能证实 0x000001fc00204524 Map 没变,但是另一个被回收了。所有这个应该就是一个关键点,我们来了解一下 RetainMap 这个机制。
std::vector<WeakArrayList> Heap::FindAllRetainedMaps() {
std::vector<WeakArrayList> result;
Object context = native_contexts_list();
while (!context.IsUndefined(isolate())) {
NativeContext native_context = NativeContext::cast(context);
result.push_back(WeakArrayList::cast(native_context.retained_maps()));
context = native_context.next_context_link();
}
return result;
}
从 FindAllRetainedMaps 入手,在源码里搜索关键字,可以找到添加 Map 的到 retained_maps 的地方。
void Heap::AddRetainedMap(Handle<NativeContext> context, Handle<Map> map) {
if (map->is_in_retained_map_list() || map->InSharedWritableHeap()) {
return;
}
Handle<WeakArrayList> array(WeakArrayList::cast(context->retained_maps()),
isolate());
if (array->IsFull()) {
CompactRetainedMaps(*array);
}
array = WeakArrayList::AddToEnd(
isolate(), array, MaybeObjectHandle::Weak(map),
MaybeObjectHandle(Smi::FromInt(v8_flags.retain_maps_for_n_gc),
isolate()));
if (*array != context->retained_maps()) {
context->set_retained_maps(*array);
}
map->set_is_in_retained_map_list(true);
}
void PipelineCompilationJob::RegisterWeakObjectsInOptimizedCode(
Isolate* isolate, Handle<NativeContext> context, Handle<Code> code) {
std::vector<Handle<Map>> maps;
DCHECK(code->is_optimized_code());
{
DisallowGarbageCollection no_gc;
PtrComprCageBase cage_base(isolate);
int const mode_mask = RelocInfo::EmbeddedObjectModeMask();
for (RelocIterator it(*code, mode_mask); !it.done(); it.next()) {
DCHECK(RelocInfo::IsEmbeddedObjectMode(it.rinfo()->rmode()));
HeapObject target_object = it.rinfo()->target_object(cage_base);
if (code->IsWeakObjectInOptimizedCode(target_object)) {
if (target_object.IsMap(cage_base)) {
maps.push_back(handle(Map::cast(target_object), isolate));
}
}
}
}
for (Handle<Map> map : maps) {
isolate->heap()->AddRetainedMap(context, map);
}
code->set_can_have_weak_objects(true);
}
阅读了 reloc-info.{cc, h} 以及 codegen/x64/assembler-x64{.h,-inl.h, .cc} 的相关代码后,可以看到 RelocInfo 是用来记录 jit 生成的代码中的可重定位区域的信息的,里面刚好有嵌入到机器码中的 Map 对象,所以这里其实就是把所有嵌入到机器码中的 Map 对象都记录到 RetainMap。那结合上面分析出的 CheckMap 的消除, o.inner 的 CheckMap 刚好被 ReduceCheckMap 消除掉了,所以没在最终生成的机器码中出现,至于 o 的 Map 由于存在对应的 CheckMap 所以对应的 AddRetainMap 机制生效,不会被 GC 回收。
下面我们在调试器里看一下 o.inner 的 Map 被回收的时候,o 的 Map 中的 PropertyDescriptor 发生的变化。
gc 在 Sweep 对象的过程中,会把对象加入到空闲内存块列表中,就需要修改原来的对象,所以我们对 000001fc00219e80 这个地址下硬件写入断点,查找 Map 被回收的地方
{
TRACE_GC(heap()->tracer(), GCTracer::Scope::MC_CLEAR_WEAK_REFERENCES);
ClearWeakReferences();
ClearWeakCollections();
ClearJSWeakRefs();
}
16 e Disable Clear 00007ffd`1563c6ad [F:\v8\v8\src\heap\mark-compact.cc @ 3476] 0001 (0001) 0:**** v8!v8::internal::MarkCompactCollector::ClearWeakReferences+0x38d "r rdi; gc;"
但是这里还有个问题,编译后的代码和 map 是关联到一起的,如果 map 变了,或者 map 被释放了, v8 是怎么做的呢?
查资料以后发现,v8 会给编译后的代码和他所依赖的 map 建立一个双向的关联关系,
- https://v8.dev/docs/hidden-classes
但是对于
PipelineImpl::CommitDependencies
DependentCode::InstallDependency 给对象的 Map 安装 DependentCode 的弱引用
参考
- https://jayconrod.com/posts/55/a-tour-of-v8--garbage-collection
废弃
// Handle<Map> 的值,是一个指向 HandleScope 的 slot 的指针
0:000> dq map
000000cd`c9ffe1d8 000001f3`81fe78f0 aaaaaaaa`00005350
000000cd`c9ffe1e8 000001f3`81fa3580 000000cd`c9ffe4e8
000000cd`c9ffe1f8 000032cf`2bce3cb6 aaaaaaaa`aaaaaaaa
000000cd`c9ffe208 aaaaaaaa`aaaaaaaa 000000cd`c9ffe4f0
000000cd`c9ffe218 00000000`00000008 00000000`00000008
000000cd`c9ffe228 000001f3`81fa3580 000000cd`c9ffe320
000000cd`c9ffe238 000000cd`c9ffe490 000001f3`81fe78c0
000000cd`c9ffe248 000000cd`c9ffe490 aaaaaaaa`aaaaaaaa
// HandleScope 里的 slot 真正存储了 Map 的指针,这里是 tagged ptr 所以末尾是 tag
// 实际的指针是 000001fc`00219e80
0:000> dq 000001f3`81fe78f0
000001f3`81fe78f0 000001fc`00219e81 000001fc`00204671
000001f3`81fe7900 1baddead`0baddeaf 1baddead`0baddeaf
000001f3`81fe7910 1baddead`0baddeaf 1baddead`0baddeaf
000001f3`81fe7920 1baddead`0baddeaf 1baddead`0baddeaf
000001f3`81fe7930 1baddead`0baddeaf 1baddead`0baddeaf
000001f3`81fe7940 1baddead`0baddeaf 1baddead`0baddeaf
000001f3`81fe7950 1baddead`0baddeaf 1baddead`0baddeaf
000001f3`81fe7960 1baddead`0baddeaf 1baddead`0baddeaf
// 查看 Map 对象
0:000> dd 000001fc`00219e80
000001fc`00219e80 00002141 1a030304 0d000421 084003ff
000001fc`00219e90 00204671 00204235 000021ed 000021e1
000001fc`00219ea0 001c43cd 00000000 beadbeef beadbeef
000001fc`00219eb0 beadbeef beadbeef beadbeef beadbeef
000001fc`00219ec0 beadbeef beadbeef beadbeef beadbeef
000001fc`00219ed0 beadbeef beadbeef beadbeef beadbeef
000001fc`00219ee0 beadbeef beadbeef beadbeef beadbeef
000001fc`00219ef0 beadbeef beadbeef beadbeef beadbeef
可以从 objects/map.h 中看到 Map 对象的内存布局,验证我们找到的确实是一个 Map 对象。
0:000> dd 0x000001fc`00204524
000001fc`00204524 00002141 1a030307 0d000421 0a4003ff
000001fc`00204534 00204671 00204235 000021ed 000021e1
000001fc`00204544 001c43cd 0020454d 000026ed 00000004
000001fc`00204554 0020455d 00000000 00003039 00000000
000001fc`00204564 00000000 00000000 00000000 00000000
000001fc`00204574 00000000 00000000 00000000 00000000
000001fc`00204584 00000000 00000000 00000000 00000000
000001fc`00204594 00000000 00000000 00000000 00000000
bp 00007ffbc0004440` trubofan function
map1 00000166`0021b9bd -> 00000166`0021b9f5
obj1 boilerplate 00000166`0021b9e5
property inner slot 00000166`0021b9e5+b
obj1-2 00000166`00242170
00000166`00242170+c
obj2-boilerplate 00000166`0021ba39
0:000> dd 00000166`0021ba39-1
00000166`0021ba38 00204525 00002259 00002259 000023e1
00000166`0021ba48 000023e1 000023e1 000023e1 cccccccc
00000166`0021ba58 cccccccc cccccccc cccccccc cccccccc
00000166`0021ba68 cccccccc cccccccc cccccccc cccccccc
00000166`0021ba78 cccccccc cccccccc cccccccc cccccccc
00000166`0021ba88 cccccccc cccccccc cccccccc cccccccc
00000166`0021ba98 cccccccc cccccccc cccccccc cccccccc
00000166`0021baa8 cccccccc cccccccc cccccccc cccccccc
obj2-2 00000166`00242189-1
0:000> dd 00000166`00242189-1
00000166`00242188 00204525 00002259 00002259 000023e1
00000166`00242198 000023e1 000023e1 000023e1 00007779
00000166`002421a8 0021ba55 beadbeef beadbeef beadbeef
00000166`002421b8 beadbeef beadbeef beadbeef beadbeef
00000166`002421c8 beadbeef beadbeef beadbeef beadbeef
00000166`002421d8 beadbeef beadbeef beadbeef beadbeef
00000166`002421e8 beadbeef beadbeef beadbeef beadbeef
00000166`002421f8 beadbeef beadbeef beadbeef beadbeef
Runtime_DefineNamedOwnIC_Miss
0:000> dd poi(value)
0000003b`5e1fdf60 00242205 00000166 00219e6d 00000166
0000003b`5e1fdf70 0000004e 00000000 00000002 00000000
0000003b`5e1fdf80 00219bd5 00000166 00242205 00000166
0000003b`5e1fdf90 00219c6d 00000166 00000027 00000000
0000003b`5e1fdfa0 0a7da030 0000015d 5e1fe000 0000003b
0000003b`5e1fdfb0 00242189 00000166 00000022 00000000
0000003b`5e1fdfc0 5e1fe000 0000003b df5c63ce 00007ffb
0000003b`5e1fdfd0 00242189 00000166 0000004e 00000000
15 d Enable Clear 00000166`0021ba70 w 4 0001 (0001) 0:**** map it's selft
16 e Disable Clear 00000166`0024216c w 4 0001 (0001) 0:**** the slot save the weak ptr to map
map 是在这创建
[0x0] v8!std::Cr::__cxx_atomic_store<int>+0x55 0x3b5e1fc5e8 0x7ffc4942fb95
[0x1] v8!std::Cr::__atomic_base<int,0>::store+0x25 0x3b5e1fc610 0x7ffc4942fb55
[0x2] v8!std::Cr::atomic_store_explicit<int>+0x25 0x3b5e1fc650 0x7ffc4943075e
[0x3] v8!v8::base::Relaxed_Store+0x2e 0x3b5e1fc690 0x7ffc49430720
[0x4] v8!v8::base::AsAtomicImpl<int>::Relaxed_Store<unsigned int>+0x30 0x3b5e1fc6d0 0x7ffc4943a655
[0x5] v8!v8::internal::TaggedField<v8::internal::MapWord,0,v8::internal::V8HeapCompressionScheme>::Relaxed_Store_Map_Word+0x55 0x3b5e1fc710 0x7ffc4943a1f8
[0x6] v8!v8::internal::HeapObject::set_map_word+0x48 0x3b5e1fc760 0x7ffc49c0f7d5
[0x7] v8!v8::internal::HeapObject::set_map_after_allocation+0x75 0x3b5e1fc7c0 0x7ffc49c4c9ec
[0x8] v8!v8::internal::Factory::NewMap+0x2bc 0x3b5e1fc850 0x7ffc4a4de54d
[0x9] v8!v8::internal::Map::RawCopy+0xcd 0x3b5e1fc950 0x7ffc4a4dfaf5
[0xa] v8!v8::internal::Map::CopyDropDescriptors+0x115 0x3b5e1fca50 0x7ffc4a4e0af3
[0xb] v8!v8::internal::Map::CopyReplaceDescriptors+0xf3 0x3b5e1fcb00 0x7ffc4a4d7a0a
[0xc] v8!v8::internal::Map::CopyAddDescriptor+0x33a 0x3b5e1fcc50 0x7ffc4a4d7545
[0xd] v8!v8::internal::Map::CopyWithField+0x405 0x3b5e1fcd80 0x7ffc4a4e3560
[0xe] v8!v8::internal::Map::TransitionToDataProperty+0x6f0 0x3b5e1fcef0 0x7ffc4a4b59f1
[0xf] v8!v8::internal::LookupIterator::PrepareTransitionToDataProperty+0x8d1 0x3b5e1fd290 0x7ffc4a567391
[0x10] v8!v8::internal::Object::TransitionAndWriteDataProperty+0xc1 0x3b5e1fd4e0 0x7ffc4a566b40
[0x11] v8!v8::internal::Object::AddDataProperty+0xbd0 0x3b5e1fd5a0 0x7ffc4a39ad15
[0x12] v8!v8::internal::JSObject::DefineOwnPropertyIgnoreAttributes+0xb15 0x3b5e1fd8a0 0x7ffc4a8d18ee
[0x13] v8!v8::internal::__RT_impl_Runtime_DefineKeyedOwnPropertyInLiteral+0xa7e 0x3b5e1fdb40 0x7ffc4a8d0b74
[0x14] v8!v8::internal::Runtime_DefineKeyedOwnPropertyInLiteral+0x164 0x3b5e1fde90 0x7ffbdf96a283
[0x15] 0x7ffbdf96a283 0x3b5e1fdf30 0x7ffbdfd9cb6a
[0x16] 0x7ffbdfd9cb6a 0x3b5e1fdf80 0x7ffbdf5c63ce
[0x17] 0x7ffbdf5c63ce 0x3b5e1fe020 0x7ffbdf5c63ce
[0x18] 0x7ffbdf5c63ce 0x3b5e1fe088 0x7ffbdf5bebdc
[0x19] 0x7ffbdf5bebdc 0x3b5e1fe0e0 0x7ffbdf5be7db
[0x1a] 0x7ffbdf5be7db 0x3b5e1fe108 0x7ffc49a7562c
[0x1b] v8!v8::internal::GeneratedCode<unsigned long long,unsigned long long,unsigned long long,unsigned long long,unsigned long long,long long,unsigned long long **>::Call+0x6c 0x3b5e1fe220 0x7ffc49a71f1a
[0x1c] v8!v8::internal::`anonymous namespace'::Invoke+0x132a 0x3b5e1fe280 0x7ffc49a7271e
[0x1d] v8!v8::internal::Execution::CallScript+0x1ce 0x3b5e1fe6c0 0x7ffc49451852
[0x1e] v8!v8::Script::Run+0x992 0x3b5e1fe7d0 0x7ffc49450ea6
[0x1f] v8!v8::Script::Run+0x66 0x3b5e1fecb0 0x7ff6a8b35c04
[0x20] d8!v8::Shell::ExecuteString+0xc04 0x3b5e1fed20 0x7ff6a8b58d97
[0x21] d8!v8::SourceGroup::Execute+0x567 0x3b5e1ff230 0x7ff6a8b5ea83
[0x22] d8!v8::Shell::RunMain+0x253 0x3b5e1ff340 0x7ff6a8b61898
[0x23] d8!v8::Shell::Main+0x1248 0x3b5e1ff450 0x7ff6a8b622d3
[0x24] d8!main+0x23 0x3b5e1ffbf0 0x7ff6a8bb6c99
[0x25] d8!invoke_main+0x39 0x3b5e1ffc30 0x7ff6a8bb6dd2
[0x26] d8!__scrt_common_main_seh+0x132 0x3b5e1ffc80 0x7ff6a8bb6e5e
[0x27] d8!__scrt_common_main+0xe 0x3b5e1ffcf0 0x7ff6a8bb6e7e
[0x28] d8!mainCRTStartup+0xe 0x3b5e1ffd20 0x7ffded34e957
[0x29] KERNEL32!BaseThreadInitThunk+0x17 0x3b5e1ffd50 0x7ffdeea4427c
[0x2a] ntdll!RtlUserThreadStart+0x2c 0x3b5e1ffd80 0x0