RFC-0244:引发用户定义的 Zircon 异常

RFC-0244:引发用户定义的 Zircon 异常
状态已接受
领域
  • 内核
说明

引入了用于引发用户定义的 Zircon 异常的系统调用

Gerrit 更改
作者
审核人
提交日期(年-月-日)2024-03-22
审核日期(年-月-日)2024-04-22

摘要

此 RFC 引入了 zx_thread_raise_exception 系统调用,它会引发用户定义的 Zircon 异常。该系统调用的第一个用例是 Starnix 在其某个进程调用 exec() 时向调试程序发出信号。调试程序将使用此信号来确定开发者是否想要连接到进程。

设计初衷

在 Starnix 中运行进程时,我们经常需要使用进程的名称来指定是否要将调试程序连接到该进程。如果进程已在运行,则此方法非常适用,因为调试程序可以检查现有进程的 ZX_PROP_NAME 以找到我们所需的进程。但是,此方法不适用于尚未运行的进程,因为 Starnix 进程由 fork() 创建,此时其 ZX_PROP_NAME 与名为 fork() 的进程的名称匹配。Starnix 在 exec() 期间更改了进程的 ZX_PROP_NAME,但调试程序绝不会注意到,因此不会连接到进程。

利益相关方

谁能决定此 RFC 是否被接受?(此部分为可选内容,但我们建议您。)

教员

由 FEC 指定的人员,负责通过 RFC 流程照管此 RFC。

审核者

  • cpu@google.com
  • jamesr@google.com
  • jruthe@google.com

咨询人员

列出应审核 RFC,但无需审批的人员。

社交

这个问题在 Zircon 聊天频道中进行了讨论,我按照收到的建议创建了一个原型,该原型演示了使用用户定义的异常按名称附加到尚未运行的 Starnix 进程的端到端流程。

要求

  1. 当 Starnix 进程调用 exec() 时,调试程序必须收到通知,以便检查进程是否与其任何过滤条件匹配(例如,进程的新名称是否与名称过滤条件匹配)。
  2. 如果未运行调试程序,通知机制不应占用大量资源。
  3. 通知机制必须处理多个调试代理同时运行的情况。
  4. 设计时不应要求我们更改系统的其他部分(例如,crashsvc)。

设计

调试程序通过监听 ZX_EXCEPTION_CHANNEL_TYPE_JOB_DEBUGGER 上的 ZX_EXCP_PROCESS_STARTING 异常来了解正在创建的新进程。此 RFC 中的方法是通过 ZX_EXCEPTION_CHANNEL_TYPE_JOB_DEBUGGER 发送其他类型的异常,通知调试程序进程名称发生了更改。

遗憾的是,我们不希望 Zircon 在进程的 ZX_PROP_NAME 属性发生更改时自动生成异常,因为该属性可由任意线程更改。相反,我们希望通过名称发生变化的进程的线程生成异常。幸运的是,Starnix 总是通过进程内的线程更改进程的名称,无论是通过 exec() 还是通过 procfs 中只能从进程内写入的文件。

因此,我们引入了一个新的系统调用,用于生成用户定义的异常。每当 Starnix 更改进程的名称时,Starnix 都会使用此系统调用来引发此类异常。调试程序将监听这些异常并重新扫描其附加过滤器列表,查看用户是否希望调试使用新名称的进程。

用户定义的例外情况

与 Zircon 对象上用户定义的信号类似,此 RFC 为用户定义的异常预留了 Zircon 异常命名空间的一部分。这项预留可确保用户定义的异常不会与将来扩展系统定义的异常冲突。

具体来说,此 RFC 定义了一个新的 zx_excp_type_t,其 ZX_EXCP_SYNTH 位集名为 ZX_EXCP_USER

#define ZX_EXCP_USER                    ((uint32_t) 0x309u | ZX_EXCP_SYNTH)

此 RFC 还定义了一些常见的用户异常代码,这些代码会显示在 zx_exception_context_tsynth_code 字段中。

ZX_EXCP_USER_CODE_PROCESS_NAME_CHANGED  ((uint32_t) 0x0001u)
ZX_EXCP_USER_CODE_USER0                 ((uint32_t) 0xF000u)
ZX_EXCP_USER_CODE_USER1                 ((uint32_t) 0xF001u)
ZX_EXCP_USER_CODE_USER2                 ((uint32_t) 0xF002u)

Starnix 和调试程序将针对上述用例使用 ZX_EXCP_USER_CODE_PROCESS_NAME_CHANGED 代码。ZX_EXCP_USER_CODE_USER0ZX_EXCP_USER_CODE_USER1ZX_EXCP_USER_CODE_USER2 代码是针对具体应用用途定义的,类似于 PA_USER0PA_USER1PA_USER2

