概述

威胁模型

Zephyr 将用户模式线程视为不可信,因此将其与其他用户模式线程及内核隔离。有缺陷或恶意的用户模式线程不能泄露或修改其他线程或内核的私有数据和资源,也不能干扰或控制其他用户模式线程或内核。

Zephyr 用户模式功能的使用场景示例:

  • 内核可以防范许多无意的编程错误,否则这些错误可能悄无声息地或以显著方式破坏系统。

  • 内核可以将解释器、网络协议和文件系统等复杂的数据解析器置于沙箱中,使恶意第三方代码或数据无法危及内核或其他线程。

  • 内核可以支持多个逻辑“应用程序”,每个应用程序都有自己的线程组和私有数据结构。某个应用程序崩溃或受到攻击时,它们彼此隔离。

设计目标

对于在非特权 CPU 状态(下文称为“用户模式”)下运行的线程,我们的防护目标如下:

  • 阻止访问未明确授权的内存,或以不符合策略的方式访问内存,例如尝试写入只读区域。

    • 对线程栈缓冲区的访问由策略控制,该策略部分取决于底层内存保护硬件。

      • 用户线程默认具有对自身栈缓冲区的读写权限。

      • 用户线程默认绝不会获得对不属于同一内存域的用户线程栈的访问权限。

      • 用户线程默认绝不会获得对特权线程栈的访问权限,也不会获得对处理系统调用特权提升、中断或 CPU 异常所用线程栈的访问权限。

      • 根据硬件的不同,用户线程可能具有对同一内存域内其他用户线程栈的读写权限。

        • 在 MPU 系统上,线程只能访问自己的栈缓冲区。

        • 在 MMU 系统上,线程可以访问同一内存域内任意用户线程的栈。可移植代码不应依赖这一点。

    • 默认情况下,整个内核中的所有线程都可以只读访问程序代码和只读数据。这项策略可以调整。

    • 除上述内存外,用户线程默认不会获得对任何其他内存的访问权限。

  • 阻止使用未明确授权的设备驱动程序或内核对象,权限粒度为单个对象或单个驱动程序实例。

  • 验证内核或驱动程序 API 调用中的错误参数,避免这些参数导致崩溃或破坏内核私有数据结构。这包括:

    • 使用错误的内核对象类型。

    • 使用超出合理边界或取值无意义的参数。

    • 传入调用线程不具备充分读写权限的内存缓冲区,具体权限要求取决于 API 的语义。

    • 使用未处于正确初始化状态的内核对象。

  • 确保能够检测并安全处理用户模式栈溢出。

  • 阻止通过系统调用调用内核配置所排除的函数。

  • 阻止禁用或篡改由内核定义、硬件强制执行的内存保护。

  • 阻止通过内核定义的系统调用和中断处理程序以外的途径,从用户模式重新进入特权模式。

  • 阻止用户模式线程引入新的可执行代码,内核系统调用明确支持的范围除外。

我们明确不防范以下攻击:

  • 假定内核本身以及所有在特权模式下执行的线程可信。

  • 假定工具链以及构建系统使用的所有辅助程序可信。

  • 假定内核构建过程可信。构建时会执行大量逻辑,用于创建有效内核对象表、定义系统调用和配置中断。假定这一过程中处理的 .elf 二进制文件均为可信代码。

  • 如果在内核模式下错误配置内存域,导致内核私有数据结构暴露给用户线程,我们无法提供保护。内核对象使用的 RAM 应始终配置为仅允许特权模式访问。

  • 可以在顶层声明用户模式线程,并赋予其内核对象访问权限。通常,假定参与内核构建、生成 zephyr.elf 的所有 C 文件和头文件均可信。

  • 我们不防范通过使线程无法获得 CPU 时间而发动的拒绝服务攻击。Zephyr 没有线程优先级老化机制,因此某一优先级的用户线程可以使所有较低优先级线程饥饿;如果未启用时间片,同优先级的其他线程也可能饥饿。

  • 构建时会定义同时活动的线程数量上限,达到上限后,创建新的用户线程将失败。

  • 在特权模式下运行的线程发生栈溢出时,可能能够检测到,但无法保证系统的完整性。

总体策略细节

总体而言,我们通过以下机制实现这些线程级内存保护目标:

  • 任何用户线程都只能访问一部分内存:通常包括自身栈、程序代码、只读数据,以及其所属 内存保护设计 中配置的分区。对其他 RAM 的访问必须通过系统调用代表线程执行,或由特权线程通过内存域 API 明确授权。新创建的线程继承父线程的内存域配置。线程可以通过同属一个内存域来通信,也可以使用信号量、管道等内核对象通信。

  • 用户线程不能直接访问属于内核对象的内存。虽然通过指向内核对象的指针引用对象,但实际操作通过系统调用接口完成。设备驱动程序和线程栈也被视为内核对象。这确保用户线程无法篡改内核对象中属于内核的私有数据。

  • 用户线程默认无权访问自身线程对象以外的任何内核对象或驱动程序。此类访问必须由另一个线程授权:该线程要么处于特权模式,要么同时拥有接收权限的线程对象以及被授权访问的内核对象的权限。创建新线程时,可以选择自动继承父线程获授的全部内核对象权限,但父线程自身除外。

  • 出于性能和内存占用方面的考虑,Zephyr 通常很少或完全不对内核对象或设备驱动程序 API 进行参数错误检查。用户模式通过系统调用访问这些 API 时,会经过额外的一层处理函数。这些函数应严格验证对象的访问权限和类型,通过边界检查等方式检查其他参数的有效性,并验证对相关内存缓冲区具有适当的读写权限。

  • 线程栈的定义确保超出指定栈空间时会产生硬件故障,具体方式随架构而异。

约束

要从用户模式使用内核对象、线程栈和设备驱动程序实例,就必须在构建时定义它们。内核对象的动态使用场景需要通过预先定义的可用对象池实现。

如果在内核启动后加载额外的应用程序二进制数据并执行,则存在一些约束:

  • 加载的目标代码无法定义可被内核识别的内核对象,而需要使用 API 从对象池中请求内核对象。

  • 同样,由于加载的目标代码不参与内核构建过程,无论运行于何种模式,它都无法安装中断处理程序、实例化设备驱动程序或定义系统调用。

  • 对于来源未经验证的已加载目标代码,应始终在 CPU 已处于用户模式时进入执行。