Syscall User Dispatch

背景

像 Wine 这样的兼容层需要一种方法来高效地模拟其进程中仅一部分的系统调用(即包含不兼容代码的部分),同时能够在进程的原生部分执行原生系统调用,且不会产生高额的性能代价。Seccomp 在此任务上能力不足,因为它对基于内存区域高效过滤系统调用的支持有限,且不支持移除过滤器。因此,需要一种新的机制。

Syscall User Dispatch 将系统调用分发器的过滤功能带回了用户空间。应用程序控制着一个切换开关,用以指示进程当前的“个性”(personality)。当跨越兼容层 API 边界时,多“个性”应用程序可以在不调用内核的情况下翻转此开关,从而启用/禁用系统调用重定向,并直接执行系统调用(禁用状态)或将其发送到用户空间通过 SIGSYS 进行模拟。

此设计的目的是提供非常快速的兼容层边界跨越,这通过避免在兼容层每次执行时都调用系统调用来更改“个性”来实现。相反,暴露给内核的用户空间内存区域指示了当前的“个性”,应用程序只需修改该变量即可配置此机制。

在大多数架构(如 x86)上,处理信号存在相对较高的成本。但至少对于 Wine 而言,目前已知原生 Windows 代码发出的系统调用并不是性能问题,因为它们相当少见,至少对于现代游戏应用程序来说是这样。

由于此机制旨在捕获由非原生应用程序发出的系统调用,它必须能够作用于那些调用 ABI 对 Linux 完全不可预知的系统调用。因此,Syscall User Dispatch 不依赖任何系统调用 ABI 来进行过滤。它仅使用系统调用分发器的地址和用户空间密钥(key)。

由于这些被拦截的系统调用的 ABI 对 Linux 未知,因此无法通过 ptrace 或系统调用跟踪点(tracepoints)对这些系统调用进行检测。

接口

线程可以通过执行以下 prctl 来在受支持的内核上设置此机制:

prctl(PR_SET_SYSCALL_USER_DISPATCH, <op>, <offset>, <length>, [selector])

<op> 可以是 PR_SYS_DISPATCH_EXCLUSIVE_ON/PR_SYS_DISPATCH_INCLUSIVE_ON 或 PR_SYS_DISPATCH_OFF,用于全局启用或禁用该线程的机制。当使用 PR_SYS_DISPATCH_OFF 时,其他字段必须为零。

对于 PR_SYS_DISPATCH_EXCLUSIVE_ON,[<offset>, <offset>+<length>) 定义了一个内存区域区间,来自该区域的系统调用总是被直接执行,而不考虑用户空间的选择器(selector)。这为 C 库提供了一条快速路径,其中包含了原生代码应用程序中最常见的系统调用分发器,同时也为信号处理程序在 (rt_)sigreturn 上返回而不触发嵌套的 SIGSYS 提供了一种方式。此接口的用户应确保至少将信号蹦床(trampoline)代码包含在此区域内。此外,对于在 vDSO 上实现蹦床代码的系统调用,该蹦床绝不会被拦截。

对于 PR_SYS_DISPATCH_INCLUSIVE_ON,[<offset>, <offset>+<length>) 定义了一个内存区域区间,来自该区域的系统调用将根据用户空间选择器进行分发。范围之外的系统调用总是被直接执行。

[selector] 是指向进程内存区域中字符大小区域的指针,它提供了一种在线程范围内快速启用/禁用系统调用重定向的方法,无需直接调用内核。selector 可以设置为 SYSCALL_DISPATCH_FILTER_ALLOW 或 SYSCALL_DISPATCH_FILTER_BLOCK。任何其他值都应导致程序通过 SIGSYS 终止。

此外,任务的 Syscall User Dispatch 配置可以通过 PTRACE_(GET|SET)_SYSCALL_USER_DISPATCH_CONFIG ptrace 请求进行窥探(peek)和修改(poke)。这对检查点/恢复(checkpoint/restart)软件非常有用。

安全注意事项

Syscall User Dispatch 为兼容层提供了快速捕获由应用程序非原生部分发出的系统调用的功能,同时不影响进程的 Linux 原生区域。它不是一种用于沙盒化系统调用的机制,也不应被视为安全机制,因为恶意应用程序通过在执行系统调用之前跳转到允许的分发器区域,或者发现地址并修改选择器值来破坏该机制是轻而易举的。如果用例需要任何形式的安全沙盒,则应改用 Seccomp。

现有进程的任何 fork 或 exec 操作都会将此机制重置为 PR_SYS_DISPATCH_OFF。