漏洞

KVM 把 24 字节 scratch 当成整页——重走 GHCB 越界(CVE-2026-53360

CVE-2026-53360 让恶意 SEV-SNP 客体把本不存在的 PSC 长度解释成 24 字节页外 KVM 堆对象,再让它借用完整 GHCB 才拥有的 253 项上限,造成公开 KASAN 已确认的越界与释放后使用;本文沿对象穿过 VMGEXIT、userspace completion 和十提交修复,把结论落实为上游与发行版版本、L0 重启、SNP 调度、证据保全和回归验收。

手绘技术封面:左侧封闭的机密客体把一只只有三格的短托盘递过单页共享服务窗,右侧 KVM 宿主的分配器货架很长,托盘末端的虚线格错误地伸向相邻宿主对象。
文章导航

研究依据SOSEC 安全研究 · 基于 2026 年 7 月 17 日固定的 Linux CNA JSON、AMD GHCB 56421 Rev. 2.04、漏洞父提交与十提交修复序列、四条稳定分支映射及发行商状态完成本地源码复核

来源Linux CNA / Linux 上游与稳定分支源码 / AMD GHCB 规范 / 发行商安全页 / SOSEC 源码复核

1 24 字节对象只有两项,旧 KVM 却用 253 项上限放行

Linux CNA 公布的 KASAN 测试把 CVE-2026-53360 压缩成一组可以复算的对象边界:一份 24 字节宿主对象先放八字节 PSC 头部,余下空间只能容纳两个八字节条目;旧 KVM 却用完整 GHCB Shared Buffer 才拥有的 253 项上限校验结束索引。数组下标 2 从 offset 24 开始,第一次八字节访问已经落在对象之外。公开测试共得到 73 份报告,其中 62 份是 slab 越界、7 份是 slab 释放后使用、4 份是一般释放后使用。

这些材料来自公开测试,不是一则已确认的云平台事故通报,也没有提供在野利用或稳定宿主代码执行的证据。它们仍足以固定完整因果:一个没有 PSC 长度含义的字段先创造短宿主对象,页级容量随后支配对象内循环,userspace completion、同步和释放又让同一边界错误跨过多个生命周期阶段。全文沿这只对象从 VMGEXIT 走到清理,再把修复落实到运行版本、SNP 调度、证据保全与回归验收。

代码位置与处置可以先落到一个决定面。问题位于 arch/x86/kvm/svm/sev.csetup_vmgexit_scratch() 曾让 GHCB v2 的页外 scratch 变成宿主对象,snp_begin_psc() 又没有按 ghcb_sa_len 计算本次 PSC 容量。Linux CNA 把受影响上游范围从 6.10 开始列出,官方修复下限为 6.12.93、6.18.35、7.0.12 和 7.1;发行版需采用含等效修复的内核并重启 L0,只有验证过的节点才能重新接收 SNP 客体。窗口内可把未知或未修节点移出 SNP 调度、迁移和 HA 目的池,必要时暂停不受信 SNP 负载;客体更新与网络规则不能代替宿主修复。

把它想成一只短托盘:第一格放八字节头部,第二、第三格各放一个八字节条目,24 字节正好用完。墙上另有一张属于整张共享工作台的告示——最多可以处理 253 个条目。旧 KVM 没有先量托盘,只抬头看了告示;只要客体写下的结束索引小于 253,它就继续向后取格子。第三个条目,也就是数组下标 2,已经在托盘之外。

这个比喻听起来像普通的数组越界,真正的难点却在“24”是怎样出现的。Page State Change 协议没有随请求提交一份总长度。GHCB 版本 2 的规范反而要求 PSC 结构待在同一页 GHCB 的 Shared Buffer 里,宿主本可从指针位置算出到页尾还剩多少字节。旧分派却把 exit_info_2 递给 setup_vmgexit_scratch(),仿佛那就是 PSC 长度,于是规范中不存在的尺寸变成了一次真实的 kvzalloc()

第二处错认紧跟着第一处。snp_begin_psc() 知道完整 GHCB 工作区最多容纳 253 个八字节条目,却不知道它收到的指针此刻只指向一个更短的外部分配。代码校验了协议理论最大值,没有校验本次分配实际最大值。一个字段被当成了不存在的长度,一张整页容量牌又被发给了短对象,两次各自看似局部的决定在同一条调用链里接上了。

Linux CNA 把后果写得很明确:恶意 SNP 客体能够破坏宿主内核堆,并借不同应答观察宿主堆布局。报告同时出现 slab 越界与释放后使用,说明错误不会在第一次读错条目时必然结束;同一对象还会经历交给 userspace、完成条目、同步回客体与释放等阶段,allocator 布局和时序会改变最终症状。

同样重要的是没有被证据证明的部分。公开材料没有确认在野利用,没有公开一条稳定的宿主代码执行链,也没有证明攻击者可以自由选择宿主写入地址和值。完成路径更新的是 PSC 条目中的 cur_page 位域,公开分析给出的受约束值为 1 或 512;越界条目是否被接受、邻接对象是什么、对象何时复用,都会改变可观察结果。本文会解释这些条件,不会把“宿主堆破坏”扩写成已有证据并不支持的稳定宿主接管。

可达链并不从普通未加密虚拟机开始。漏洞验证分支要求当前 vCPU 属于 SEV-SNP 客体,客体能够控制 GHCB 字段并发起相应 VMGEXIT,VMM/userspace 还要支持 PSC 流程使用的 KVM_HC_MAP_GPA_RANGE exit。硬件支持 SEV、宿主加载了 kvm_amd,都只是资产筛选的第一层,不能单独证明攻击路径可达。

1.1 机密计算保护客体私有房间,不替宿主检查共享托盘

AMD Secure Encrypted Virtualization 先让客体内存以不同密钥加密,SEV-ES 进一步保护寄存器状态,SEV-SNP 又加入反重映射完整性、度量与证明能力。它们解决的是宿主或其他客体不应随意窥看、替换受保护客体私有状态的问题。受保护客体仍要请求宿主执行 I/O、修改页状态或模拟特权操作,双方因此必须保留一个都能读写的通信面。

Guest-Hypervisor Communication Block,简称 GHCB,就是这扇服务窗。它占一页共享/解密内存,客体在里面说明退出原因、哪些保存字段有效、辅助参数在哪里;宿主映射这一页,验证协议版本和字段,再把请求分派给 KVM 内部或 VMM userspace。共享让宿主能看见数据,也让客体能继续改变数据,绝不等于这些字节已经可信。

把 SNP 的证明报告想成房间封条,会更容易理解。封条可以证明客体以预期镜像启动、私有内存遵循某些保护,它不会替宿主量一只从服务窗递出来的托盘。托盘里的地址、索引、长度语义和条目仍由对端组织;越是高权限的宿主解析器,越要把每个值限制在自己实际接受的对象里。

CVE-2026-53360 没有破解内存加密。客体没有让宿主直接读出私有页,也没有伪造 SNP 的密码学证明。缺陷发生在双方有意共享的 GHCB 控制面:宿主解析器先违反版本 2 对 scratch 位置的要求,又用错误的容量规则遍历复制到宿主堆的对象。加密继续完成它的工作,C 代码却走出了自己的分配。

这一区分会改变调查重点。团队不需要先寻找内存加密算法异常,而应核对宿主正在运行的 KVM 分支、GHCB v2 处理代码、SNP 客体清单和崩溃栈。客体证明成功也不能把节点标成安全,因为证明回答“客体是谁”,不回答“宿主怎样解析客体提交的共享结构”。

它也解释了为什么威胁模型允许“恶意机密客体”。云平台把客体内核控制权交给租户,本来就是虚拟机产品的正常含义;机密计算更强调宿主不能阅读租户私有状态。平台不能假设一台通过证明的客体永远善意,也不能把客体内核被攻陷后的 GHCB 请求当成可信输入。hypervisor 必须在面对最坏客体时仍守住宿主对象。

普通应用进程通常不能直接随意构造 GHCB PSC 结构。攻击者需要控制 SNP 客体内核、加载等价高权限代码,或取得能够形成相同 VMGEXIT 状态的能力。这个前提缩小了入口,却没有降低多租户宿主的故障半径:一台恶意客体触发的是 L0 内核堆错误,同宿主上其他客体和管理进程都可能随崩溃受影响。

从工程视角看,GHCB 只是“共享内存 RPC”的一个严肃版本。发送者写结构,接收者检查并执行;结构可能在检查后继续变化;不同协议版本可能改变地址和长度含义;一项操作还可能跨越 kernel 与 userspace 完成。许多设备环形队列、virtio descriptor、共享 mailbox 和固件通信区都有同一类风险,本文最后会把这只托盘留下的规则带回那些系统。

手绘剖面:左侧机密客体的私有内存封闭在深蓝房间,中央只有一扇单页共享服务窗,右侧宿主工程师用卡尺检查托盘后才接近内核对象货架。
图 1:SNP 保护左侧私有房间;GHCB 是双方有意打开的共享窗。宿主能读取窗内托盘,不代表托盘尺寸、索引或内容可信。

1.2 先固定证据,再重建因果链

本报告以 Linux CNA 原始 JSON 为版本和影响主锚,以 AMD GHCB 56421 Rev. 2.04 为协议锚,再把漏洞父提交、主修复和九个相邻加固提交逐个对照。当前 Linux 头文件确认 struct ghcbpsc_hdrpsc_entry 的实际布局;Ubuntu、Red Hat 和 SUSE 页面只承担产品包查询入口,不代替上游源码解释。

这样分层很重要。CVE 摘要擅长告诉读者“发生了什么”和“哪些上游版本修复”,规范告诉我们“版本 2 本来承诺什么”,提交差异才显示“错误值怎样穿过函数”。如果只读摘要,很容易把 exit_info_2 误写成客体按 PSC 协议提供的长度;如果只读补丁那四行,又会看不见后续容量和快照修复为何必要。

厂商状态则必须带日期阅读。发行版会按自己的内核基线回补提交,一个低于上游 6.12.93 的包可能已经含等效修复,一个高于某个数字的自定义构建也可能缺少关键加固。正文给出 2026 年 7 月 17 日的查询快照和官方上游修复地板,最终验收仍要求把 vendor advisory、包构建、源码行为与正在运行的 boot ID 连起来。

2 一页 4,096 字节,真正能摆托盘的只有 2,032

服务窗的外框是一页 GHCB,x86 常见页面大小为 4,096 字节。站在远处看,它像一块完整的共享板;走近源码,板面被分成三个不同用途的区域。前半放 GHCB save area 与固定控制字段,中间从 2,048 字节偏移开始是一块 2,032 字节 Shared Buffer,最后十六字节留给协议版本、usage 和保留尾部。

struct ghcb 的静态布局要求整体恰好等于 PAGE_SIZE。这不是为了美观:客体和宿主必须对每个字段所在位置达成一致,才能在寄存器状态受保护时交换信息。sw_exit_codesw_exit_info_1sw_exit_info_2sw_scratch 等字段位于保存区中;可变长负载则借用 shared_buffer[2032]

“页内”还不够精确。scratch 指针必须落在 Shared Buffer 的允许范围,不能指向 save area,也不能落在页尾元数据。即使指针合法地位于 Shared Buffer 中段,它能使用的也只有从当前位置到 Shared Buffer 末尾的余量,不会因为大家都在同一页就自动得到完整 2,032 字节。

这个细节正是提交 5867d7e202e0 后来补齐的内容。准备函数接收的不再是一个含糊的“总长度”,而是消费者至少需要的 min_len;当地址在页内时,KVM 从 scratch 起点算到共享区末尾,把这段真实余量保存为 ghcb_sa_len。地址越靠后,容量越小,后续消费者都必须服从这把尺。

这和普通 C flexible array 的安全规则相同。结构体类型能告诉编译器头部多大,不能告诉它本次分配到底带了多少尾随元素;指针落在某个大对象内部,也不意味着从该指针向后仍有整个大对象长度。唯一可靠的长度来自已验证的起点和已知末尾之差。

2.1 从 save area 走到 Shared Buffer 末端

Linux 的 GHCB 结构在偏移 0 处开始保存受保护退出所需的寄存器与控制状态,再用填充把下一块工作区对齐到 0x800,也就是十进制 2,048。紧随其后的 Shared Buffer 有 2,032 字节,从 0x800 延伸到 0xff0。页末最后 0x10 字节容纳保留字段、两字节协议版本和四字节 usage。

这些十六进制偏移不是读者必须背诵的 IOC,却解释了为何“2032”不是四舍五入。若把中间工作区误写成 2,048 字节,PSC 理论容量就会从 253 被算成 255;若把完整页面 4,096 当作工作区,差距更大。安全审计里的一个常见错误,是拿容器总尺寸替代某个成员的可用尺寸。

sw_scratch 保存的是客体物理地址,不是宿主已验证指针。KVM 映射当前 GHCB 后,要判断这个地址是否指向本页 Shared Buffer,并将客体地址转换成已映射页内的位置。旧路径还允许地址落在 GHCB 之外,再从客体内存复制一块临时对象;版本 1 的某些操作需要这种兼容行为,版本 2 的 PSC 明确不该走那里。

地址检查不能只验证起点。任何最低头部读取都要保证 start + min_len 没有越过允许区域,并处理整数回绕;可变数组的总容量则以验证后的对象余量计算。修复后的 helper 把“能不能读最小结构”与“后面总共还有多少空间”分开,这比让调用者传一个既像需求又像事实的 len 更清楚。

版本字段位于页尾,却影响整个页的解释。客体和 KVM 协商 GHCB protocol version 后,同一个 sw_scratch 在不同版本下拥有不同合法位置。安全代码不能只看地址“以前是否接受过”,必须先根据协商版本选择规则,再验证这次地址是否符合该规则。版本不是显示信息,而是解析契约的一部分。

对下游发行版源码复核而言,成员名和函数签名可能变化,页布局却由 ABI 规范固定。验收人员不必执着于补丁文本完全相同,而应确认行为:版本 2 及以上拒绝 Shared Buffer 之外的 PSC scratch,页内有效长度从实际地址算到工作区末尾,最小头部在第一次解引用前已被覆盖。

手绘单页内存板:深蓝固定控制区占据前部,赭黄色共享工作区接近后半并分成格子,灰绿色页尾元数据只是一条细带,卡尺和卷尺标出 scratch 从中段到末尾的真实余量。
图 2:GHCB 总计 4,096 字节;可变负载使用的是 2,032 字节 Shared Buffer。scratch 若从中段开始,只能继承右侧余量。

2.2 253 是整张工作台的极限,不是每只托盘的配额

SEV-SNP 的 PSC 全称是 Page State Change,不是 Process State Change。它让客体请求改变一段 guest physical page 的状态,例如在私有与共享用途之间转换。客体把一份 psc_hdr 放在 scratch 开头,后面紧跟若干 psc_entry;宿主与 userspace 逐批处理,并把进度写回共享结构。

struct psc_hdr 正好八字节:两个 16 位索引 cur_entryend_entry 加一个 32 位保留字段。每个 packed psc_entry 也正好八字节,里面用位域保存当前页进度、40 位 GFN、操作类型与保留位。这里没有 C 指针,也没有协议总长度字段,数组有多少项必须从承载对象的字节数推出来。

如果头部从 2,032 字节 Shared Buffer 的第一个字节开始,算式是 (2032 - 8) / 8 = 253。这 253 是条目数量,合法的零基结束索引最大为 252;内核用 end_entry >= max_nr_entries 一类判断拒绝第一次越界。只要 scratch 起点后移八字节,可用条目就少一个。

24 字节对象的同一算式是 (24 - 8) / 8 = 2。下标 0 与 1 在对象内,下标 2 不在。旧代码却没有把 24 带进这个算式,只拿 end_entry 与固定的 VMGEXIT_PSC_MAX_COUNT 比较。因此 end_entry = 10 通过页级检查,却要求访问从 0 到 10 的十一项,远超两项容量。

Linux 头文件里还有一个容易混淆的 VMGEXIT_PSC_MAX_ENTRY 64。它服务于客体侧分批组织条目的栈上缓冲,不是 GHCB 页能容纳的总条目数。安全文章若把 64、253 与本次对象容量混成一个“限制”,读者就无法判断哪一层检查缺失。名字相似的常量必须回到使用位置解释。

cur_entryend_entry 还表达处理进度。宿主不一定一次完成整个数组,userspace 返回后可以从更新后的当前位置继续。这让对象在一次 VMGEXIT 内跨越多个阶段,也让客体在共享页上修改字段的时机更重要。容量正确只解决空间问题,稳定读取还要解决时间问题。

修复因此选择实际对象长度,没有简单把 253 改小。即使把固定上限降到 64,一只 24 字节对象仍只能容纳两项;任何大于 2 的常量都不安全。反过来,对完整页一刀切只允许两项又会无谓破坏合法批量操作。正确约束必须随本次对象变化。

规范还明确说 PSC 结构应位于 GHCB Shared Buffer,并要求 hypervisor 确认索引没有越过该工作区。只落实位置、不落实索引容量,会让指针靠近页尾时仍出错;只落实容量、不落实位置,又保留页外临时对象与版本语义冲突。十提交序列之所以有价值,正因为它把两条要求在实现里重新接在一起。

读到这里,那只 24 字节托盘仍然不该存在。按照版本 2 的规则,PSC 应当留在页内,宿主从页尾自然得出容量。下一章要追的就是历史兼容路径怎样绕开这份自然尺寸:一项新的协议版本进入 KVM,一段旧 helper 仍等待调用者交给它一个长度,而调用者手里根本没有这项协议数据。

3 协议没有提交长度,KVM 却替它编了一个

时间回到 2024 年。提交 4af663c2f64a 让 KVM 可以为每个客体配置 GHCB 协议版本,并把可协商上限提升到版本 2。新的 KVM_SEV_INIT2 路径能为受保护客体选择 v2,旧的初始化接口继续保留 v1 行为。Linux CNA 将受影响上游范围从 6.10 开始计算,正是因为版本 2 的语义从这里进入原有 SEV VMGEXIT 代码。

引入新版本本身没有写出越界循环。旧的 scratch helper 为若干操作兼容页外客体内存:调用者给一个地址与长度,helper 可以分配宿主缓冲、把客体字节复制进来、处理后再同步回去。这在允许页外 scratch 的旧协议场景里有明确用途。问题在于版本 2 的 PSC 已经改变了契约,调用端和 helper 之间的接口却仍像过去一样等待一个 len

PSC 支持来自更早的提交 9b54e248d264。当 SNP 客体用对应的 sw_exit_code 发起 VMGEXIT,KVM 验证必需 GHCB 字段,再进入 SVM_VMGEXIT_PSC 分支。旧代码随后调用 setup_vmgexit_scratch(vcpu, true, control->exit_info_2)。最后一个实参看起来规整,语义却没有协议依据。

AMD PSC 定义并没有说 exit_info_2 是总长度。后续修复提交 2be54670bdc0 的说明直言,原先把它当长度是实现自行编造的行为。修复不再假定客体给了完整尺寸,而只向 setup 表达消费者的最低需求:在第一次读取前,至少要有一份 sizeof(struct psc_hdr)

3.1 从 VMGEXIT 分派到错误的 exit_info_2

一轮受保护退出从客体执行 VMGEXIT 开始。KVM 获取并映射当前 GHCB,读取 protocol version、usage 和 valid bitmap,根据 sw_exit_code 选择处理分支。PSC 的验证函数还要求 vCPU 属于 SNP 客体,并确认 sw_scratch 等必需字段已经被客体声明有效。普通 SEV-ES 客体不会因为同样有 GHCB 就自动进入这条 PSC 漏洞路径。

验证通过只说明“这是一份可以开始解析的 PSC 请求”,没有说明数组有多长。旧分派在这个位置把 control->exit_info_2 当作 setup 的长度,令一个本不承担该含义的共享字段跨过了接口。参数类型都是整数,编译器看不见语义错误;函数签名也无法区分“协议提供的事实长度”和“调用者最低需要的字节数”。

若客体把这个值设为 24,setup_vmgexit_scratch() 会拿 24 参与地址末尾、页内范围与页外分配判断。对位于 GHCB 之外的 sw_scratch,旧代码允许继续,最终以该值调用 kvzalloc()。一项没有协议身份的数字因此拥有了 allocator 后果。

协议版本让错误更隐蔽。版本 1 的若干操作确实允许 helper 处理外部 scratch,因此开发者很容易把“helper 能做到”误当成“版本 2 的新操作也允许这样做”。新版本如果缩小合法地址集合,兼容代码必须在版本分派处明确收口;仅把最大版本常量从 1 改成 2,会把旧能力一起带进新契约。

主修复 db3f2195d293 正是在 setup 内加入版本门:协商版本达到 2,而 scratch 不在当前 GHCB Shared Buffer 时,函数立即失败。改动只有四行,作用却是把规范重新放到 allocator 前面。页外地址再也不能借旧兼容路径变成版本 2 的宿主堆对象。

检查放在 helper 内,是因为这里正是地址转化和页外分配的共同入口,也同时拥有协商版本与“地址是否在 GHCB 内”的结果。把规则压在能力真正发生的位置,可以覆盖未来调用者,避免另一个 v2 消费者重复犯错;各调用者仍负责声明自己的最低结构尺寸。

这一步关闭了制造 24 字节页外对象的主要入口,却还没有把所有消费者写成同一种安全模式。页内 scratch 可能从工作区中段开始,PSC 仍要知道实际条目数;共享头部还可以在检查后变化。上游维护者没有把四行主修复当作终点,而是沿接口继续清理长度和对象所有权。

3.2 页外兼容路径把一枚错误数字变成宿主对象

漏洞父提交里的 setup_vmgexit_scratch() 先读取保存的 sw_scratch 客体物理地址,并根据调用者给出的长度计算末端。若整个最低范围位于当前已映射 GHCB 的 Shared Buffer,helper 直接让 sev_es.ghcb_sa 指向页内字节,不需要新分配。

若地址在页外,旧 helper 检查长度没有超过十六页,然后调用 kvzalloc(len, GFP_KERNEL_ACCOUNT)。它用 kvm_read_guest() 从客体物理内存复制请求,把新对象地址保存为 ghcb_sa,把长度保存为 ghcb_sa_len,并设置以后需要同步、释放的标志。这是一条完整的输入对象生命周期,不只是临时指针转换。

kvzalloc() 会按尺寸和分配器策略选择合适内存。CNA 示例中的 24 字节请求可落进 32 字节级别的 accounted kmalloc cache,例如报告所示的 kmalloc-cg-32。分配器可能给对象一些对齐空间,但 C 代码的合法对象长度仍是 24;不能把 slab slot 的内部大小当作 PSC 可用数组长度。

处理完成时,sev_es_unmap_ghcb() 检查同步标志。若消费者修改了外部 scratch,它按保存的地址和 ghcb_sa_len 写回客体内存;若对象由 setup 分配,再执行 kvfree(),最后清空指针。异步 PSC 在 userspace 完成后还可能继续读取与更新同一个对象,因此任何错误索引都会穿过更长的时间窗口。

页内与页外两条路径看似都返回 ghcb_sa,实际拥有完全不同的边界来源。页内对象的末端由 GHCB Shared Buffer 固定,页外对象的末端由调用者传入长度决定。消费者若只接收一个裸指针而忽略 ghcb_sa_len,就会把两种对象抹平成“某处有一份 PSC”。

提交 ebe4b2dc9cfb 后来让 sev_es.ghcb_sa 成为 PSC 的权威对象,不再把另一个显式 buffer 参数一路传递。这样做不是单纯缩短函数签名,而是把准备阶段验证得到的地址、长度、同步与释放状态绑定在同一上下文里。对象身份只有一份,消费者更难绕开已经建立的事实。

提交 5867d7e202e0 又把 helper 的 len 改写为 min_len 语义。对于页内地址,ghcb_sa_len 是从 scratch 到 Shared Buffer 末尾的实际可用长度;对于仍被旧协议允许的页外兼容路径,只按消费者最低需要建立对象。调用者不再假装知道协议没有提供的总尺寸。

手绘工作台:前景只有头部与两个条目共三格的短托盘,上方却悬着对应整面大型格架的长尺;第四个虚线格越过托盘,落向相邻宿主分配对象。
图 3:24 字节对象只容纳八字节头部和两个八字节条目。253 属于完整 Shared Buffer 布局,不能成为这只短对象的索引上限。

到这里,托盘已经从客体地址变成宿主堆对象:它有明确起点、24 字节合法长度、同步责任和释放时刻。下一位接手者若读取 ghcb_sa_len,越界链会在这里止步;旧 snp_begin_psc() 却只看头部索引与一个全局常量。它拿到了托盘,却没有拿起旁边的卡尺。

这条链还说明为何不能只在 allocator 层寻找“危险的大分配”。实际对象很小,申请也通过了十六页上限;问题不在请求超过最大值,而在消费者访问超过请求本身。最大分配限制防止资源滥用,无法替代访问边界。

异步 userspace completion 还意味着函数返回不等于对象工作结束。KVM 把一批页状态变更交给 VMM,VMM 完成后内核继续推进条目,最终才统一清理。引用所有权、长度与 guest-writable 状态需要跨越这段异步边界保持一致,不能由调用栈上的临时假设维持。

公开记录中的 UAF 类报告正提醒我们:越界值可能影响后续控制流或对象状态,不同运行把错误暴露在不同生命周期位置。无需推断每份报告的确切 allocator 重用,已经可以确定修复不能只是“在第一次读取前多检查一次固定上限”。

4 门卫看见 10 小于 253,第三个条目已经踩空

snp_begin_psc() 是短托盘真正走出边缘的地方。它先确认 VMM 支持所需的 userspace hypercall exit,然后把 ghcb_sa 开头解释成 PSC 头部,读取当前索引与结束索引。旧实现拒绝 end_entry 达到 253,却没有根据 ghcb_sa_len 计算本次对象能放多少条目。

假设 cur_entry = 0end_entry = 10。十小于 253,旧门卫放行。循环先处理下标 0、1,它们确实位于 24 字节对象;当索引递增到 2,地址计算已经来到 offset 24,正好越过对象末端。此后每前进一步,代码都从另一个八字节位置取出一份并不存在于本对象的 psc_entry

消费者不会无条件把每八字节都写坏。它解析条目中的 GFN、operation、reserved 与进度位,检查请求是否有效,尝试把相邻范围合并成批次,再通过 KVM_HC_MAP_GPA_RANGE 交给 userspace。邻接 slab 字节被偶然解释成什么,会决定它在哪一步失败、返回什么状态,或者是否进入完成路径。

这正是信息判别出现的原因。若越界八字节不满足条目条件,客体能从响应看到一种结果;若它恰好非零并通过部分验证,又会看到另一种结果。反复 VMGEXIT 可以让对端观察不同位置的零/非零性质,公开 CNA 因而把宿主堆布局泄露列入影响。它是一种受协议解析约束的 oracle,不等于客体直接读取任意宿主地址。

当越界条目被认为有效并完成,__snp_complete_one_psc() 会更新条目的 cur_page。公开分析指出写回值受转换粒度约束为 1 或 512,写入的也是 packed 八字节条目中的特定位域。它足以破坏邻接宿主对象,却不应被描述成攻击者自由选择地址和值的任意写。

4.1 从头部索引走进相邻 slab

PSC 头部使用零基索引,end_entry 表示最后一项,并非字节长度。一个看似很小的 10 实际要求处理十一项。安全检查若只比较数字大小,很容易忽略它还要经过“加一”“乘元素大小”“加头部偏移”三步才能变成访问范围。对象容量修复直接以条目数量比较,避开了单位混乱。

修复后的计算先确认 ghcb_sa_len 至少覆盖头部,再执行 (ghcb_sa_len - sizeof(struct psc_hdr)) / sizeof(struct psc_entry)。24 得到 2,2,032 得到 253,指针在 Shared Buffer 中段则得到相应更小结果。随后若结束索引大于或等于容量,函数在第一次数组访问前拒绝。

减法顺序也有意义。若长度小于头部就直接用无符号数执行 len - 8,结果会下溢成巨大值;因此提交 2be54670bdc0 先让 setup 保证最小头部存在,容量计算才能安全进行。空间安全不是一条孤立 if,而是一组按顺序成立的前置条件。

旧代码中的 253 检查仍然有价值:它表达架构允许的最大 PSC 工作量,也限制批量处理。修复不是删除协议上限,而是让实际对象上限更早、更窄地约束访问。最终允许值相当于“协议最大容量”和“本次对象容量”两者中更小的那一个,在页内正确布局下二者自然一致。

条目循环还会把连续 GFN 与相同操作聚合,减少 userspace 往返。聚合意味着代码可能先读多个条目,再把范围作为一个 exit 交出去;若其中某项来自对象外,错误状态可跨越内核/userspace 边界保存。对象长度必须在任何预读和批处理前生效,不能等 completion 时才发现。

在 CNA 的 24 字节示例里,第一个越界访问恰好对齐在对象末尾,KASAN 很容易报告八字节 slab-out-of-bounds。现实 allocator 中对象可能位于更大的 slot,硬件不会为 C 对象边界自动 fault;没有 KASAN 的生产内核可能继续执行,直到邻接元数据被解释、写回或稍后使用,才以完全不同的栈崩溃。

因此“生产没有 KASAN 报告”不是安全证据。KASAN 是测试阶段把对象边界涂成红线的工具,生产处置要依赖版本和修复行为确认,并把 allocator corruption、general protection fault、KVM SVM 警告与无解释宿主重启作为相关线索。等待出现同一份实验室栈,会错过更隐蔽的表现。

同样不能把任何 kmalloc-cg-32 错误都归给该 CVE。32 字节 cache 服务大量内核对象,堆损坏还会在受害对象稍后使用时爆发。有效关联至少需要易受影响 KVM 版本、活跃 SNP 客体、相关 SEV/PSC 栈帧或八字节越界形态,再结合 vCPU 与迁移时间线。

手绘宿主分配器近景:蓝色对象边框只包住头部与两个条目共三块,红色边缘之后的第四块以虚线落入相邻 slab,小型进度印章只触及其中一个受约束字段。
图 4:offset 0–23 属于 24 字节对象;下标 2 从 offset 24 开始,已经进入邻接 slab。公开写回是受约束的 cur_page 位域更新,不是任意写。

4.2 73 份 KASAN 报告说了什么,也没有说什么

62 份 slab 越界报告最直接对应数组走出对象。KASAN 为分配周围设置 shadow 状态,当八字节读取或写入落到红区,就在当场记录访问栈、分配栈、对象 cache 和偏移。这些报告已经把 snp_begin_psc() 一类路径的静态风险变成可观察的对象外访问。

7 份 slab UAF 与 4 份一般 UAF 表明错误还能与对象释放、异步完成或邻接对象生命周期相遇。公开记录没有逐份给出可复现的 allocator 剧本,我们也不需要自行补写。可靠结论是同一根因可在不同运行里表现为越界和释放后使用,宿主完整性与可用性都应纳入风险。

布局泄露通过条目解析结果表现出来;KVM 并未把八字节原样返回客体。客体能区分某处被解释成全零、无效还是足以继续的结构,重复观察有助于推断宿主堆状态。任何这种跨客体/宿主的内存 oracle 都很严肃,但它的带宽和可控性取决于协议路径,不能等同于 read(host_address)

受约束写同样可能对内核对象造成实质破坏。内核元数据常由位标志、引用计数、长度和指针片段组成,小范围变化也可能改变后续控制流;同时,固定或半固定值并不保证攻击者能稳定选择有利对象。风险说明应保留两句话:公开证据支持宿主堆完整性受损;公开证据没有证明通用、稳定的任意代码执行。

宿主拒绝服务是最直接的运营后果。一个不受信 SNP 客体若反复让 L0 内核触发 allocator 检测、页故障或后续对象损坏,可以令整台物理节点重启,共驻虚拟机同时中断。高可用编排若把这些客体自动迁往另一台尚未修复的节点,还可能把故障沿调度池移动。

没有在野证据不应拖延补丁。hypervisor 漏洞通常难以从网络遥测直接发现,客体控制共享页又让宿主应用日志几乎看不到原始请求。风险处置应依据可达条件和源码状态,无需等待传统 IOC;调查团队则要提前确保 pstore、kdump、远程串口与调度历史在宿主崩溃后仍能保存。

相反,也不应把一台偶发重启直接宣布为利用。生产内核可能缺少 KASAN,堆损坏的最终栈又可能远离 KVM。严谨结论可以是“与该路径一致,尚缺直接归因证据”,随后保全 crash dump、核对 booted kernel、列出当时 SNP 客体与 vCPU,再判断是否需要按安全事件隔离。

对于安全负责人,最可执行的结论是把暴露节点归类为宿主内核内存安全风险,不能按客体应用漏洞处理。虚拟化平台、Linux 内核与调度控制面需要共同参与;让租户在客体里更新软件不会改变 L0 的 sev.c,也不会让已启动宿主自动进入新内核。

5 十个提交把工作台一件件装回去

主修复只有四行,却不是这次修理的全部。维护者随后沿着 GHCB scratch 的所有邻近消费者继续检查:哪些调用者可能传零长度,哪些操作在版本 2 下有更窄的尺寸,helper 应接收最低需求还是总长度,PSC 到底从哪一份指针取对象,条目上限怎样从有效余量计算,以及客体能否在宿主检查后改写共享字段。

最终形成十个连续提交。第一项对应 CVE 标题,后九项补齐相同接口周围暴露出来的契约缺口。把它们读成一组,比逐条背哈希更有价值:地址规则决定对象在哪里,最低长度决定最少能读什么,有效余量决定对象有多大,权威指针决定消费者读哪一份,容量决定数组走多远,快照决定检查和使用是否面对同一个值。

这组提交还解释了发行版回补为什么可能看起来“比 CVE 多”。vendor 可以把主修复与相邻 hardening 一起纳入一个内核更新,也可能为了稳定性分批落地。验收不能只看到 db3f2195d293 的等效四行就停止;至少要确认实际 PSC 容量和共享字段读取已经在该分支获得等效保护。

5.1 从位置门到有效长度尺

顺序提交装回的规则防止的错误继承
1db3f2195d293GHCB v2+ 的 scratch 必须在 Shared Buffer 内旧协议页外兼容路径进入新协议
21aa8a6dc7dac零长度 MMIO 直接忽略把“没有负载”送入 scratch setup
3dcf1b2d4b056v2+ MMIO 长度不得超过 8 字节邻近消费者借用过大 scratch
43988bd2723de零长度或零计数 Port I/O 直接忽略零尺寸参数穿过公共 helper
52be54670bdc0PSC 最低需求是 sizeof(psc_hdr)exit_info_2 编造成协议总长度
65867d7e202e0页内长度从 scratch 起点算到工作区末尾中段指针继承完整 2,032 字节
7f185e05dce6f调用者给出零最低长度时发出 WARN未来消费者绕开最小对象契约
8ebe4b2dc9cfbsev_es.ghcb_sa 成为权威 scratch准备对象与消费对象分叉
9121d88de56bcghcb_sa_len 计算实际条目容量短对象借用整页的 253 项
10c8cc238093caREAD_ONCE() 读取索引与条目客体在检查与使用之间换值
移动端可横向滑动核对十个提交、行为变化与它们关闭的错误继承

第一项先把版本规则放回地址入口。对 v2+ 请求,页外指针在任何分配和客体复制前失败;对规范仍允许的旧版本行为,helper 可以保留兼容路径。这样的条件收口避免为了修新协议而粗暴删除旧 ABI,也避免兼容性成为新协议的默认后门。

第二到第四项处理邻近 MMIO 与 Port I/O 调用者。长度或计数为零时,没有需要准备的 payload,直接忽略比向公共 helper 传零更清楚;版本 2 的 MMIO 又有不超过八字节的协议要求,调用者应在 setup 前落实。公共 helper 越少接收语义模糊的极值,内部状态越容易证明。

第五项是叙事转折:PSC 没有总长度,调用者只能声明至少需要八字节头部。这个改动把一个“远端提供的事实”还原成“本地消费者的需求”。setup 不再相信 exit_info_2 能描述对象,PSC 也不能靠改变该字段控制宿主分配尺寸。

第六项随后计算页内真实余量。旧代码用调用者长度验证一小段范围,却把同一个长度保存为对象长度;修复后,ghcb_sa_len 对页内对象覆盖从当前 scratch 到 Shared Buffer 末端的全部可用字节。消费者既不会读过页尾,也能合法使用最低头部之后的其余条目。

“最低需求”与“有效总长”分开后,接口语义变得可组合。MMIO 可以要求固定八字节,PSC 可以只要求头部,然后自己根据余量解析 flexible array;未来消费者也必须明确说出第一次访问需要多少。第七项 WARN 像门口的报警铃,确保新调用者不会用零绕过这份声明。

手绘修理车间分成上下两排各五个工位,十道不同的挡门、筛网、卡尺、警铃、托盘夹与快照夹由同一根红线串联,逐项修复共享服务窗。
图 5:十个连续提交不是十块互不相干的补丁;它们沿同一 scratch 生命周期依次修复位置、极值、最低长度、实际余量、权威对象、容量和读取稳定性。

5.2 对象只有一份,容量也只能从这一份得出

第八项 ebe4b2dc9cfb 看似只是删除一个 PSC 函数参数,实质是在确定“谁说了算”。准备阶段已经把经验证的地址、有效长度、同步和释放标志存进 svm->sev_es;消费者继续接收另一份显式 buffer,就可能在重构中使用与上下文不一致的对象。让 ghcb_sa 成为唯一来源,后续检查才有共同主语。

第九项终于把卡尺放到托盘上。121d88de56bc(ghcb_sa_len - sizeof(struct psc_hdr)) / sizeof(struct psc_entry) 计算 max_nr_entries,并要求 end_entry 严格落在这个数量以内。24 字节得到 2,完整页首得到 253,页中段按实际余量得到更小值。

这项检查要在读取任何数组条目前执行,并与 cur_entry <= end_entry 一类索引关系共同成立。单独证明结束索引在容量内,不能允许当前索引大于结束索引;单独证明两个索引顺序正确,又不能防止它们一起落在对象外。结构验证通常是多条不变量的交集。

第十项处理时间。GHCB 是客体与宿主共享的页,客体 vCPU 或恶意并发执行可以在宿主处理期间改变头部和条目。若代码在边界检查时读取一次 end_entry,循环条件里又重新读取,第二次值可能已经变大;若先验证 entry 后又从共享页重新取字段,内容也可能换掉。

READ_ONCE() 不会把整个请求永久冻结,也不是锁。它告诉编译器和 CPU 按一次明确读取取得当前值,代码把这个局部快照用于对应检查或处理步骤。对更复杂结构,先复制到私有内存再完整验证可能更强;在这段实现里,稳定读取索引和条目关闭了多次加载带来的 TOCTOU 窗口。

空间和时间必须同时受约束。即使容量公式完全正确,客体若能在检查后把 end_entry 从 1 改成 10,循环仍会越界;即使索引快照稳定,若容量仍固定为 253,快照只会稳定地使用一个错误上限。修复链把二者放到同一权威对象上。

这可以归纳成四道连续门。第一道问地址是否属于该协议允许的工作区;第二道问最小头部是否完整;第三道根据实际余量计算数组容量;第四道让校验和使用面对同一次共享值观察。任何一道的输出都是下一道的输入,不能跳过或用全局常量替代。

源码回补验收时,四道门比十个哈希更便于沟通。vendor 可能压缩提交、改名 helper 或用一次私有复制替代 READ_ONCE(),只要行为等价即可;若只有地址门,没有对象容量,仍需追问该分支为何不可能出现短页内对象。安全证明应说明条件,搜索命中无法替代行为解释。

手绘托盘沿导轨通过四个连续检查站:页内位置框、最低头部压尺、按当前托盘调节的容量卡尺、固定共享卡片的快照夹,随后才进入整齐的宿主对象柜。
图 6:修复后的四项继承关系——位置确定对象,最低长度允许读头,实际余量决定条目数,稳定快照保证检查的正是随后使用的值。

当最后一只快照夹扣上,24 字节托盘已经无法再出现在 v2 PSC 路径:页外地址先被拒绝;页内对象又由真实余量决定容量。源码分析至此转向运行环境。下一项工作不是在每台 AMD 服务器上机械搜索 253,而是找出哪些运行中的宿主真正允许不受信 SNP 客体走到这张工作台。

6 不是每台 AMD 主机都在门口,真正的队伍更短

资产系统里带 AMD CPU 的节点可能成百上千,真正暴露需要几项条件同时成立。宿主必须运行含易受影响 KVM SVM/SEV 代码的 booted kernel;平台必须启用并调度 SEV-SNP 客体;攻击主体要能控制该客体内核或等价 GHCB 请求;VMM/userspace 还要支持 PSC 交给 userspace 的映射 exit。少一项,公开路径就走不到同一位置。

这不是为漏洞降级,而是为了把有限维护窗口用在最危险组合上。多租户 confidential-computing 池通常排在最前,因为允许租户控制自己的受保护客体是产品正常功能,宿主又承载其他租户;只在实验室运行固定镜像 SNP 的隔离节点可以稍后处理,但仍应升级,因为客体内核一旦被攻陷便获得同样的协议控制面。

同样,CPU 报告支持 SEV-SNP 不代表固件、宿主命令行、RMP 初始化、KVM 模块与 VMM 都已开启。反过来,资产库没标 SNP 也不能直接放心:新固件、节点镜像或编排 flavor 可能已启用而 CMDB 尚未更新。硬件能力、运行状态和可调度产品能力必须三方核对。

6.1 从 booted kernel 一直查到迁移目的端

第一张表应按宿主列出完整运行内核包、构建标识、启动时间与 boot ID。包管理器显示新内核已经安装,不代表机器已重启;磁盘上的 vmlinuz 也不会改变当前内存里的 sev.c。验收证据必须来自正在运行的 image,并能回到 vendor 公告或源码 backport。

第二组字段描述平台能力:CPU 代际、固件版本、SEV/SNP 平台状态、kvm_amd 模块参数、内核命令行、QEMU 与 libvirt 版本,以及 VMM 是否启用相应 KVM exit。这里不需要收集租户私有内容,只要确认宿主是否具备并开放这条控制面。

第三组字段落到每台虚拟机。保存 domain XML、QMP 状态或编排规格中与 SEV-SNP guest object、policy、machine type 相关的配置,区分正在运行、已停止模板、自动扩容镜像与灾备副本。当前节点上没有 SNP VM,不代表下一次调度不会创建一台。

第四组字段是信任。哪些租户能上传自定义内核,哪些 appliance 由外部供应商维护,哪些管理 VM 有内核模块或低层虚拟化权限,哪些工作负载虽为公司自有却暴露高风险应用。客体是否“内部”不能用一个布尔值代替;攻击者在客体内取得 kernel control 后,宿主看到的就是恶意协议端。

第五组字段是所有可能的落点。live migration 白名单、HA failover 组、维护疏散目标、autoscaling 节点池、冷迁移模板与备用容量都要加入。同一 SNP 客体可以从已修节点迁往未修节点,也可以在滚动升级时被调度器送回刚排空但尚未重启的主机。

节点状态应至少分为“已验证修复并重启”“已安装未重启”“版本未知”“未修但已从 SNP 调度池隔离”“未修且仍可接收”。把所有未完成项统称 maintenance,会让调度器和安全团队对可用性有不同理解。机器可运行普通 VM,不等于可重新接收不受信 SNP 客体。

临时缓解可以从条件链中切断一环:停止向未修节点调度 SNP,迁走或关闭高风险客体,限制新的 confidential flavor 创建,收紧迁移目的端。是否能在不停机情况下禁用某能力取决于平台实现,不能假设修改一个模块参数会安全撤销正在运行客体的状态;优先使用编排层 placement 控制。

网络隔离对这条本地客体到宿主路径帮助有限。它可以降低攻击者先控制客体的机会,却不能修复已经被租户合法控制或已经失陷的客体。把补丁延期、只增加防火墙规则,会把风险从“直接可达”变成“需要前置客体控制”,而公开威胁模型本来就包含这项能力。

暴露清单最终应能回答一句很具体的话:在某个时间点,哪一台可控 SNP 客体可以落到哪一台运行什么内核的宿主上。若 CMDB、虚拟机清单与调度策略无法连接,CVE 处置同时暴露了资产治理问题;修复完成后,应把这种关系变成持续查询,不能停在一次性电子表格。

6.2 四条上游修复线只是起点,发行版包才是落点

Linux CNA 把受影响上游设计从 6.10 开始列出,并给出四条 fixed floors:6.12.y 的 6.12.93、6.18.y 的 6.18.35、7.0.y 的 7.0.12,以及 mainline 7.1。对应稳定提交分别为 bf9ba093fbb8c9b4198fbc6eb328ede59ac3,主线为 db3f2195d293

上游线首个官方修复版本对应修复对象运维含义
6.12.y6.12.93bf9ba093fbb83c0c9a3dedd50efec29424eca2fc低于该点需查 vendor 等效 backport
6.18.y6.18.35c9b4198fbc6ed99a9da4bee9f74bb730f926c9ae安装后仍必须确认已启动
7.0.y7.0.12b328ede59ac34e7998e1eee5e5f0cc26c2a91846迁移目标要使用同等或更强修复
mainline7.1db3f2195d29344a3cf1e9dd9ab7f21ced7308cf7作为源码行为与回补比较锚
移动端可横向滑动核对上游线、首个修复版本、提交对象与运维含义

fixed floor 不是字符串比较器。Ubuntu、RHEL、SLES 等发行版常在自己的 ABI 与包号上回补上游提交,uname -r 看起来较旧仍可能安全;自编译或供应商分支看起来更新,也可能丢掉相关修复。最强证据是官方产品状态加具体包版本,再由源码或 vendor changelog 证明行为等效。

截至 2026 年 7 月 17 日复核,Ubuntu 官方页面把该 CVE 标为 Medium,并对不同 kernel package 给出不同状态:26.04 generic 与若干 cloud 路线仍显示 vulnerable/WIP,24.04 的部分 6.17、7.0 HWE 路线也在处理;较旧 24.04、22.04、20.04 generic 基线显示 not affected。页面会随更新发布变化,正文不把这份快照当永久答案。

“旧 generic 不受影响”可能是因为其基线没有引入受影响 GHCB v2 设计;这不说明旧内核普遍更安全。相同发行版版本里的 HWE 或 cloud kernel 可以来自更新基线,状态因而不同。资产清单必须记录包 flavor,不能只写 Ubuntu 24.04。

Red Hat 安全数据入口在本次复核时给出 Important、CVSS v3 7.0、CWE-787,并按产品线区分包状态;SUSE 也提供永久 CVE 查询页。动态产品表可能在文章发布后变化,因此本文把链接留在“来源与材料”,不复制一张很快过期的完整 SKU 表。

安装修复包后必须重启 L0。live patch 是否覆盖这条复杂 KVM 状态路径,需要发行商明确支持,不能自行假设;普通包更新不会替换正在运行内核。记录重启前后 boot ID、内核构建和节点重新入池时间,能防止一台“已安装”主机提前接回 SNP 负载。

升级客体内核不修复 L0 的 arch/x86/kvm/svm/sev.c。客体更新可以降低攻击者获得 kernel control 的概率,也可能修复其自身 GHCB 实现,但恶意客体模型不依赖善意更新。宿主平台必须独立完成修复。

固件升级也不能替代该 Linux 代码修复,除非 vendor 明确说明其产品通过禁用相关路径等方式消除可达性。固件、QEMU 和内核可在同一维护窗口更新,但验收记录要区分哪一项关闭了 CVE,便于以后回滚或故障分析。

7 出现宿主异常时,把崩溃证据与调度历史拼在一起

如果暴露节点出现无法解释的重启、panic 或内核堆异常,第一件事不是在生产客体里重放畸形 PSC,也不是立刻清空所有磁盘,而是保全仍会消失的证据:pstore、kdump、远程串口、BMC 事件、宿主 journal、QEMU/libvirt 日志、编排调度与迁移记录。任何再次启动、自动疏散或节点重建都可能改变现场关系。

团队先确认当时运行的 boot ID 与内核包,判断是否低于 vendor 修复。随后列出崩溃时间窗内的 SNP 客体、vCPU 运行节点和迁入迁出事件。若节点根本没有可达 SNP 客体,或运行分支已含四道等效保护,CVE 相关性会显著下降;若二者都吻合,内核堆错误便值得按安全事件继续调查。

实验室 KASAN 的理想栈可能包含 setup_vmgexit_scratchsnp_begin_psc__snp_complete_one_pscsev_handle_vmgexit 等函数,并显示八字节访问越过很小对象。生产构建通常没有 KASAN,最终 fault 可能落在 allocator、完全无关的受害对象或稍后的 cleanup。调查要追因果邻近,不要只做固定字符串匹配。

原始 VMGEXIT 数量也不是好 IOC。受保护客体正常工作就会频繁通过 GHCB 请求宿主服务,负载变化能自然推高计数;没有包含 exit 类型、返回结果、vCPU 和基线的上下文,高频只会制造噪声。更有价值的是版本、特定 PSC 路径、内存损坏形态与客体时间线组合。

7.1 一份 crash dump 不能独自定罪,五类证据可以互相校正

第一类是代码证据。保存运行内核包、符号、配置和 vendor build,确认实际二进制是否包含 v2 页内要求、最低头部、有效余量、实际容量与稳定读取。若只凭版本字符串判断,后续法证结论会建立在最脆弱的前提上。

第二类是内存安全证据。KASAN 环境记录访问大小、偏移、对象 cache、分配与释放栈;生产 crash dump 则可检查 fault address、slab 元数据、污染标记和相邻对象。24 字节请求配合八字节越界最贴近公开示例,但攻击路径不要求每次都保留同样的 allocator 外观。

第三类是虚拟化证据。确认出事 vCPU 是否属于 SNP 客体、VMM 是否支持对应 hypercall exit、日志是否在时间上出现 SEV/PSC 处理或 KVM SVM 警告。普通 VM 与 SNP VM 共驻时,不能因为普通 VM 最先报告掉线,就把它误当成触发源。

第四类是身份和控制证据。谁管理该客体,是否允许自定义内核,客体此前是否有入侵告警、镜像来源是否可信、管理凭据是否被滥用。缺少客体内部证据不等于恶意,因为机密计算本来就限制宿主可见性;平台应以最小取证原则与租户、法务和事件响应流程协调。

第五类是调度时间线。客体可能在崩溃前几分钟刚迁入,也可能在另一节点上出现过相似异常;HA 可能在节点重启后立即把它送往下一台未修宿主。保存 placement decision、migration ID、源端与目的端、vCPU 运行区间,能把一台机器的 panic 还原成整个池的事件。

这五类证据不一定都完整。严谨报告可以分成“已确认”“高度一致”“未确认”和“排除”四栏:已确认运行易受影响代码;高度一致的八字节越界栈;尚未确认客体是否恶意;排除磁盘硬件故障。保留未知比用 CVE 标题填满空白更有价值。

若 crash dump 显示邻接对象已被破坏,节点应在保全后重建或至少经过更严格完整性处置;只重启并继续服务并不足够。内核堆损坏可能留下未被立即发现的状态,单次 reboot 会清除内存但不能回答攻击者是否通过其他路径建立持久性;处置深度取决于证据和环境价值。

若只有版本暴露,没有任何异常,流程可以保持为预防性漏洞修复,不必制造“疑似入侵”标签。相反,若出现与路径一致的宿主内存损坏,即便没有传统 IOC,也应提高事件等级并隔离相关客体与节点。版本管理和事件响应在同一 CVE 下可以有不同结论。

手绘取证桌:崩溃转储卷盘、三组不同规模的内存安全报告、放大镜下的八字节越界、受保护客体到 vCPU、宿主与迁移目标的时间线,以及封存的版本账本共同摆在桌面。
图 7:没有一枚单独 IOC 能证明 CVE-2026-53360。代码状态、内存形态、SNP 路径、客体控制与调度时间线需要互相校正。

7.2 先把客体留在已验证池,再滚动宿主

处置从 placement 开始。把所有版本未知、仅安装未重启和已知未修节点从 SNP 目的池移出,阻止新建、迁移和故障切换落上去。若容量不足,优先为高风险不受信租户保留已修节点,其余受控工作负载按业务恢复计划暂停或降级,不要让调度器暗中突破安全隔离。

  1. 冻结错误落点。 从 SNP 调度、迁移、HA 与 autoscaling 目标中撤下未知和未修节点。
  2. 保全异常证据。 保存 crash dump、pstore、BMC、VMM 与调度历史,记录相关客体和 vCPU。
  3. 安装并启动修复 L0。 使用 vendor 固定包,重启,核对 booted build 与 boot ID。
  4. 验证四项源码行为。 检查 v2 地址、最低头部、实际容量和稳定快照的等效实现。
  5. 验证正常 SNP 服务。 覆盖证明、PSC、I/O、暂停恢复与受控迁移,不在生产运行破坏性 PoC。
  6. 按证据重新开放。 只有节点、目的端和备用容量全部合格,才恢复 placement 与自动故障切换。

回滚计划也要服从同一规则。若新内核出现功能故障,不能简单回到易受影响版本并保持 SNP 调度开放;可选择暂时关闭 confidential flavor、把客体留在其他已修节点,或使用 vendor 提供的另一固定构建。可用性回滚不应悄悄撤销安全前提。

集群容量会影响窗口长度。提前统计每个池的可迁移余量、巨大页与 NUMA 约束、证明服务依赖和停机预算,可以避免升级中途因资源不足把未修节点重新拉回。安全修复计划若不包含调度容量,往往在最后一公里失效。

对无法立即升级的遗留节点,最强临时措施是停止承载不受信 SNP 客体并从所有自动目的端删除。仅在客体里安装监控、限制网络或要求租户签署承诺,都没有改变宿主解析恶意 GHCB 的能力。缓解需要切断技术路径,责任文字不构成技术控制。

完成滚动后,重新跑一遍从客体到所有目的端的关系查询。某台灾备节点、关机备用机或新加入 autoscaling group 的镜像最容易漏掉;它们平时不承载业务,却会在故障时成为唯一落点。关闭 CVE 的范围是未来可达池,不只是今天在线列表。

手绘集群恢复工作台:蓝色已验证宿主组成调度池,红色未验证节点被隔离;工程师依次安装内核、拨动重启杆、检查新启动封印、测试受保护客体,再把节点和迁移路线放回池中。
图 8:修复不是“包已下载”。未修节点先退出 SNP placement,L0 重启并通过正常功能与安全负例后,才恢复迁移、HA 和扩容资格。

当最后一台备用节点完成验证,团队不必假装已经证明一次利用,也不必因为缺少传统 IOC 而什么都不做;它可以同时写下两项清楚结论:生产路径已经通过运行修复关闭,既有宿主异常是否源于恶意利用仍按证据等级保留。

对租户沟通也应保持同样精度。若只有预防性维护,可以说明宿主内核升级与受保护 VM 迁移窗口;若存在与内存安全路径一致的异常,再按合同和事件流程披露受影响时间、证据范围和仍未知部分。泛称“SEV 被攻破”只会制造错误预期。

最终变更记录应附 vendor advisory、包哈希或构建、boot ID、四项源码验收、功能测试结果、节点退入池时间和所有迁移目的端清单。下一次镜像重建或内核 rebase 时,这些材料能成为回归基线,防止修复在定制补丁队列中丢失。

如果平台以 golden image 扩容,还要更新镜像与节点引导流水线。仅修现有实例会让下一台自动创建的宿主重新带入旧代码;镜像版本、签名与 admission policy 应阻止低于固定构建的新节点加入 SNP 池。

若使用多个区域或云边缘站点,修复状态需要在全局调度视图聚合。一个区域容量紧张时,跨区灾备可能选择平时不参与主池的目标;安全资格标签应跟随镜像和节点证明,人工维护的区域名单很容易在故障前过期。

8 服务窗重新打开时,253 不再遮住眼前的托盘

修复后的 KVM 再遇到同样的 PSC 条件时,路径会在更早处改变。若 GHCB v2 的 sw_scratch 指向页外,setup 在 allocator 之前拒绝;若地址位于页内,helper 只要求头部完整,并从实际指针量到 Shared Buffer 末端;PSC 用保存的 ghcb_sa_len 得出条目数,再以稳定读取的索引处理。

24 字节不再是一张由客体随意填写的“协议长度”。对版本 2 的合法 PSC,它甚至不该形成页外宿主对象;在任何仍允许短对象的兼容或测试语境里,容量公式也只会给出两项。墙上的 253 仍然存在,但它只在完整工作区从起点开始时生效。

这就是一条好的修复链应有的感觉:不是在每个危险下标前塞一个神秘常量,而是让每个阶段继承上一个阶段已经证明的事实。版本选择地址规则,地址建立对象余量,对象余量建立数组容量,快照把检查结果带进使用阶段。因果顺序比补丁行数更能抵抗下一次重构。

8.1 回归矩阵要证明接受什么,也要证明拒绝什么

第一条用例是版本 2 页外指针。测试在隔离 SNP 节点构造规范上不允许的 scratch 地址,期望 KVM 返回失败,不触发外部 kvzalloc(),也不把客体字节写回。它直接验证主 CVE 修复,验收标准是明确拒绝,不能只看“系统没崩”。

第二条把 scratch 放在 Shared Buffer 起点,只提供八字节头部和最小合法索引,确认正常通过。第三条逐步增加到完整页最大合法条目,再测试第一次越过容量的结束索引。两条组合证明计算没有 off-by-one,也没有把条目数量与最后索引混淆。

第四条把 scratch 起点向工作区末端移动。相同 end_entry 在前部可能合法,在后部必须失败;这能抓住仍把完整 2,032 字节保存为 ghcb_sa_len 的不完整回补。移动一格、恰好只剩头部、比头部少一字节,都值得成为边界用例。

第五条覆盖零长度与邻近消费者。零长度 MMIO、零长度或零计数 Port I/O 应按新语义忽略,v2+ 的异常 MMIO 大小应被拒绝,公共 helper 不收到没有最低需求的调用。它确认九项 hardening 没有在 backport 选择时掉队。

第六条制造共享字段变化,但目标不是竞速破坏生产宿主。测试客体在受控同步点修改 cur_entryend_entry 与条目内容,观察宿主是否以一次稳定值完成当前检查和批次。源码里的 READ_ONCE() 或更强私有复制应能解释结果。

第七条覆盖 userspace completion。合法 PSC 被分成多个相邻范围,VMM 返回成功、部分失败与错误,KVM 更新进度并最终清理;每次继续都使用同一权威 scratch 对象和有效长度。安全修复不能只保护第一批,随后 continuation 又绕回裸指针。

第八条验证正常产品能力:SNP 客体启动与证明、私有/共享页转换、I/O、压力运行、暂停恢复、live migration 和关机。负例全部拒绝而正常客体无法运行,不是可交付修复。功能结果应与内核版本和 VMM 构建一起归档。

回归对象应接受应拒绝或阻断证明的不变量
地址v2 Shared Buffer 内v2 页外 scratch协议版本决定合法位置
最低尺寸完整 psc_hdr短于八字节首次解引用前头部存在
容量最后索引小于实际条目数第一次达到或越过容量数组服从本次对象
页内偏移按剩余空间缩小批次中段指针借用完整 2,032长度来自起点到末尾
共享时间稳定快照的一批请求检查后换值扩大范围校验值就是使用值
运营已重启、已验证节点仅安装或版本未知节点入池运行证据决定 placement
移动端可横向滑动核对地址、尺寸、容量、时间与运营五层回归

这些测试在隔离环境完成,不需要在生产节点复现公开内存破坏。可以用 instrumented guest、KASAN/KFENCE staging kernel、VMM test harness 与 tracepoint 观察拒绝位置;生产只执行正常功能验证和非破坏性配置检查。安全验证的目标是证明修复,不是再次制造事故。

若 vendor 不提供源码,行为测试仍可给出重要证据,但应避免把不可见部分说成完全证明。记录测试覆盖、返回结果、内核包和支持工单,必要时要求供应商确认 continuation 与共享字段处理。审计结论可以有置信度,不必在“完全安全”和“毫无信息”之间二选一。

8.2 从一只短托盘留下的六条工程纪律

第一条:版本是解析规则,不是展示字段。协商到 GHCB v2 后,scratch 的合法位置发生变化,旧版本兼容能力必须被明确关在旧分支里。任何协议升级都应列出新增、删除和收紧的语义,而不只是扩大 max_version

第二条:最低需求与实际长度不是同一个参数。调用者可以说“我至少要读八字节头部”,实际对象有多大应由验证后的映射或分配得出。把远端声明、消费需求和本地事实塞进一个 len,会让类型相同的整数在函数间悄悄换身份。

第三条:数组上限属于具体对象。253 是 GHCB Shared Buffer 的页面布局上限,不是每个 psc_entry * 指针的属性;协议最大包、设备 ring 最大 descriptor、文件格式最大记录数也都一样。先确定本次 backing object,再谈元素数量。

第四条:对象身份只能有一个权威来源。地址、长度、同步方式和释放责任若分别通过多个参数传递,重构后很容易组合成从未验证过的混合对象。把这些事实绑定到一个上下文,并让消费者从那里取用,能减少“正确检查了 A,却使用 B”的缺陷。

第五条:共享内存既是不可信空间,也是不稳定时间。范围检查通过以后,对端仍可能改字段;用一次读取、私有副本、锁或协议状态机让校验值与使用值一致。空间安全公式若建立在变化中的索引上,只是瞬间正确。

第六条:修复完成于运行和调度,不完成于仓库。内核提交进入 vendor 包、包安装到节点、节点重启进入新 build、正常 SNP 服务通过、迁移与备用目的端全部合格,才构成生产关闭。任何中间状态都应该在控制面留下不可接收标签。

它们并不只属于机密计算。virtio descriptor、vhost ring、GPU command buffer、固件 mailbox、网络包的可变尾部、io_uring 共享队列,都可能出现“全局最大值覆盖短对象”和“检查后对端换值”。CVE-2026-53360 的具体函数会老去,这套问题仍会反复出现。

修复没有把 24 字节对象做大,也没有把完整布局的 253 项上限改小。它只是把每个数字放回自己的主语:一页是 4,096 字节,Shared Buffer 是 2,032 字节,头部和条目各八字节,完整布局最多 253 项,24 字节对象只有两项;任何循环都只能走到眼前对象允许的位置。

如果未来某次重构再次把 PSC buffer 作为裸指针传下去,回归矩阵应立刻变红;如果新协议版本重新允许另一种 scratch 位置,设计评审必须给出新的尺寸来源;如果扩容镜像落后,admission policy 必须拒绝。好的漏洞复盘不是记住一次函数名,而是把同类错误重新变得难以发生。

CVE-2026-53360 最终揭开的不是一项神秘的机密计算破法,而是一个非常朴素的事实:接收方不能让远端替自己发明对象长度,也不能让最大的房间替眼前的托盘担保。只要这条事实沿地址、容量、时间和运营一路不断,服务窗可以继续打开,客体仍然不需要被信任。

研究记录

9证据、对象与来源

下面保留本文实际使用的标识、时间和原始材料,便于继续调查。

9.1研究对象

报告涉及的产品、行为者、技术、受影响对象和控制点。

CVECVE-2026-53360

Linux KVM SEV-SNP GHCB scratch 宿主堆越界

组件arch/x86/kvm/svm/sev.c

受影响的 KVM AMD SEV VMGEXIT 处理文件

函数setup_vmgexit_scratch

错误接受 GHCB v2 页外 scratch 并建立宿主对象的准备函数

函数snp_begin_psc

按页级上限消费短 scratch 对象的 PSC 起始函数

修复db3f2195d29344a3cf1e9dd9ab7f21ced7308cf7

GHCB v2+ 强制页内 scratch 的主线修复

修复121d88de56bc5c0ba0ce2f6381af67f948a7e7c1

按实际 scratch 长度计算 PSC 条目容量

修复c8cc238093ca6c99267032f6cfe78f59389f3157

稳定读取客体可改 PSC 索引与条目

布局2032-byte GHCB shared buffer

GHCB 页内可变负载工作区

容量(ghcb_sa_len - 8) / 8

PSC 容量必须来自本次实际对象

9.2事件时间

  1. GHCB v2 进入每客体配置

    4af663c2f64a 允许 KVM 为客体配置 GHCB 协议版本 2,Linux CNA 将受影响上游范围映射到 6.10。

  2. 主线修复完成

    db3f2195d293 要求版本 2 及以上的 scratch 位于 GHCB Shared Buffer,随后相邻提交补齐长度、容量和快照语义。

  3. Linux CNA 发布记录

    CVE-2026-53360 公开宿主堆破坏、布局泄露、73 份 KASAN 报告和四条官方修复版本线。

  4. SOSEC 完成源码重建

    SOSEC 核对 CNA、规范、漏洞父提交、十提交修复、稳定分支与发行商状态。

9.3来源与材料

  1. CVE.org:CVE-2026-53360 官方记录https://www.cve.org/CVERecord?id=CVE-2026-53360
  2. CVEProject:Linux CNA 原始 JSONhttps://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/53xxx/CVE-2026-53360.json
  3. AMD:GHCB 规范 56421 Rev. 2.04https://docs.amd.com/api/khub/documents/oJly8EPzLO1Bt7ncrkCytw/content
  4. Linux 文档:KVM AMD memory encryptionhttps://docs.kernel.org/virt/kvm/x86/amd-memory-encryption.html
  5. Linux 源码:GHCB 页与 2,032 字节 Shared Buffer 布局https://codebrowser.dev/linux/linux/arch/x86/include/asm/svm.h.html
  6. Linux 源码:PSC 头部、条目与数量常量https://codebrowser.dev/linux/linux/arch/x86/include/asm/sev-common.h.html
  7. Linux 源码:当前 KVM SEV VMGEXIT 实现https://codebrowser.dev/linux/linux/arch/x86/kvm/svm/sev.c.html
  8. Linux:每客体 GHCB 协议版本配置 4af663c2f64ahttps://git.kernel.org/stable/c/4af663c2f64a8d252e690c60cf8b8abf22dc2951
  9. Linux:Page State Change VMGEXIT 支持 9b54e248d264https://git.kernel.org/stable/c/9b54e248d2644be71cb394eb85f31ad99e023a05
  10. Linux:GHCB v2+ 页内 scratch 主修复 db3f2195d293https://git.kernel.org/stable/c/db3f2195d29344a3cf1e9dd9ab7f21ced7308cf7
  11. Linux:零长度 MMIO 加固 1aa8a6dc7dachttps://git.kernel.org/stable/c/1aa8a6dc7dac
  12. Linux:GHCB v2+ MMIO 长度上限 dcf1b2d4b056https://git.kernel.org/stable/c/dcf1b2d4b056
  13. Linux:零长度或计数 Port I/O 加固 3988bd2723dehttps://git.kernel.org/stable/c/3988bd2723de
  14. Linux:PSC 最低头部长度修复 2be54670bdc0https://git.kernel.org/stable/c/2be54670bdc0
  15. Linux:页内 scratch 有效余量计算 5867d7e202e0https://git.kernel.org/stable/c/5867d7e202e0
  16. Linux:零最低长度调用警告 f185e05dce6fhttps://git.kernel.org/stable/c/f185e05dce6f
  17. Linux:统一权威 scratch 对象 ebe4b2dc9cfbhttps://git.kernel.org/stable/c/ebe4b2dc9cfb
  18. Linux:PSC 实际容量修复 121d88de56bchttps://git.kernel.org/stable/c/121d88de56bc5c0ba0ce2f6381af67f948a7e7c1
  19. Linux:PSC 共享字段快照加固 c8cc238093cahttps://git.kernel.org/stable/c/c8cc238093ca6c99267032f6cfe78f59389f3157
  20. Linux 6.12.y 稳定修复 bf9ba093fbb8https://git.kernel.org/stable/c/bf9ba093fbb83c0c9a3dedd50efec29424eca2fc
  21. Linux 6.18.y 稳定修复 c9b4198fbc6ehttps://git.kernel.org/stable/c/c9b4198fbc6ed99a9da4bee9f74bb730f926c9ae
  22. Linux 7.0.y 稳定修复 b328ede59ac3https://git.kernel.org/stable/c/b328ede59ac34e7998e1eee5e5f0cc26c2a91846
  23. Ubuntu:CVE-2026-53360 包状态https://ubuntu.com/security/CVE-2026-53360
  24. Red Hat:CVE-2026-53360 产品状态入口https://access.redhat.com/security/cve/CVE-2026-53360
  25. SUSE:CVE-2026-53360 产品状态入口https://www.suse.com/security/cve/CVE-2026-53360.html