漏洞
DevTools IndexedDB 回调的会话释放后使用(CVE-2026-15107)
CVE-2026-15107 藏在两只不同步的时钟之间:DevTools 发出请求时,V8 检查会话仍然存活,可当 IndexedDB 的最后一行数据返回时,导航已经处置了会话,于是回调还能取得 agent、复制已经过期的原生指针,再要求释放后的检查器状态包装那行数据。

文章导航
1 最后一行数据库记录回来时,导航已经关掉了房间
1.1 代码与处置图:迟到游标回调借用了已经结束的检查会话
1.2 从命令离开请求栈,到成功事件撞上已处置会话
想象检查器向页面询问对象仓库里有哪些内容。问题刚从 DevTools 发出时,页面仍然存在,执行上下文可以使用,原生 V8 检查会话也能创建远程对象,一切看起来都很稳。危险来自这个干净开场之后:IndexedDB.requestData 不会在同一条 C++ 调用栈上把答案带回来,它启动了一项工作,结尾要由数据库事件另行送达。
这项协议方法接收安全来源、存储键或存储桶,以及数据库和对象仓库名称。客户端还可以选择索引、跳过若干记录、限制页大小并给出键范围。正因这些参数足够完整,应用面板和自动化客户端都能用它读取数据;返回页中的每一项都含键、主键和值,并以 DevTools 的 Runtime.RemoteObject 形式交给调用方。
InspectorIndexedDBAgent::requestData() 先把协议键范围转换成 Blink 的 IDBKeyRange,再解析指定存储身份对应的执行上下文,拒绝彼此冲突的输入,最后创建引用计数管理的 DataLoader。加载器接过协议回调和分页状态,开始打开数据库。到这里,原来的命令处理函数已经把责任交给几种寿命更长、彼此又不相同的对象。
打开数据库本身也是异步动作。通用加载框架等待 IDBDatabase 出现,取得 ScriptState,此后才调用 DataLoader::Execute()。较慢的存储后端、繁忙的渲染器、较大的值或普通任务调度,都可能拉长这段时间,不需要人为制造竞态工具。操作自然跨过 Blink 事件循环的多个回合,而被检查的文档仍可以随时导航。
数据库可用后,Execute() 创建只读事务并找到指定对象仓库。若请求给出了索引,它先解析索引;否则直接读取仓库。两条路径最终都会按正向顺序调用 openCursor()。返回的 IDBRequest 此刻还没有任何一行数据,它只是事件源,Blink 随后把 OpenCursorCallback 挂到它的成功事件上。
每次游标成功事件都推动同一台小状态机。监听器可能先跳过若干记录,在请求页已满时结束,也可能追加一项后继续等待下一事件。它会先调用 Continue(),再执行注入脚本,以免数据库事务过早结束。这个顺序让数据库一侧保持健康,也意味着检查器一侧的值转换可能远晚于最初请求被接受的时刻。
第一只时钟因此属于 IndexedDB:打开、建事务、交付游标、继续,最后凑齐一页。第二只时钟属于被检查目标:导航、替换文档、断开调试会话或直接关闭。CVE-2026-15107 出现在两只时钟之间:回调携带的状态足以跟着第一只时钟前进,它打算借用的一个原生资源却没有公开宣布自己的时间已经结束。
根框架导航不只是另一条数据库事件。它会重新组织检查目标,并可能处置旧会话关联的 agent。页面的数据库任务此时也许已经排队,而负责为 DevTools 描述值的 V8 检查会话,会随着目标转换被拆除。两项操作并不承诺按照人眼在屏幕上看到的先后顺序完成。
IndexedDB agent 原本知道在协议域被禁用或根框架提交新文档时,释放 indexeddb 对象组。那项清理处理的是已经为客户端创建的远程对象。修复前,通用 agent 处置却没有给借来的会话指针留下终止状态。清理函数和寿命信号彼此相关,回答的仍是两个不同问题。
等待中的监听器通过弱引用指向 InspectorIndexedDBAgent。弱引用不会迫使事件监听器让 agent 永远存活,这项设计本身是对的;但 agent 能否取得,并不能代表通过它访问的所有资源都拥有同样寿命。在出错的调度顺序里,事件执行时仍可取到 agent,OpenCursorCallback::Invoke() 的第一道检查自然会通过。
agent 内部的 v8_session_ 是带有 DanglingUntriaged 标注的 raw_ptr。这枚地址来自 agent 外部,也不归 agent 所有。外部所有者销毁会话时,已经存下的地址位不会自动变成零。哪怕再做一次非空判断,也只会看到旧地址仍然存在,无法证明地址后面的原生对象还活着。
回调还会检查 script_state_->ContextIsValid()。这道保护仍然必要:无效的 V8 上下文不能进入;但脚本上下文是否有效,与检查会话是否存活是两件事。旧代码可能在竞态窗口看到可用上下文,随后取出过期会话地址。通过一种对象的寿命检查,从来不能替另一种对象作证。
紧接着的三次调用才是内存安全落点。wrapObject() 依次把游标键、主键和值转换成 indexeddb 对象组里的检查器描述,并启用预览生成。若 v8_session_ 指向已经释放的存储,第一次转换就会进入失效的原生状态。后两次调用也说明这不是装饰性指针:返回给客户端的协议对象都要经它处理。
Chrome 把问题定为 IndexedDB 中危释放后使用;公开 CVE 描述称,构造的 HTML 可以触达它,并可能在渲染器沙箱内造成代码执行。公开记录没有宣称沙箱逃逸,也没有确认在野利用。源码给出的画面更精确:页面可影响的数据与导航遇到正在进行的存储检查,一条迟到成功事件抵达时,检查房间已经被拆掉。
2 四套所有权系统在一只回调里相遇,时钟却从未合为一只
2.1 加载器、监听器、agent 与会话各有不同寿命的正当理由
理解这项缺陷最直观的方法,是在桌上摆四张卡片:DataLoader、OpenCursorCallback、InspectorIndexedDBAgent 和 V8InspectorSession。回调把四者连在一起,却没有一只垃圾回收器或智能指针拥有整套对象。安全性只能依靠各自规则之间的交接。
DataLoader 使用引用计数。数据库打开可能在 requestData() 返回后才完成,所以普通栈寿命远远不够。创建游标监听器之前,加载器持有尚未完成的协议回调,以及仓库名称、可选索引、键范围、跳过数量和页大小。它通过 WeakPersistent 连接 agent,避免仅因存储工作尚未结束就强留检查器 agent。
OpenCursorCallback 属于 Blink 的 Oilpan 世界。MakeGarbageCollected 创建它,IndexedDB 请求把它注册成原生事件监听器,追踪机制覆盖其 agent 与脚本状态引用。DataLoader 移交协议回调后,它接过所有权并累积协议数组。游标逐行走过仓库时,正是这个对象不断接收成功事件。
监听器通过 WeakMember 指向 agent。若垃圾回收已使 agent 不可用,agent_.Get() 会返回空值,回调随即报告会话断开。补丁之前,实质相同的检查已经存在。它可以阻止访问被回收的 agent,却处理不了另一种情况:agent 存储仍在,但逻辑上已经经过 Dispose()。
InspectorIndexedDBAgent 是协议一侧的协调者。它记录 IndexedDB 域是否启用,解析框架或工作线程作用域,启动加载器,并保存借来的 V8 检查会话。目标拆除时,agent 可以先被处置;为了让等待中的事件完成收尾,它的 C++ 对象可能还会短暂可达。服务是否仍开放,与存储是否仍存在并不是同义词。
V8InspectorSession 遵循 Oilpan 之外的所有权系统。agent 在构造函数中接收原生指针,保存为 raw_ptr<..., DanglingUntriaged>,却不负责删除会话。只要每个使用方都能识别出借方何时收回资源,这种借用关系完全正常。提交 f507e6c6334f 之前,处置流程没有留下这样的可观察标记。
引用计数、追踪、弱可达性与指针加固一个不少,它们证明的却是不同事实:加载器可以活到完成,监听器可以被追踪,agent 仍可取得,地址仍能表达。任何一项都不能证明另一个所有者掌管的原生会话还接受调用。内存错误恰好住在这句缺失的证明里。
2.2 代码把“agent 仍可取得”误当成“借用资源仍存活”
把问题概括成“弱指针失效”很诱人,弱指针实际上完成了 Blink 交给它的任务:目标消失就返回空值,目标仍在就返回 agent。错误发生在下一步,代码把非空 agent 当作其内部所有资源仍然存活的证书,而那枚会话地址只是借来存放。
旧成员甚至带着 DanglingUntriaged 标注。这个名字是一声警告,不是一套寿命策略。Chromium 的 raw_ptr 机制可以改善诊断,并在受支持配置上增加悬空访问的利用难度;它不会取得对象所有权,也不会自动协调外部拆除。借用指针仍然需要一套撤销协议。
agent 处置与 agent 销毁可以有意分开。协议 agent 往往在周围的垃圾回收对象图消失之前,就先释放协议域状态、脱离会话或停止服务。等待中的回调可能还要结清协议结果,却不能再触碰处置流程已经退休的资源。同步删除所有回调并不总是可行,也未必是最稳的设计。
这项区别可以写成一条简洁不变量:InspectorIndexedDBAgent::Dispose() 一旦宣布会话不可用,后续事件就不得再经旧 V8InspectorSession 地址调用任何方法。事件仍可执行,也可能取得有效 agent 和脚本上下文,甚至必须结束客户端请求;允许的结果已经从数据转换变成一次定义明确的错误。
最可靠的检查应紧挨借用资源的使用位置。放在 requestData() 的检查只能证明命令开始时会话存在;数据库打开时再查一次,后面仍有游标事件等待交付。修复把检查放在游标继续和上下文验证之后,正好位于键、主键和值跨入检查器管理对象之前。
公开空值也让重复清理变得容易理解。DidCommitLoadForLocalFrame()、协议域的 disable() 与通用处置都可以汇入同一辅助函数。会话已经消失,辅助函数便直接返回,不会再次释放对象组;会话尚在,则用它完成最后一次清理。明确终态把脆弱的时间假设变成普通控制流。
桌上的四张卡片到这里终于讲成同一个故事:引用计数让数据库工作完成,Oilpan 安全保存事件状态,弱成员拒绝强留 agent,可空的借用指针则说明原生服务还能否调用。补丁没有把四套所有权模型硬揉成一套,只在它们相遇的位置补上了缺失的翻译。
3 只要给每次异步交接起名,受影响路径就是一条直线
3.1 requestData 变成只读事务,再变成一串游标事件
路径从输入验证开始,还没有触碰危险内存。只有协议提供了 KeyRange,requestData() 才会尝试转换;Blink 无法解析的范围会直接被拒绝。随后,它用被检查框架或工作线程全局作用域,以及唯一的存储身份调用 ResolveExecutionContext()。任一步失败,协议回调都会结束,不会创建加载器。
验证成功后,agent 创建 DataLoader,通过继承的 Start() 把执行上下文、存储桶和数据库名称交给通用打开流程。数据库成功或失败前,框架会一直持有加载器引用。调用路径因此不能停在 requestData():命令方法里既没有游标,也没有最终那次检查器调用。
数据库抵达时,DataLoader::Execute() 会再次检查弱 agent。若此时已无法取得 agent,它会报告 DevTools 会话在操作完成前断开。可触发问题的调度顺序里,这项检查依然可能通过,因为处置并不要求对象立刻被回收。加载器带着一个有效 agent 对象继续执行,却仍没有取得原生会话的新鲜证明。
加载器调用 TransactionForDatabase(),为请求的仓库创建只读事务,然后解析对象仓库;若协议指定了索引,再查找对应索引。每一步都有清楚的服务端错误返回。格式错误的协议输入与已经消失的表结构因此会变成普通失败。释放后使用只会出现在数据库操作已经成功产生游标状态之后。
索引路径和对象仓库路径最终都以 mojom::IDBCursorDirection::Next 调用 openCursor()。加载器创建一只 OpenCursorCallback,把协议回调移交给它,再把监听器注册到 success 事件。此后,这只游标监听器掌握着正常完成 IndexedDB.requestData 的唯一通道,迟到事件不能在不考虑等待客户端的情况下简单丢弃。
Invoke() 会检查事件类型和 IDBAny 结果。IDBValue 表示迭代结束,它以 hasMore=false 完成当前页;除 IDBCursorWithValue 之外的其他类型都会被拒绝。只有真实有效的游标会进入分页逻辑,此时记录的键和值都已经准备好,只差协议转换。
跳过逻辑调用一次 advance(),再清空保存的计数;页面已经装满时,监听器把累积数组与 hasMore=true 一同送回。若当前行应被收入,它会先调用 Continue(),为下一事件保住事务。完成这些数据库职责后,代码才开始靠近 V8 检查器状态。
3.2 三次 wrapObject 把过期地址变成内存安全故障
继续游标之后,监听器验证 script_state_->ContextIsValid()。上下文已经消失便直接返回;上下文有效,ScriptState::Scope 才能进入 V8。修复没有削弱或挪动这道保护,只在它后面补上缺失的会话判断,保留原有事务推进和脚本状态检查顺序。
决定性的一行把 agent->v8_session() 复制到局部指针。补丁之前,控制流会从复制直接进入 V8 作用域和对象转换。处置流程从未清空成员,因此局部值可能指向已经被外部会话所有者回收的存储。旧地址之所以显得稳定,恰恰因为旧代码没有可观察的撤销状态。
监听器还会为常量对象组 indexeddb 构造 StringView。对象组让 DevTools 能一次释放存储检查创建的远程对象。这个寿命关系决定了后面的清理顺序:存活会话必须先用于释放对象组,而处置开始后,迟到回调又绝不能继续向组里添加新成员。
一项 DataEntry 由三次检查器转换构成。游标键变成一只 Runtime.RemoteObject,主键变成第二只,存储值再变成第三只;每次调用都要求生成预览。操作因此会把真实 V8 值和有效上下文交给原生服务,而服务内部的记录归调试会话管理。
预览生成会检查足够的结构,以便向客户端描述值;公开修复并没有宣称某个通用的已释放字段,也没有给出固定利用布局。分配器状态、指针加固、值形状和调度时点都会改变可见故障。源码已经证明、也足以驱动修复的事实更窄:代码可能在会话销毁后,仍通过它的地址调用方法。
完整出错顺序到这里已经很短:客户端启动 requestData;数据库打开和游标工作异步继续;导航处置 agent 的服务关系并销毁外部 V8 会话;agent 对象暂时仍可取得;游标成功事件通过 agent 与上下文检查,复制过期指针,进入 wrapObject()。整个过程不需要畸形 IndexedDB 字节。
这条顺序也定义了安全验收。测试只需证明处置会公开空值,迟到事件得到会话断开错误,此后没有 wrapObject() 调用;无需操纵堆复用,也不必保留一次崩溃。最稳定的证据出现在内存破坏之前:一只回调、一项终止会话状态,以及终止状态之后零次转换。
4 处置流程终于会留下人人看得见的关门牌
4.1 旧清理会释放对象组,却从未撤销会话地址
类声明已经说明 InspectorIndexedDBAgent 同时服务多种环境:检查工作线程时,inspected_frames_ 为空;检查框架时,worker_global_scope_ 为空。同一协议代理因此会经历由文档或工作线程塑造的不同拆除流程。原生会话指针与这些 Oilpan 成员放在一起,管理规则却来自另一个所有者。
构造函数接收 v8_inspector::V8InspectorSession*,直接赋给 v8_session_。后续代码既用它包装值,也用它释放 IndexedDB 对象组。旧头文件没有说明处置后的空值状态,类本身也没有覆盖 Dispose();只看这段局部代码,它很容易被误认为在代理对象整个存储寿命内都有效。
旧代码有两处显式清理。根本地框架提交新文档时,DidCommitLoadForLocalFrame() 调用 releaseObjectGroup();协议域被禁用时,disable() 先清除启用标记,再调用同一方法。两处都假设会话指针在那个时点仍可调用。
释放对象组,会终结已经以该名称创建的远程对象在客户端一侧的寿命;它不会改变代理保存的指针,也不会阻止排队中的游标事件再创建一项。旧方法整理了某一批检查器对象,却没有说明创建这些对象的服务本身也可能已经消失。
通用代理处置由 InspectorBaseAgent 提供。IndexedDB 类没有覆盖实现,因此处置转换可能结束外部会话关系,却让 v8_session_ 原样保留。后续代码找不到一项本地信号,去区分“构造时拿到且仍存活的会话”和“从前拿到、后来已销毁的会话”。
这就是关键的状态表达缺口。程序里明明存在真实终止事件——代理处置——却没有把它映射成回调所读成员的终止值。于是,回调能做的最便宜观察反而会误导:地址不是零。一个已经过期但仍非零的地址,正是普通异步完成转成释放后使用的典型形状。
提交说明直白写出问题:InspectorIndexedDBAgent 在处置后不得继续使用 v8_session_。它还把 InspectorDOMAgent 指为既有模式。因此,这次修复没有另造一套寿命框架,而是把检查器代理已有的惯例带到此前遗漏了它的异步游标路径。
4.2 先释放、再清空、后委托:顺序本身就是安全证明
提交 f507e6c6334f3791b4b0e808343ceb0e95832273 增加 IndexedDB 专用的 Dispose()。第一行调用 ReleaseObjectGroup(),第二行执行 v8_session_ = nullptr,第三行调用 InspectorBaseAgent<protocol::IndexedDB::Metainfo>::Dispose()。三行就是修复核心,顺序经过有意安排。
对象组释放排在最前,因为在代理处置过程的这个时点,会话仍然存活。为键、主键和值创建的远程对象,能够通过拥有其检查器记录的服务正常退休。若先清空,辅助函数便失去唯一可用指针,对象组状态可能被遗留。补丁先保住清理,再公开关门。
清空紧随其后,因为所有后来的观察者都需要一项没有歧义的答案。赋值执行后,调用 v8_session() 的代码能够看到任何原生检查操作都已禁止。这个值不负责解释会话为何结束;导航、目标断开与其他处置原因,最终都汇成“可调用”或“已关闭”的同一种判断。
新的 ReleaseObjectGroup() 会在调用 releaseObjectGroup() 前检查指针。根框架提交与协议域禁用也改用该函数,因此多条清理路径可以依次运行,不会解引用已被处置流程清空的会话。一个可重复执行的集中规则,取代了散落在各寿命钩子里的假设。
OpenCursorCallback::Invoke() 现在复制指针后立即检查。遇到空值,它以 DevTools session detached during operation. 结束协议操作,并在进入 ScriptState::Scope 或构造 DataEntry 前返回。弱代理已经不可用时也复用同一文本,客户端因此得到一致的终止解释。
修复没有尝试在导航发生的瞬间取消每一个 IndexedDB 请求。取消动作需要同时协调数据库打开、事务状态、排队事件和协议完成。公开会话关闭状态可以让迟到交付自然失效:回调仍可抵达,看到最终服务已经不可用,结清请求,然后停在原生转换之前。
这块关门牌如今在关系两端都能看到。处置流程在会话所有者仍处于受控拆除时写下它;回调在最后一个可靠时点读取它。两者之间,C++ 对象可达性与数据库调度可以自由变化,安全不再押注哪一条事件循环先跑完。
5 回归让时序故障稳定重现,却不教堆如何崩坏
5.1 两轮、六十四行和一次立即导航,放大了天然竞态
一项好的寿命回归不必主动制造内存破坏。它只需让普通工作保持足够久,使拆除动作有机会超过它,再断言程序能够存活,并给出合理结果。Chromium 新增的检查器协议测试正是这样设计:它放大合法 IndexedDB 工作量,再用普通导航触发产品本来就必须处理的关系。
三个常量定义工作量:rounds = 2、valueCount = 64 与 valueSize = 8192。每条记录因此携带 8,192 个字符的主体载荷,再加数字后缀。六十四行合起来足以让游标转换持续一段时间,规模又很小、确定且完全良性。
决定性的协议调用一次请求整页:数据库名称、对象仓库名称、skipCount: 0,页大小则等于记录数量。测试故意不等待 dp.IndexedDB.requestData()。这不是漏掉断言,而是让命令的异步完成仍在途中,下一条协议指令便改变被检查文档。
紧接着,页面导航到一份带正文和内嵌框架的 data: 文档。它会替换旧文档并触发检查拆除,却不需要攻击者专用网址或畸形数据库。内嵌框架还让目标与上下文机制在转换期间处理真实工作。回归全程停留在公开浏览器行为内,只增加调度变化。
一项 500 毫秒的等待给拆除和游标交付留下重叠时间。单独的固定等待往往不是强证明;这里还有确定的大工作量、两次重复,以及提交说明所称的“修复前 ASAN 构建百分之百重现”。等待只是围绕真实不变量的放大器,并不声称命中某一条精确指令顺序。
预期输出刻意保持朴素:一行场景说明,随后分别写着第 0 轮和第 1 轮完成且没有崩溃。样例里没有已释放地址、栈布局或恶意载荷。成功的含义,是浏览器连续两次活过处置竞态。下游验收还可以加入本地观测,确认实际进入空值与错误路径。
两轮结束后,测试回到原来源,删除两个数据库。清理阶段接受成功、错误或阻塞完成,避免残留测试数据污染后续运行。这次返回也证明协议会话与页面在活过竞态后仍可使用。如果一项回归只是不崩溃,却把 DevTools 留在损坏状态,就不能算完整修复。
5.2 提交身份、四份变更文件和 ASAN 行为构成一条可审查证据
修复以 f507e6c6334f3791b4b0e808343ceb0e95832273 于 2026 年 6 月 26 日进入主线,作者是 Steve Becker,提交者为 Chromium LUCI CQ,主线序号是 #1653288。直接父提交 f51640cf965e2aea01e44a32ba6798dc170f32c1 位于 #1653287,为下游分支留下了准确的一前一后。
在 inspector_indexed_db_agent.cc 中,补丁增加共享的会话断开文本、紧挨 v8_session() 的空值检查、Dispose() 实现和 ReleaseObjectGroup()。根框架提交与协议域禁用的直接释放也改用辅助函数。这些改动把关闭状态接到了差异中每个可见的生产使用点。
头文件则新增虚函数覆盖声明、私有清理辅助函数,并明确记录 v8_session_ 在处置后为空。注释也是未来维护者要遵守的接口契约。下游若只复制回调检查,却遗漏处置清空,那项检查永远会看到非空旧值,缺陷仍会存在。
提交说明称,该测试在修复前的 AddressSanitizer 构建上百分之百重现。ASAN 可以直接识别无效原生访问,不必从通用渲染器退出反推原因;但这不意味着生产利用同样确定,检测器的分配行为本就服务于发现错误,也会改变时序与内存复用。
正向回归还要配正常功能对照。稳定页面应继续枚举对象仓库,遵守跳过数量与页大小,返回键和值,并在协议域禁用时释放对象组。这些对照证明新的终止状态只关闭已经断开的工作,不会让普通存储检查无缘无故失败。
寿命覆盖也不应停在上游导航用例。显式目标断开、页面关闭、协议域禁用后进入处置、工作线程终止和快速重新附着,都可能改变代理与会话事件的顺序。每种用例都应断言没有检测器报告、公开空值后没有原生转换,并且协议只按最终状态完成一次。
对直接基于上游的产品,f507e6c6334f 祖先关系加精确回归,是最干净的源码证明。供应商可以采用等价补丁或分支专用哈希,但必须把每项生产改动、测试语义和编译产物映射回这份契约。一张相似空值检查的截图,无法证明交付引擎真正运行修复顺序。
6 页面提供数据,检查器客户端才会打开那扇危险的门
6.1 构造页面与正在进行的存储检查,在一条狭窄源码路径相遇
公开 CVE 描述把 CVE-2026-15107 概括为 Chrome 150.0.7871.115 之前的 IndexedDB 释放后使用,称远程攻击者可以借构造的 HTML 在沙箱内执行代码,并记录 Chromium 严重度为中危。这段话确定产品影响;已经落地的源码则解释页面可控状态究竟通过哪项操作抵达过期会话。
受影响文件并非普通网页直接调用的 IndexedDB 接口实现,而是 Blink 的 InspectorIndexedDBAgent,也就是 DevTools Protocol 中 IndexedDB 域的实现。危险转换发生在完成 IndexedDB.requestData 时:页面掌握数据库内容并能发起导航,附着的检查器客户端则启动最终包装这些值的存储枚举。
这项分工会改变暴露分析。构造的页面行为提供调度顺序的一侧,DevTools、自动化框架、远程调试客户端或其他调试使用方提供协议请求。源码不要求页面本身取得调试权限;它要求检查活动正在进行,页面可影响的存储与导航才会进入那条异步路线。
Chrome DevTools 的应用面板是最直观的合法使用方。协议客户端也能直接调用该方法,清点数据库、浏览对象仓库、分页读取记录或诊断应用状态。浏览器测试框架、支持控制台、爬虫观测、Web 安全扫描器和内嵌开发工具,都可能在没有人显式打开 DevTools 窗口时执行类似操作。
没有附着检查器的普通浏览会话,更难直接走到这只特定回调。这项观察可以帮助排列调查顺序,却不能成为保留脆弱构建的理由。调试以后可能开启,终端资产清单也许根本不知道后台自动化负责人,产品还可能通过完全不像桌面 DevTools 的接口暴露协议能力。
远程调试需要单独治理,因为控制该端点本来就会获得强大的浏览器操作能力。服务应绑定到预期接口,外围实施认证与网络策略,并移除意外的公网暴露。这些措施能减少许多攻击机会,却无法修复合法本地检查会话遇到的寿命竞态。
共享浏览器农场应进入较早更新批次。一只长期运行的工作进程可能访问不可信站点,同时由测试框架收集存储、控制台、网络或页面状态;它可以跨越许多作业,只在个别任务里发出 IndexedDB 命令。这种组合使运行可达性与旧进程风险都很容易在主机级清单里消失。
类中成对出现的 InspectedFrames 与 WorkerGlobalScope 成员说明,它可以服务框架和工作线程。公开回归证明的是根框架导航,并没有覆盖每一种工作线程拆除顺序。产品应测试自己真正暴露的上下文,对外结论则要紧扣补丁中可见的框架导航证据和共享处置契约。
公开来源没有给出完整跨进程利用链、沙箱逃逸、持久化或已经确认的在野利用,这些未知项仍需保留。即便如此,一项源码已证实的释放后使用会处理攻击者可影响的值;尤其在自动执行存储检查或调试入口较宽的环境,仍足以要求快速更新浏览器。
6.2 产品版本、内嵌 Chromium 与正在运行的检查进程是三项不同事实
Chrome 7 月 8 日桌面公告把 Windows 与 macOS 稳定版更新到 150.0.7871.114 或 150.0.7871.115,Linux 则是 150.0.7871.114。同一公告还给出 2,000 美元奖励、问题编号 503553615,以及蚂蚁集团天穹安全实验室 zh1x1an1221 在 4 月 17 日报告的归属。部署规则必须保留平台差异。
CVE 记录把影响范围统一表达为低于 Chrome 150.0.7871.115,这种写法适合漏洞数据库;若脱离平台语境直接用于软件包关口,却会误导:公告里的 Linux 固定构建是 .114,Windows 或 macOS 也可能从同一次发布取得相邻构建。应采用厂商实际渠道软件包,不能拿一个通用后缀判断所有平台。
Chromium 内嵌产品需要源码映射。Electron 应用、CEF 外壳、自动化浏览器、厂商支持工具、自助终端运行时、IDE 内嵌视图和私有桌面外壳,都可能携带自己的 Chromium 快照。提交 f507e6c6334f 或有文档支持的等价回移,比遵循另一套日历的外层产品版本更精确。
同一主机可以同时存在系统 Chrome、Chrome for Testing、框架下载的浏览器、应用内置引擎、旧回滚目录和容器镜像。更新一个软件包后便把整台主机标成“已修复”,很可能抹掉自动化实际调用的那份副本。资产状态应绑定到可执行文件和它加载的 Blink 模块。
协议可达性同样是运行时事实。命令行开关、企业策略、嵌入接口、端口转发、测试框架配置与扩展权限,都会影响哪个客户端能抵达目标。应在真实业务会话中记录有效调试路线,包括进程所有者和服务账号。文档里的默认关闭值,无法描述生产启动参数已经覆盖后的结果。
完成安装也不会替换正在运行的代码。桌面 Chrome 可能保留后台进程,浏览器农场会让工作进程常驻,IDE 也可能在整个应用退出前一直保留内嵌渲染器。证据要说明固定软件包已经落盘、旧浏览器和渲染进程已经结束、新进程确实从修复目录映射文件。
持续运行的自动化服务需要受控排空。先停止分配新作业,再按服务策略等待或结束当前工作,安装固定浏览器,重启控制器与工作进程,最后执行良性存储检查。若只更新工作进程模板,已经运行的脆弱实例可能一直活到弹性扩缩容偶然将其回收。
软件包尚在发布途中时,限制调试访问可以暂时降低暴露,但必须有负责人和到期时间。还要确认本机辅助服务、扩展、支持隧道与内嵌客户端都无法继续发出存储命令;恢复服务必须以固定运行时证明为条件。单独一条网络防火墙记录,回答不了主机内部的协议使用方。
7 崩溃栈只是故事里的一枚时间戳,不是判决书
7.1 先连接构建、协议请求、导航与会话拆除,再判断是否相关
用户可能只看到渲染器消失、DevTools 目标断开、应用面板请求失败,或自动化作业报告页面关闭。任何一种症状都不会直接写出 CVE-2026-15107,导航和调试本来就会产生相似消息。只有脆弱构建、正在进行的 IndexedDB 检查、会话处置与迟到游标转换出现在同一时间线上,调查才开始具有专属性。
先收集不可变版本事实:外层产品及完整四段版本、可取得的内嵌 Chromium 修订、软件包来源与摘要、可执行文件路径、已加载 Blink 模块身份、进程启动时间,以及进程是否跨过最近更新继续存活。由框架管理的浏览器还要记录故障作业实际使用的下载缓存键与启动路径。
较强的原生调用栈可能包含 OpenCursorCallback::Invoke、V8InspectorSession::wrapObject、远程对象预览代码、IndexedDB 事件分发或检查器代理处置。AddressSanitizer 还有机会指出会话对象的分配与释放位置。厂商构建的符号质量不同,缺少某一个精确函数名不能直接结束调查。
协议时间要与渲染器时间分开还原。保存 IndexedDB.requestData 的请求标识、去除敏感值后的参数、目标或会话标识、页面导航事件、断开通知与客户端最终结果。分析时可以统一时钟,同时保留原始时间戳,方便以后识别日志管线引入的偏差。
开发者在打开 DevTools 查看存储时正常导航,也能触发相同竞态,单次崩溃无法证明恶意意图。作出事件归因前,应寻找同一页面反复投递、有意放大数据库、立即导航模式、未预期的调试连接,或多目标协调故障。正常开发与支持行为需要被明确区分。
没有崩溃也不是有力排除。事件窗口会受到数据库时延、任务调度、是否启用检测器、分配器复用、指针加固和客户端请求记录数量影响。脆弱进程可以多次经过旧路径而不产生可见故障。全局修复应由源码与运行版本驱动,不能由崩溃频率决定。
某个更新前事件确需复现时,先在隔离环境运行公开良性回归,确认旧构建在 ASAN 下失败,固定构建则在转换前公开空值。事件样本只按组织既有证据规则封存。决定引擎必须更新,并不需要把输入继续加工成稳定利用。
7.2 低噪声遥测无需记录数据库内容,也能观察寿命判断
测试与金丝雀构建可以观测三个位置:进入 InspectorIndexedDBAgent::Dispose()、回调读取 v8_session(),以及每一次准备调用 wrapObject()。为检查会话分配单调递增的代际值,关系便会显形:代际 N 处置之后,任何标记为 N 的转换都不得开始。
为协议请求、代理和回调生成同一枚随机、短期的关联标识。不要拿用户账号、网址、数据库值或稳定设备标识充当连接键。目标只是排序一次调试操作内部的事件,不是另外建立一份被检查页面存储历史。
一条紧凑事件可以包含构建标识、进程类型、协议方法、目标种类、会话代际、回调阶段、代理是否存在、会话是否存在、上下文是否有效、记录序号与完成类别。这些字段足以区分“代理被回收”“会话已处置”“上下文消失”、正常页面完成,以及关门后仍开始转换。
除了崩溃,还要统计协议完成。每项请求只能让客户端观察到一次成功或失败,并识别因整个目标消失而真正放弃的请求。若崩溃曲线下降,挂起请求却不断增加,回归仍然不干净。寿命加固必须保住一项有结尾的客户端契约。
检测器浏览器农场仍是保存详细轨迹的最佳位置。在受支持操作系统和关键下游分支上反复运行上游样例,启用符号与分配栈,并围绕空值状态加入确定性断言。这样,未来调度变化即使让原崩溃消失,也不能把测试悄悄变成假绿。
生产遥测应关注发布质量:仍在运行的脆弱版本、安装后没有退出的旧进程、固定构建上的会话断开错误、存储检查期间的异常渲染器退出,以及无限重试的自动化作业。这些信号用于发现部署漏点和兼容问题,不能替代提交祖先关系或精确回归。
最好的检测结果其实很朴素:固定进程报告一次会话断开,回调记录会话为空,零次转换开始,下一项合法 DevTools 请求照常工作。这段小小顺序证明关门牌确实能在运行时看到,比一张空白崩溃图更具体,也比在生产环境探测内存破坏安全得多。
8 一次发布要经受四次证明:源码、构建、进程,以及那次迟到的回调
补丁落库并不等于风险已经离开运行环境。源码要证明撤销状态出现在正确位置,回归要证明迟到事件真正撞上这项状态,制品要证明编译进去的是同一语义,进程证据则要证明旧 Blink 模块已经退出。四次证明沿用同一个对象身份,才能把开头那间已经关门的房间真正从生产里拿走。
8.1 源码与回归必须证明同一次状态转换
固定起点是提交 f507e6c6334f3791b4b0e808343ceb0e95832273、父提交 f51640cf965e2aea01e44a32ba6798dc170f32c1、Chromium 位置 #1653288 与 Gerrit 变更 8007837。四个改动文件共同完成一件事:让处置流程在仍可清理对象组时使用会话,随后公开空值;让游标回调在首次对象转换前读取这个值。下游实现可以采用可空句柄、会话代际或主动取消,但必须保住同样的撤销时点、首次使用判断和正常清理顺序。
检查范围应覆盖构造、Dispose()、DidCommitLoadForLocalFrame()、域停用、对象组清理,以及所有读取 v8_session_ 的异步消费者。关键不是补丁文本相似,而是每条迟到路径都无法在撤销后抵达 wrapObject()。构建清单还要指向这份实现,避免仓库已经更新,目标平台却继续编译旧快照或缓存对象。
上游网页回归把时序写得很清楚:连续两轮创建同源页面和数据库,写入 64 条记录,每条值含 8,192 个字符;随后以页大小 64 发出 IndexedDB.requestData,不等待回复便导航到带正文与内嵌框架的数据网址,再等待 500 毫秒。提交说明称父修订版上的 ASAN 构建可稳定复现;这项复现率来自上游提交说明,不应被改写成独立复现结论。
固定构建的通过标准不能只有“没有崩溃”。事件记录应显示导航触发处置、会话被置空、游标回调随后抵达、空值判断被执行、对象转换次数为零,协议请求只完成一次;新目标中的下一次普通 IndexedDB 检查还要成功。ASAN 能捕捉非法内存访问,协议断言则能发现重复完成、悬挂请求或通过永久禁用功能换来的假安全,两者回答的问题不同。
8.2 安装只有在旧进程退场后才会生效
签名制品要带着平台、架构、渠道、版本、摘要和源码映射进入部署。Chrome 7 月 8 日公告给出的锚点是 Windows/macOS 150.0.7871.114/.115 与 Linux 150.0.7871.114;衍生产品则应给出自己的构建身份以及上游修复或等价回移。外层产品号较大、关于页面显示新版本,都不能代替实际 Blink 模块的来源。
发布前先盘点正在运行的浏览器、渲染器和自动化工作进程,停止接收新任务,排空或有序结束现有会话,再以不可变制品替换并启动干净进程。金丝雀随后打开受控页面,建立一个小型 IndexedDB 仓库,通过生产实际使用的客户端枚举记录、执行导航,再在新目标中发出第二项正常请求。第二项成功证明修复关闭的是旧会话,而不是把整个检查能力永久封死。
历史事件仍要按证据强度分层。脆弱构建上出现 IndexedDB 请求与导航,只能证明经过相关路径;相符的 ASAN 或调用栈会提高置信度;日志若显示处置后仍开始对象转换,才更接近根因。公开资料没有确认在野利用,也没有公开沙箱逃逸链,因此任何代码执行、攻击者身份或实际数据影响都必须由本地证据另行证明。
在进程替换完成前,限制远程调试接口、保护自动化凭据、隔离工作网络并暂停非必要的存储检查,能够减少触发机会,却不能成为永久结论。本地 DevTools 与可信自动化仍使用同一 agent,页面仍能控制数据库内容和导航时序;旧进程归零、固定行为通过验证以后,临时控制才有撤除依据。
这项修复留下的设计问题比一个 CVE 更耐久:异步回调借用外部原生资源时,必须写清主人、撤销事件、状态公布、最后一次合法操作和全部迟到消费者。弱引用只说明 agent 是否可达,脚本检查只说明上下文是否有效;把失效状态写成可观察值,并在第一次依赖资源的操作前检查,才能让不同所有权系统共享同一条终止规则。
最后一行记录仍然会迟到。不同的是,固定构建先完成合法清理,再把借来的会话写成空值;回调抵达后在第一项转换前停下,以定义明确的错误结束。下一间房用新会话打开,下一项请求正常返回。源码、回归、制品和运行进程都证明了这次关门,故事才真正结束。
研究记录
9证据、对象与来源
下面保留本文实际使用的标识、时间和原始材料,便于继续调查。
9.1研究对象
报告涉及的产品、行为者、技术、受影响对象和控制点。
Blink IndexedDB 检查器释放后使用
处置流程清空借用的 V8 检查会话
用于源码对比的干净父节点
迟到的游标结果进入 wrapObject
异步读取对象仓库或索引
两轮、64 行、8,192 字符载荷与立即导航
9.2事件时间
- 问题报告给 Chrome
蚂蚁集团天穹安全实验室的 zh1x1an1221 报告了 IndexedDB 释放后使用。
- 生命周期修复进入 Chromium 主线
提交 f507e6c6334f 在序号 #1653288 发布处置状态并加入 ASAN 回归。
- Chrome 发布稳定版安全更新
Chrome 分配 CVE-2026-15107、评为中危,并在 150.0.7871.114/.115 发布中交付各平台软件包。
- SOSEC 完成源码重建
SOSEC 贯通协议入口、四种独立寿命、回调顺序、产品映射、事件证据与发布证明。
9.3来源与材料
- Chrome 2026 年 7 月 8 日桌面稳定版更新https://chromereleases.googleblog.com/2026/07/stable-channel-update-for-desktop_01162222768.html
- NVD 的 CVE-2026-15107 记录https://nvd.nist.gov/vuln/detail/CVE-2026-15107
- Chromium 修复 f507e6c6334fhttps://chromium.googlesource.com/chromium/src/+/f507e6c6334f3791b4b0e808343ceb0e95832273
- 父提交 f51640cf965ehttps://chromium.googlesource.com/chromium/src/+/f51640cf965e2aea01e44a32ba6798dc170f32c1
- Chromium 评审 8007837https://chromium-review.googlesource.com/c/chromium/src/+/8007837
- 修复后的 InspectorIndexedDBAgent 实现https://chromium.googlesource.com/chromium/src/+/f507e6c6334f3791b4b0e808343ceb0e95832273/third_party/blink/renderer/modules/indexeddb/inspector_indexed_db_agent.cc
- 修复后的 InspectorIndexedDBAgent 声明https://chromium.googlesource.com/chromium/src/+/f507e6c6334f3791b4b0e808343ceb0e95832273/third_party/blink/renderer/modules/indexeddb/inspector_indexed_db_agent.h
- 双轮导航竞态回归源码https://chromium.googlesource.com/chromium/src/+/f507e6c6334f3791b4b0e808343ceb0e95832273/third_party/blink/web_tests/http/tests/inspector-protocol/storage/indexed-db-request-data-race-with-navigation-large-db.js
- 导航竞态预期输出https://chromium.googlesource.com/chromium/src/+/f507e6c6334f3791b4b0e808343ceb0e95832273/third_party/blink/web_tests/http/tests/inspector-protocol/storage/indexed-db-request-data-race-with-navigation-large-db-expected.txt
- Chrome DevTools Protocol 的 IndexedDB 域https://chromedevtools.github.io/devtools-protocol/tot/IndexedDB/
- Chromium raw_ptr 文档https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/memory/raw_ptr.md
- Blink Oilpan 垃圾回收接口参考https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/platform/heap/BlinkGCAPIReference.md
- Chromium 网页测试文档https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/testing/web_tests.md
- 修复提交引用的 InspectorDOMAgent 处置模式https://chromium.googlesource.com/chromium/src/+/284b36ac2742525000db2ca28f448f6cc8584f40/third_party/blink/renderer/core/inspector/inspector_dom_agent.cc