PWN-Tips
  • CVE-2022-3723 分析

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