小于 ZX_EXCP_USER_CODE_USER0 的代码已预留用于系统范围使用,并且您可以在以后的 RFC 中定义。

引发用户定义的异常

该 RFC 定义了一个用于引发用户定义的异常的系统调用:

zx_status_t zx_thread_raise_exception(uint32_t options,
                                      zx_excp_type_t type,
                                      const zx_exception_context_t* context);

此系统调用会在具有给定异常上下文的当前线程上引发 type 类型的异常。

目前,options 参数必须为 ZX_EXCEPTION_JOB_DEBUGGER,其值为 1。如果调用方传递任何其他值,系统调用会返回 ZX_ERR_INVALID_ARGS。如果提供此值,系统将在作业调试程序通道上传递异常(如果存在此类通道)。

type 参数必须是 ZX_EXCP_USER。如果调用方传递任何其他值,系统调用会返回 ZX_ERR_INVALID_ARGS

系统会忽略 contextarch 字段。目前,context 中的 synth_codesynth_data 字段是通过异常传达信息的主要机制。

如果我们希望扩展此系统调用,以便为其他类型的异常通道提供异常,可以在之后的 RFC 中扩展该系统调用的语义。

实现

此功能将通过添加“设计”部分中所述的系统调用来实现。Zircon 中已存在所有用于引发异常的机制。第二个 CL 将指示 debug_agent 监听这些异常并重新检查生成异常的进程的名称。

概念验证 CL 表明,在 Zircon 和 debug_agent 中的实现非常简单。

性能

此设计对系统性能的影响很小。在没有正在运行的 debug_agent 这种常见情况下,zx_thread_raise_exception 会在导航到任务层次结构的根并注意到没有人监听调试程序异常通道后提前返回。

相反,如果系统中有许多正在运行的 debug_agent 实例,此机制会高效地向每个实例传递此通知。

此代码路径已经针对这两种情况进行了优化,因为系统使用相同的机制将其他常见事件(例如启动进程)通知 debug_agent

工效学设计

本 RFC 中介绍的方法并非特别符合人体工程学。例如,Starnix 需要记住在更改进程名称时引发相应类型的异常。更符合工效学的设计是,每当进程名称发生更改时,Zircon 都会自动引发此异常。不过,这种方法很难实现,因为进程名称可以从系统中具有进程句柄的任何线程更改。Zircon 没有从远程线程引发异常的机制,添加此类机制会使 Zircon 大幅增加复杂性(例如,在返回用户空间之前检查待处理的异常)。

向后兼容性

本文档中所述的设计可向后兼容现有系统。zx_thread_raise_exception 可能生成的异常会明确标记为用户生成的异常,并且具有与内核生成的异常不同的命名空间。该设计还为将来的用户和内核生成的异常预留了命名空间。

安全注意事项

zx_thread_raise_exception 为用户空间提供了一种生成之前无法生成的异常的方法,攻击者可以使用这些异常来操控正在监听异常的软件。该方案旨在将用户生成的异常限制为设置 ZX_EXCP_SYNTH 位,并将其设为包含这些异常的预留命名空间,从而降低这种风险。

用户空间已在微架构级别生成一些异常,例如分别在 ARM 和 Intel 上使用 brkint3 指令,这意味着添加内核中介机制来生成异常的风险也可以降低。

隐私注意事项

虽然进程名称可能包含隐私敏感信息,但这种新机制不会允许任何新进程访问这些信息。例如,与带有新进程名称以及例外的设计相比,此设计具有略微更好的隐私属性。

测试

新的系统调用将通过 Zircon 核心测试进行测试。

系统将使用集成测试来测试调试程序集成。

文档

和 Zircon 系统调用一样,新的系统调用会记录在系统调用手册页面中。新的异常语义也将记录在异常处理概念页面中。

缺点、替代方案和问题

我们考虑了一些替代方案:

使用微架构异常

我们可以使用现有的微架构机制来引发异常,而无需添加系统调用来引发异常(例如,brkint3 指令)。这种方法的缺点是,除非得到处理,否则这些异常将是严重的。我们可以像处理回溯请求一样教 crashsvc 识别这些异常,但不希望教 crashsvccrashsvc 无关的系统功能。

在进程名称发生更改时自动生成异常

我们可以让 Zircon 在每次进程名称更改时自动生成异常,而不是要求 Starnix 在更改进程名称后调用 zx_thread_raise_exception。不过,如上所述,Zircon 进程的名称可由任何具有进程句柄的线程更改,而 Zircon 缺少在远程线程上生成异常的机制。

幸运的是,只有在以下情况下,Starnix 才会真正更改进程名称:名称更改发生在该进程的线程上,这意味着我们不需要解决在远程线程上生成异常的问题,即可解决当前用例。