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 流程的人员。

Reviewers:

  • 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_USERZX_EXCP_SYNTH 位集的新 zx_excp_type_t

#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 实际更改进程名称的情况只有一种,即在该进程内的线程中发生名称更改,这意味着我们无需解决在远程线程中生成异常的问题,即可解决当前的用例。