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

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

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

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

摘要

此 RFC 引入了 zx_thread_raise_exception 系统调用,该调用会引发用户定义的 Zircon 异常。此系统调用的第一个使用情形是,当 Starnix 的某个进程调用 exec() 时,Starnix 会向调试器发出信号。调试器将使用此信号来确定开发者是否想要附加到进程。

设计初衷

在 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_t 中的 synth_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 识别这些例外情况,就像我们教 crashsvc 识别回溯请求一样,但我们不希望教 crashsvc 了解与 crashsvc 无关的系统功能。

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

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

幸运的是,Starnix 实际更改进程名称的唯一情况是名称更改发生在相应进程内的线程上,这意味着我们无需解决在远程线程上生成异常的问题,即可处理当前的使用情形。