今年在家总算有时间折腾些新东西
INTEL 为 TDX 出了三份文档
TDX Whitepaper
intel TDX module spec 1.0 (1.5)
TDX Guest-Hypervisor Communication Interface (GCHI)
年前开始研究 TDX module & KVM 实现也,陆续看到一些文章,大多是依据 intel 文档自上而下的介绍,没有太多我关心的细节问题。为了弄清我所关心的细节问题,才有了附件中的 PPT。
我一直是野路子,对新东西总是能调试就先调试,调试不了的有源码就不会先看文档,总是想先撕开个口子,看这一杆子能捅多深,在这个过程中我特别关注标志,不同标志代表不同路径,不同路径则又是一片天地,不同的标志(参数)组合构建出不同上下文,不同的输入输出代表不同的场景,要把所有分支走到又能把圈画园,还能有个合理的解释,这是个体力活儿。
当然这样肯定有它的弊端,就是对全局的认识会有个过程,甚至会走不少弯路(对这种稍微新一点的技术 GPT o4 与 deepseek 表现很不好,多次把我往沟里代,还好我对其结果有一种天然的不信任感),但没办法形成习惯了,要么说是野路子呢。在这个意义上讲,我一直觉得分析源码的意义在于对场景的理解,尤其在一些高并发,异步调用,会产生条件竞争的场景下,这段代码为什么出现在这里,解决什么问题?意义是什么?像我对 secure EPT pagewalk 的分析,不同调用者输入不同参数,返回不同状态,其返回成功并不代表当前场景的成功,比如请求 2M 的 SEPT page ,SEPT pagewalk 成功返回但下面已经有了一个已被 TD Guest accept 的 4K page,要理解这种场景,做流程分析很容易忽略过去,而不同环境下的 SEPT pagewalk 都是同一个函数,这明显是经过上层设计的,代码的位置前后错一行都不行,所以本着 “大胆假设,小心求证” 的方法尽量的去还原场景,此时再参考 spec 文档才能对整体有个概念
文档分析了 TDX module + KVM runtime 动态内存转换和加密的几个调用
1、TD Guest TDVMCALL(MapGPA)
2、TDX module - TDH.MEM.SEPT.ADD、TDH.MEM.PAGE.AUG
3、TD Guest TDG.MEM.PAGE.ACCEPT
比如为了深入理解 inte TDX module spec 与 intel GHCI 中相关图片和 TDX module 代码中的注释,PPT 中按照以上 1、2、3 逐步进行展开。
我这里节选了 TDG.MEM.PAGE.ACCEPT 的 TD Guest -> TDX module ,因其比较有代表性,像其中的 lock_sept_check_and_walk_internal、
secure_ept_walk 在 Host VMM (KVM) 发起的 TDH_MEM.SEPT.ADD / TDH.MEM.PAGE.AUG 时也会调用到,只不过传入的参数和标志不同,
所以我把详细分析都放到了最后。下面的分析先从调用流程开始,然后是一些细节,然后构建场景带入上下文分别讨论,这样可以比较精准的解读
TDX module 中代码的注释以及 intel TDX module spec 、TDX GCHI 文档中的内容
【TD Guest】
tdx_accept_memroy->
try_accpet_one(PAGE_LEVEL_1G/2M/4K)->
__tdcall(TDG_MEM_PAGE_ACCETP)
tdx_accept_memory:
TD Guest 调用该函数来接受内存页。
try_accept_one(PAGE_LEVEL_1G/2M/4K):
根据内存页的大小(1G/2M/4K),尝试接受一个内存页。
__tdcall(TDG_MEM_PAGE_ACCEPT):
通过 __tdcall 发起一个 TDX 调用,进入 TDX Module 执行 TDG_MEM_PAGE_ACCEPT 操作。
【TDX module】
td_call->tdg_mem_page_accept->
walk_private_gpa(&sept_entry, &ept_level, &sept_entry_copy)->
lock_sept_check_and_walk_internal->
secure_ept_walk
is_secure_ept_leaf_entry() => is_leaf
check_tdaccept_failure(walk_failed,is_leaf)=> fail_type
fail_type == TDACCEPT_VIOLATION、TDACCEPT_ALREADY_ACCEPTED、TDACCEPT_SIZE_MISMATCH
async_tdexit_ept_violation(VMX_EEQ_ACCEPT)
init_sept_4k_page (req_accept_level == LVL_PT)->
map_pa_with_hkid
zero_area_cacheline
free_la
init_sept_4k_page (req_accept_level != LVL_PT) && (!interrupt_pending)
(!interrupt_pending)
sept_entry_copy.raw |= SEPT_PERMISSIONS_RWX
sept_update_state(!is_sept_pending) SEPT_STATE_MAPPED_MASK || SEPT_STATE_EXP_DIRTY_MASK
atomic_mem_write_64b(&sept_entry_ptr->raw, sept_entry_copy.raw);
td_call->tdg_mem_page_accept(RCX):
TDX Module 接收到 Guest OS 的调用请求,进入 tdg_mem_page_accept 函数。
walk_private_gpa(&sept_entry, &ept_level, &sept_entry_copy):
遍历 private GPA 对应的 SEPT(Secure EPT)entry,获取 SEPT entry、EPT level 和 SEPT entry 的副本。
is_secure_ept_leaf_entry() => is_leaf:
检查当前 SEPT entry 是否为最终层级 leaf entry,并将结果存储在 is_leaf 标志中。
check_tdaccept_failure(is_leaf) => fail_type:
fail_type == TDACCEPT_VIOLATION 等情况
init_sept_4k_page (req_accept_level == LVL_PT)
map_pa_with_hkid
zero_area_cacheline
free_la
作用是初始化(清零)一个 4KB 的 SEPT(Secure EPT)page;
具体步骤:
1、获取 SEPT entry 的 HPA (sept_entry.raw & IA32E_PAGING_STRUCT_ADDR_MASK)
2、将 SEPT entry 的 HAP 映射到 LA (VA),再次强调这里指的是 TDX module 中的 LA(VA),因为其拥有自己的地址空间
3、将页面清零,确保页面内容不会被之前的残留数据污染。(zero_area_cacheline)
4、清零完成,此映射没必要存在了,解除映射(free_la)
关于 TDX module 中的 LA(VA) 与 HPA->GPA 的关系,参考前面对 TDH_MEM_SEPT_ADD 的分析。
init_sept_4k_page (req_accept_level != LVL_PT)
如果请求的是非 4K page,那么按照 2M page 进行处理。这重复调用 init_sept_4k_page,直至满足 sept_entry_copy.accept_counter == NUM_OF_4K_PAGES_IN_2MB,为止。这个很好理解所谓 2M 的 SEPT page 就是由多个 4K SEPT page 描述的,所以要一个个的进行初始化,可以很清晰的看到 accept_counter 字段就是为 2M page准备的,每初始化一个 4K page 就把 accept_counter + 1,一直到初始化 512 * 4K 为止。
这里考虑一种情况:当 TD Guest 有 pending 的中断要响应时会提前终止这个流程,比如初始化到第 256 个 4K page 时收到 pending interrupt 会直接退
出,当前的 sept_entry_copy.accept_counter 的值为 256,剩余的 256 个 4K page 因没有初始化处于不可用的状态。这里需要注意的是此时的返回值为
TDX_SUCCESS ,但这并不代表此 2M page 可以被 TD Guest 使用;也就是说只有 512 个 4K page 都初始化完成 SEPT entry 才可以被 TD Guest
使用(具体细节我下面会讲到)。
当前上下文中又引出了一个新的问题,那就是返回的是 TDX_SUCCESS,但 2M SEPT page 只初始化了一半,那当再次访问时会出现什么样的情况,该如何处理呢?如果这 2M在某一时刻已被 TD Guest 使用了(accept),又会是怎样的情况?这里分 5 种场景讨论
(1):TD Guest 申请 2M page 相同地址(即:被中断的 2M page),处于 pending 状态
(2):TD Guest 申请 2M page 相同地址(即:被中断的 2M page),处于 accept 状态
(3):TD Guest 申请 4K page 地址,落在了之前被中断的 2M page 范围内,2M 处于 pending 状态
(4):TD Guest 申请 4K page 地址,落在了之前被中断的 2M page 范围内,2M 处于 accept 状态
(5):TD Guest 申请 2M page 任意地址,返回 4k page(非 leaf entry 情况),2M 处于非 free 状态
要回答以上 5 个问题,我采取的方法是从 check_tdaccept_failure 函数反推至 secure_ept_walk,跟踪相关标志了解处理细节,然后再回到 check_tdaccept_failure 把所有注释中提到的情况根据处理细节总结出不同场景,然后再根据场景结合 TDX module spec 对这些场景进行验证。
check_tdaccept_failure
walk_failed == TRUE
sept_state_is_guest_accessible_leaf =>(TDACCEPT_ALREADY_ACCEPTED or TDACCEPT_VIOLATION)
SEPT_CONVERT_TO_ENCODING
sept_special_flags_lookup[idx].guest_accessible_leaf
walk__failed == FALSE
TDACCEPT_SIZE_MISMATCH
TDACCEPT_ALREADY_ACCEPTED
TDACCEPT_VIOLATION
这里 check 的是什么呢?那就是之前 walk_private_gpa->lock_sept_check_and_walk_internal->secure_ept_walk 返回的结果,因 secure_ept_walk 并没有对 sept entry 做任何有效性验证,它判断错误的逻辑是根据 SEPT leaf entry 与 request EPT level 和 current EPT level 进行比较得来的(这个结论是根据后面对 lock_sept_check_and_walk_internal->secure_ept_walk 输入不同参数对比并结合 TDH.MEM.SEPT.ADD / TDH.MEM.PAGE.AUG 等调用环境分析得到的,这里可以先把它当结论来看);要想根本性解读(1)(2)(3)(4),需要先将 secure_ept_walk 细节讲明白,这里分别举4个例子说明:
secure_ept_walk 的源码在 src/common/memory_handlers/sept_manager.c 可对照参考
secure_ept_walk 的第一个参数是 EPTP 是在 lock_sept_check_and_walk_internal 中通过 tdcs_p->executions_ctl_fields.eptp 获取到的,根据 GPAW 要么是 PML5 要么是 PML4,也就是至少会循环 5 次或者 4 次,且 EPT level 一定大于请求的 request EPT level,因为 TDX module 1.5 只支持 2M 或 4K 的 SEPT 。下面例子假定跳过 PMLx 从 EPT PDP 开始 ,且不考虑 EPT misconfigured 情况。
在 secure_ept_walk 代码中传入的参数 l2_sept_guest_side_walk 为 FALSE ,且每次循环获得 SEPT entry 后都有如下操作:
cached_sept_entry->raw = pte->raw; // Atomic copy
例1:请求的是 2M page (LVL_PD),根据当前 current EPT level 获得 EPT entry ,此时得到的是 LVL_PDP,发现与 request EPT level (LVL_PD) 不等,验证当前 SEPT entry 是否有效((!l2_sept_guest_side_walk && (cached_sept_entry->rwx == 0)))和是否为最低层 leaf entry :is_secure_ept_leaf_entry(cached_sept_entry)(当前指的是 1G page)不满足条件,继续判断是否到了 LVL_PT ,这里也不会满足条件,继续循环,此时 current EPT level 指向了 LVL_PD,再次获得 EPT entry ,发现与 request EPT level 的 LVL_PD 相等,退出并返回 SEPT entry,这时的退出认为是“成功”退出。
例2:请求的是 4K page (LVL_PT), 与上面循环递减一样,一直到 current EPT level 为 LVL_PT,此时发现与 request EPT level 相等退出循环,注意:这个过程中最后获取到的 4K SEPT entry 的有效性(RWX) 与 leaf entry 还没有来得及判断,因为 current_lvl == LVL_PT 判断在之前满足后直接退出了,所以最后验证的是 2M SEPT entry (RWX)有效且不是 leaf entry,这时的退出认为是“成功”退出。
以上的 (例1)(例2) 都是“正常情况”,首先 l2_sept_guest_side_walk 为 FALSE 那么 sept_free 条件也就不会满足,再有 SEPT entry 是否有效只有在 request EPT level 与 current EPT level 不等情况下才会判断,如果这里满足条件那说明上级的 SEPT entry 缺失会提前退出,下面会讲到这种情况,那究竟什么场景下会有不等的情况,有几种这样的场景?这就与当前环境有关了,当前上下文是 TD Guest 要 accept 一段已映射但处于 pending 状态的 private memory,这段内存是由 Host VMM 发送 TDH.MEM.ADD.SEPT & TDH.MEM.PAGE.AUG 联合创建的,前者也就是tdh_mem_sept_add 的调用流程在之前分析过,它主要的目的是为 private memory 准备好相关的 SEPT entry(这里再次强调这个调用没有建立 GPA->HPA 映射,仅仅是在固定大小的 ia32e_sept_t sept[512] 中根据 EPT level“分配”位置并初始化建立了相应的 SEPT entry)。分配好的 SEPT entry 的状态由 SEPT_FREE 改为 SEPT entry RWX !=0 和 NL_MAPPED (SEPT_PERMISSIONS_RWX ),也就是 SEPT entry 变为有效但是没有映射的一种中间状态。最终由后者 tdh_mem_page_aug 真正建立了 GPA ->HPA 的映射,成功建立映射后,这里需要格外留意此时这个 SEPT page 状态由之前的 RWX 有效和非 MAPPED 变为 RWX 无效即 0 和 pending 状态(SEPT_PERMISSIONS_NONE | SEPT_STATE_PEND_MASK),而当前的 tdg_mem_page_accept 恰好是要将此
pending 状态的 SEPT page 变成 MAPPED 状态,这样 TD Guest 才能够使用此段 private memory。
把接口设计成这样的意义是,按照 TDX module spec 1.5 解释这么设计是为了防止 Host VMM 发起 TDH.MEM.PAGE.REMOVE 替换 TD Guest 页面,从而可能导致的潜在攻击,这也充分体现了不信任 Host VMM 的思想,把“使用”(accept)内存的权力交给 TD Guest,如果访问了一个未被 accept 的 SEPT entry 则产生一个 #VE 异常。有点儿扯远了,回到当前场景,那么在“此阶段”中就可能存在一种情况那就是 TDH.MEM.PAGE.AUG 是2M SEPT page ,但在某段时间过后 TD Guest 调用 TDCALL(TDG.MEM.PAGE.ACCEPT) 一个 4K SEPT page,而这 4K SEPT page 恰好落在了这2M SEPT page 范围内。好了下面我们看“非正常情况”
例3:请求的是 4K page (LVL_PT), 根据当前 current EPT level 获得 EPT entry ,此时得到的是 LVL_PDP,发现与 request EPT level (LVL_PD) 不等,验证当前 SEPT entry 有效性(RWX)( 这里先假设为有效,如果无效那就是“例4”的故事了)和是否为最低层 leaf entry(当前指的是 1G page)此时不满足条件,继续判断是否到了 LVL_PT ,这里显然不会满足条件,继续循环,此时 current EPT level 指向了 LVL_PD,再次获得 EPT entry ,验证当前 SEPT entry 是否为最低层 leaf entry(当前指的是 2M page) 满足条件提前退出,即这里满足了 is_secure_ept_leaf_entry(cached_sept_entry) 条件导致了错误,注意:此时的 request EPT level != current EPT level 准确的说是 current EPT level > request EPT level ,这也就是 check_tdaccept_failure 注释中描述的那种情况,那这到底意味着什么?这里先不说,放到后面介绍 lock_sept_check_and_walk_internal 错误处理与check_tdaccept_failure 时再讲 。
再考虑另一种情况前我们回头去看 secure_ept_walk 的调用者 lock_sept_check_and_walk_internal,它是以 lock_type = TDX_LOCK_NO_LOCK 方式进行调用的,也就是在 SEPT pagewalk 时没有加 secure_ept_lock,而当前环境又是可被 interrupt pending 中断的且再调用 secure_ept_walk 的时候最后一个参数 l2_sept_guest_side_walk = FALSE,这样就为 !l2_sept_guest_side_walk && (cached_sept_entry->rwx == 0) 这条语句提供了可能性。
例4:请求的是 2M page (LVL_PD), 根据当前 current EPT level 获得 SEPT entry ,此时得到的是 LVL_PDP,发现与 request EPT level (LVL_PD) 不等,查看传进的参数 l2_sept_guest_side_walk 这里显然不会是 TD Guest 发起的EPT pagewalk 所以为 FALSE,那么进一步查看 SEPT entry 的有效性,这里的有效性是通过 SEPT entry 的 RWX 属性判断的,此时若发现为 0 ,则认为 SEPT entry 可能被“完成”,所以这里 !l2_sept_guest_side_walk && (cached_sept_entry->rwx == 0) 语句被满足导致错误提前退出。注意此时的 request EPT level != current EPT level 准确的说是 current EPT level > request EPT level 。
在这里我想对以上情况还有必要进一步澄清,否则疑问越攒越多很难继续,可以看到在 TDX module spec 1.5 中给出了 SEPT entry state,其中只有 SEPT_PRESENT 时其 RWX 才不为 0,其余的像 SEPT_FREE、SEPT_BLOCKED、SEPT_PENDING 的 RWX 则都为 0,而在当前上下文中我们要处理的 SEPT entry 就是 SEPT_PENDING ,也就是说在这之前调用的 TDH.MEM.SEPT.ADD 和 TDH.MEM.PAGE.AUG 只是使用 GPA->HPA 建立了映射,但此 SEPT entry 还不可用,而在此场景中就是为了把 SEPT_PENDING 变为 SEPT_PRESENT,如果按照 例4 中的 !l2_sept_guest_side_walk && (cached_sept_entry->rwx == 0) 那岂不是没有一个可转换的 SEPT entry 了?这就需要仔细观察更高层的代码逻辑了,也就是调用 secure_ept_walk 的 walk_private_gpa->lock_sept_check_and_walk_internal,这个 lock_sept_check_and_walk_internal->secure_ept_walk 的调用流程是多个 TDG_XXX 与 TDH_XXX 复用的,由不同参数(更准确的说是标志)区分当前调用场景,在 TDG.MEM.PAGE.ACCEPT 调用场景下 lock_sept_check_and_walk_internal 传入的第7个参数是 SEPT_WALK_TO_LEVEL,这个参数的含义是当 SEPT pagewalk 时当前 EPT level 与请求的 EPT level 一致时停止继续遍历,在其内部调用 secure_ept_walk 后有如下判断 ((walk_type == SEPT_WALK_TO_LEVEL) && (*level != requested_level)) ,注意这个细节当使用 SEPT_WALK_TO_LEVEL 标志时,只判断了 secure_ept_walk 的 request EPT level 与current EPT level 是否相等的情况,没有对 SEPT entry 有效性(RWX) 和 是否为 leaf entry 做进一步判断,也就是说只要是满足了相等那么就认为是“成功”的返回 TDX_SUCCESS ,这也就解释通了,在 TDG.MEM.PAGE.ACCEPT 的场景下因其 SEPT entry 处于无效(RWX == 0 )且为 pending 状态(但此时 GPA->HPA 已成功建立),此时所谓的“成功”与否只需要判断请求 accept 的 EPT level 和当前 EPT pagewalk 时的 EPT level 是否相等,相等则可 accept,这里也解释了 (例1)(例2) 的“成功”的场景。
到现在 secure_ept_walk 的细节分析完毕,我们总结出 4个例子来说明 TDG.MEM.PAGE.ACCEPT 下的 SEPT pagewalk 场景,前两个例子到现在我们认为是“成功”的,之所以是打引号的“成功”仅是因为 pagewalk 的“成功”不能代表 accept SEPT entry 的成功,这里还需要进一步的判断当前场景,这就需要回过头查看 secure_ept_walk 的调用者 walk_priveate_gpa >lock_sept_check_and_walk_internal 以及回答 check_tdaccept_failure 到底在 check 什么,关于 lock_sept_check_and_walk_internal 调用 secure_ept_walk 时输入的标志之前已经提到过了 l2_sept_guest_side_walk 为 FALSE,在 walk_private_gpa 调用 lock_sept_check_and_walk_internal 的时候传入的是,SEPT_WALK_TO_LEVEL 标志, 在其内部可以看到如下对 secure_ept_walk 返回后的检查:
ept_level_t requested_level = *level;
*sept_entry_ptr = secure_ept_walk(septp, gpa, hkid, level, cached_sept_entry, false);
if (// When we walk to leaf we check that the final entry is a valid, existing leaf
((walk_type == SEPT_WALK_TO_LEAF) &&
(!is_secure_ept_leaf_entry(cached_sept_entry) || !cached_sept_entry->rwx)) ||
// When we walk to level, we just check that we reached requested level
((walk_type == SEPT_WALK_TO_LEVEL) && (*level != requested_level)) ||
// When we walk to leaf-level, check that we reached an actual leaf
((walk_type == SEPT_WALK_TO_LEAF_LEVEL) && !is_secure_ept_leaf_entry(cached_sept_entry))
)
{
。......
return api_error_with_operand_id(TDX_EPT_WALK_FAILED, operand_id);
}
在当前 TDG.MEM.PAGE.ACCEPT 的上下文中只需验证 ((walk_type == SEPT_WALK_TO_LEVEL) && (*level != requested_level)) 这一种情况,在上面已经分析过了,这里再次强调一下,此处没有对 SEPT entry 的有效性(RWX)进行检查,试想一下如果在这里检查有效性,那么所有的 TDH.MEM.PAGE.AUG 的 SEPT page 都将不会被 TD Guest 使用,因为现在的 SEPT page 是 SEPT_PENDING 状态,这个状态的页面一定是 RWX == 0 的,也就是无效的,而TDG.MEM.PAGE.ACCEPT 的工作恰恰是将这些已经建立了 GPA->HPA 映射的无效状态的 SEPT page 修改成有效并标记为 MAPPED,这样才能被 TD Guest 使用。这也从侧面说明在当前上下文中只能验证这一种情况,否则在语义上会存在矛盾。
到现在我们明白了只要 request EPT level 与 SEPT pagewalk 中 current EPT level 不相等,准确的说后者大于前者就认为是 SEPT pagewalk 失败返回 EPT_WALK_FAILED,只要两者相等就认为是“成功”的返回 TDX_SUCCESS,这也是 walk_private_gpa->lock_sept_check_and_walk_internal->secure_ept_walk 的验证细节和整体逻辑,根据现在掌握的细节,我们回过头去看 check_tdaccept_failure 并回答之前提出的 (1)(2)(3)(4)问题
我们这里分开讨论:
check_tdaccept_failure 的源码在 src/td_dispatcher/vm_exits/tdg_mem_page_accept.c 可对照参考
(1):TD Guest 申请 2M page 相同地址(即:被中断的 2M page), 处于 pending 状态:
上下文条件:
参考 secure_ept_walk 的 “例1”
1、循环第一次 reqeust EPT level (2M) < current EPT level(1G)不相等,继续
1.1 验证 LVL_PDP 的 SEPT entry (1G PAGE) 此时为有效 ,继续
1.2 验证 leaf entry ,此时不是 leaf entry, 继续
2、循环第二次 reqeust EPT level (2M) == current EPT level (2M)相等,退出循环(注意:此时直接返回,没有判断 SEPT entry (RWX) 有效性以及 leaf entry)
3、lock_sept_check_and_walk_internal 验证 request EPT level 与 current EPT level 相等返回 TDX_SUCCESS
4、is_leaf == TRUE (当前 2M 是 leaf entry)
check_tdaccept_failure:
当前场景首先满足了 walk_failed == FALSE 的情况,见 check_tdaccept_failure 的第一个参数(return_val != TDX_SUCCESS),也就是 SEPT pagewalk 成功,接下来我们看 check_tdaccept_failure 函数内部的判断,首先因为传进来的是 TDX_SUCCESS 说明不会走 IF_RARE (walk_failed) 分支,然后因为 is_leaf == TRUE 所以 IF_RARE (!is_sept_free(sept_entry_copy) && !is_leaf) 这个条件也不会满足,再然后会对 SPT entry 进行判断即sept_state_is_guest_accessible_leaf(*sept_entry_copy) , 看到了么,这里才对 SEPT entry 进行验证,但这里的验证并不是直接对其有效性(RWX != 0) 的验证,而是验证这个 SEPT entry 是否可被 TD Guest 访问,我在前文也从侧面提及过所谓 TD Guest 可访问的 SEPT page 状态,其实就是 MAPPED 状态(state_encoding_X,X=1_4 or 5_6 索性把3个标志位放到这里体现,这是 ia32_sept_entry 状态字段,通过查表判断当前处于 SEPT_STATE_MAPPED、SEPT_STATE_PENND、SEPT_STATE_BLOCKED 等其中一种)结合当下(1) 的场景是在 tdg_mem_page_accept 时被 interrupt pending 打断,所以导致 2M SEPT page没有初始化完成(因只有所有相关的 4K SEPT page 都初始化完成后才会设置 SEPT page 为有效(RWX) 和 MAPPED 状态),所以这里仍旧是 pending ,那么也就是不满足 sept_state_is_guest_accessible_leaf(*sept_entry_copy) 的条件,接着 !sept_state_is_tdcall_leaf_allowed(TDG_MEM_PAGE_ACCEPT_LEAF, *sept_entry_copy) 判断的是当前 TD Guest 是否有权 accept 这个 SEPT entry(这实际上也是个查表操作,具体的操作细节我会在最后对 sept_state_is_tdcall_leaf_allowed 与 sept_state_is_guest_accessible_leaf 分析中给出),那什么叫有权什么叫无权呢?首先就是当前 TD Guest 所处环境能否调用 TDG.MEM.PAGE.ACCEPT ,再就是TD Guest 只能 accept 一个 pending 的 SEPT page,如果处于 MAPPED 或 BLOCKED 再或者其它什么状态的 SEPT page 则无法 accept,将这两种状态组合为一个值查表得到的结果作为判断。就当前场景这个状态会返回为 TRUE也就是允许,所以这个条件也不会满足,最后整体返回为 TDACCEPT_SUCCESS,代表 tdg_mem_page_accept 可以继续操作此上次未处理完成处于 pending 的 2M SEPT page。
总结(1):
第一步 SEPT pagewalk (walk_private_gpa->lock_sept_check_and_walk_internal->secure_ept_walk)返回 TDX_SUCCESS,这里代表 SEPT pagewalk 成功返回 SEPT entry。
第二步 check_tdaccept_failure 对 SEPT entry 检查全部通过(仅为 SEPT_PENDING 状态)返回 TDACCEPT_SUCCESS。
也就是以上两部都成功,才能继续操作,那接下来会发生什么呢?这里假设后续没有 interrupt pending 处理:
之前的 SEPT pagewalk 时从 keyhole LRU 命中(非重新分配)获得此 SEPT entry,其 accept_counter 保持原有计数,当下继续 SEPT entry 的 accept_counter += 1 的操作,直至满足 SEPT entry 的 accept_counter ==
(NUM_OF_4K_PAGES_IN_2MB - 1) 情况,设置 RWX 有效(SEPT_PERMISSIONS_RWX)并 MAPPED (SEPT_STATE_MAPPED_MASK)最终完成全部的 TDG.MEM.PAGE.ACCEPT 操作
(2):TD Guest 申请 2M page 相同地址(即:被中断的 2M page), 2M 处于 MAPPED 状态:
上下文条件:
参考 secure_ept_walk 的 “例1”
1、循环第一次 reqeust EPT level (2M) < current EPT level(1G)不相等,继续
1.1 验证 LVL_PDP 的 SEPT entry (1G PAGE) 此时为有效 ,继续
1.2 验证 leaf entry ,此时不是 leaf entry, 继续
2、循环第二次 reqeust EPT level (2M) == current EPT level (2M) 相等,退出循环(注意:此时直接返回,没有判断 SEPT entry (RWX) 有效性)
3、lock_sept_check_and_walk_internal 验证 request EPT level 与 current EPT level 相等返回 TDX_SUCCESS
4、is_leaf == TRUE (当前 2M 是 leaf entry)
check_tdaccept_failure:
当前场景首先满足了 walk_failed == FALSE 的情况,见 check_tdaccept_failure 的第一个参数(return_val != TDX_SUCCESS),也就是 SEPT pagewalk 成功,接下来我们看 check_tdaccept_failure 函数内部的判断,首先因为传进来的是 TDX_SUCCESS 说明不会走 IF_RARE (walk_failed) 分支,然后 is_leaf == TRUE 所以 IF_RARE (!is_sept_free(sept_entry_copy) && !is_leaf) 这个条件也不会满足, 其实到这一步同(1)的流程是一致的,接着调用 sept_state_is_guest_accessible_leaf(*sept_entry_copy),到这里满足 MAPPED 条件,也就是 TD Guest 已经在某个时刻 accept 了这个 2M page,这里处于重复请求已被 TD Guest 使用的页面,从细节上说就是这个 2M SEPT page 的RWX 为有效且为 MAPPED 状态,结合当下(2) 的场景是虽然之前 tdg_mem_page_accept 被 interrupt pending 打断,导致 2M SEPT page 没有初始化完成,但在某时刻通过其它线程或者从 interrupt pending 返回后继续完成了这次 accept ,再次对相同 2M 地址的提交视为重复提交,当前场景返回 TDACCEPT_ALREADY_ACCEPTED。
总结(2):
第一步 SEPT pagewalk (walk_private_gpa->lock_sept_check_and_walk_internal->secure_ept_walk)返回 TDX_SUCCESS,这里代表 SEPT pagewalk 成功返回 SEPT entry。
第二步 check_tdaccept_failure 对 SEPT entry 检查不通过(因 SEPT_STATE_MAPPED_MASK 状态)返回 TDACCEPT_ALREADY_ACCEPTED。
这里也从侧面说明了,SEPT pagewalk “成功”不代表 TDG.MEM.PAGE.ACCEPT 的成功,需要配合 check_tdaccept_failure 成功,才是 TDG.MEM.PAGE.ACCEPT 的成功。
(3):TD Guest 申请 4K page 地址,落在了之前被中断的 2M page 范围内,2M 处于 pending 状态
上下文条件:
参考 secure_ept_walk 的 “例4”
1、循环第一次 reqeust EPT level (4K) < current EPT level(1G)不相等,继续
1.1 验证 LVL_PDP 的 SEPT entry (1G PAGE) 此时为有效 ,继续
1.2 验证 leaf entry ,此时不是 leaf entry, 继续
2、循环第二次 reqeust EPT level (4K) < current EPT level(2M)不相等,继续
2.1 验证 LVL_PD 的 SEPT entry (2M PAGE) 此时为无效 ,错误退出
3、lock_sept_check_and_walk_internal 验证 request EPT level != current EPT level 不相等返回 TDX_EPT_WALK_FAILED
4、is_leaf == TRUE (当前 2M 是 leaf entry)
check_tdaccept_failure:
当前场景首先满足了 walk_failed == TRUE 的情况,见 check_tdaccept_failure 的第一个参数(return_val != TDX_SUCCESS),也就是 SEPT pagewalk 失败,接下来我们看 check_tdaccept_failure 函数内部的判断,首先因为传进来的是 TDX_EPT_WALK_FAILED 说明会走到 IF_RARE (walk_failed) 分支内,接着对 SPT entry 进行判断即 sept_state_is_guest_accessible_leaf(*sept_entry_copy) , 验证这个 SEPT entry 是否可被 TD Guest 访问,在对(1)的讨论中提到
过判断细节,所谓 TD Guest 可访问的 SEPT page 状态,其实就是 MAPPED 状态,在(3)的场景中显然不是,因其 SEPT page 处于 pending 状态,所以条件不满足走到了 else 分支,返回 TDACCEPT_VIOLATION。这也就是 “Case 1.2”与 “Case 2” 注释中的具体场景。
总结(3):
第一步 SEPT pagewalk (walk_private_gpa->lock_sept_check_and_walk_internal->secure_ept_walk)返回 TDX_EPT_WALK_FAILED,这里代表 SEPT pagewalk 失败。
第二步 check_tdaccept_failure 对 SEPT entry 检查发现是 SEPT_PENDING 状态 返回 TDACCEPT_VIOLATION。
那接下来会发生什么呢?在 tdg_mem_page_accept 中调用 async_tdexit_ept_violation 以 TD VMEXIT 的方式通知 Host VMM,多数情况下会选择把这个 2M page split 为 4K (这里就不在追踪了)。
(4):TD Guest 申请 4K page 地址,落在了之前被中断的 2M page 范围内,2M 处于 MAPPED 状态
上下文条件:
参考 secure_ept_walk 的 “例3”
1、循环第一次 reqeust EPT level (4K) < current EPT level(1G)不相等,继续
1.1 验证 LVL_PDP 的 SEPT entry (1G PAGE) 此时为有效 ,继续
1.2 验证 leaf entry ,此时不是 leaf entry, 继续
2、循环第二次 reqeust EPT level (4K) < current EPT level(2M)不相等,继续
2.1 验证 LVL_PD 的 SEPT entry (2M PAGE) 此时为有效 ,继续
2.2 验证 leaf entry ,此时是 leaf entry, 错误退出
3、lock_sept_check_and_walk_internal 验证 request EPT level != current EPT level 不相等返回 TDX_EPT_WALK_FAILED
4、is_leaf == TRUE (当前 2M 是 leaf entry)
check_tdaccept_failure:
当前场景首先满足了 walk_failed == TRUE 的情况,见 check_tdaccept_failure 的第一个参数(return_val != TDX_SUCCESS),也就是 SEPT pagewalk 失败,接下来我们看 check_tdaccept_failure 函数内部的判断,首先因为传进来的是 TDX_EPT_WALK_FAILED 说明会走到 IF_RARE (walk_failed) 分支内,接着对 SPT entry 进行判断即 sept_state_is_guest_accessible_leaf(*sept_entry_copy) , 验证这个 SEPT entry 是否可被 TD Guest 访问,在对(1)的讨论中提到
过判断细节,所谓 TD Guest 可访问的 SEPT page 状态,其实就是 MAPPED 状态,在(4)的场景中满足了此条件,直接返回TDACCEPT_ALREADY_ACCEPTED。
总结(4):
第一步 SEPT pagewalk (walk_private_gpa->lock_sept_check_and_walk_internal->secure_ept_walk)返回 TDX_EPT_WALK_FAILED,这里代表 SEPT pagewalk 失败。
第二步 check_tdaccept_failure 对 SEPT entry 检查发现是 SEPT_STATE_MAPPED_MASK 状态返回 TDACCEPT_ALREADY_ACCEPTED。
在 check_tdaccept_failure 的验证中有两处会返回 TDACCEPT_ALREADY_ACCEPTED,一种是(2)中的情况,也就是 SEPT pagewalk 成功的前提下,最终验证为 TD Guest 已在使用此页面(TDACCEPT_ALREADY_ACCEPTED),在(2)的上下文触发条件是 secure_ept_walk 中的 “例1”。还有一种就是当前(4)的情况,也就是 SEPT pagewalk 失败的前提下,最终验证为 TD Guest 已在使用此页面(TDACCEPT_ALREADY_ACCEPTED),在(4)的上下文
触发条件是 secure_ept_walk 中的“例3”。虽然都是返回 TDACCEPT_ALREADY_ACCEPTED 但可以看到因场景不同,走的路径也都不同,在(2)中返回 TDACCEPT_ALREADY_ACCEPTED 是代表请求 2M 但发现此 2M 已被使用,而在 (4) 中的是请求的是 4K 但落在了已被使用的 2M 范围内。
(5):TD Guest 申请 2M page 任意地址,返回 4k page(非 leaf entry 情况),2M 处于非 free 状态
上下文条件:
参考 secure_ept_walk 的 “例1”
1、循环第一次 reqeust EPT level (2M) < current EPT level(1G)不相等,继续
1.1 验证 LVL_PDP 的 SEPT entry (1G PAGE) 此时为有效 ,继续
1.2 验证 leaf entry ,此时不是 leaf entry, 继续
2、循环第二次 reqeust EPT level (2M) == current EPT level (2M) 相等,退出循环(注意:此时直接返回,没有判断 SEPT entry (RWX) 有效性,
这里还 有必要提示一下,此处没有判断 leaf entry)
3、lock_sept_check_and_walk_internal 验证 request EPT level 与 current EPT level 相等返回 TDX_SUCCESS
4、is_leaf == FALSE (当前 2M 为 non-leaf)
check_tdaccept_failure:
当前场景首先满足了 walk_failed == FALSE 的情况,见 check_tdaccept_failure 的第一个参数(return_val != TDX_SUCCESS),也就是 SEPT pagewalk 成功,接下来我们看 check_tdaccept_failure 函数内部的判断,首先因为传进来的是 TDX_SUCCESS 说明不会走 IF_RARE (walk_failed) 分支,然后 is_leaf == FALSE 所以 IF_RARE (!is_sept_free(sept_entry_copy) && !is_leaf) 满足条件,这里需要解释,所谓非 leaf entry 就是 non-left 即 SEPT pagewalk 的一个中间结果,下面还有更低层的SEPT entry 存在,现在遍历到 2M SEPT entry,这不是最终的,下面一定还有最终的 4K SEPT entry,也就是有在此场景下实际上是 4K 的 SEPT page,那么最终返回的是 TDACCEPT_SIZE_MISMATCH,说明请求的是 2M SEPT page,但是得到的却是一个 4K 的 SEPT page,那这 4K 的 SEPT page 的状态在此场景中也就显得没那么重要了,因为请求级与结果不符无需继续验证了。这也就是注释中所说的 “Case 3” 的具体场景。
总结(5):
第一步 SEPT pagewalk (walk_private_gpa->lock_sept_check_and_walk_internal->secure_ept_walk)返回 TDX_SUCCESS,这里代表 SEPT pagewalk 成功返回 SEPT entry。
第二步 check_tdaccept_failure 对 SEPT entry 的检查为 free 与 non-leaf 状态时返回 TDACCEPT_SIZE_MISMATCH
在(5)场景下的 check_tdaccept_failure 验证中只要不是 free 状态,其它什么其它状态都不重要了,因为它是一个 non-leaf 也就是当前的 SEPT page 肯定不是请求的 2M SEPT entry,只不过是在 SEPT pagewalk 时 secure_ept_walk 只判断了 request EPT level == current EPT level 的情况还没来得及进行 leaf entry 的验证就直接返回了,所以要延迟到 check_tdaccept_failure 时再进一步验证 “请求 2M page 但返回 4K page”的这种非常奇葩的状态,我个人觉得在
这里判断其实是和 TDX module 中 secure_ept_walk 与 lock_sept_check_and_walk_internal 的代码结构有关,因需要考虑到多种场景不同参数组合且提供统一的接口,否则的话这种情况在 SEPT pagewalk 中就可以直接判断出来。
到此把 3个例子结合到 5 个场景下的情况都讨论完成了,这里的 5 个场景都是以 2M SEPT page 为例,就像上面介绍的那样,因其包含了覆盖 4K SEPT page 的情况,这样引发的路径不同可讨论的情况也就更多。这里有一个 secure_ept_walk 的“例2:请求的是 4K page (LVL_PT)为我觉得“错误”的情况在 (2)(3)(4)中都已经涉猎到了,只不过用的是 2M SEPT page 的场景,而成功的场景(1)也相同,把其置换成 4K SEPT page 是一样仅仅是多遍历一层而已。
PPT 是为了讲的,但做研究的时候不是为了讲,发在这里,我索性把一些相关内容贴出来,对此有兴趣的方便一起探讨,如果您能指出文章中的错误我将不胜感激
[培训]科锐逆向工程师培训第53期2025年7月8日开班!
最后于 2025-2-26 09:42
被metaphys编辑
,原因: 上传附件