- 网络安全
- 开发工具
- 质量保障
【免费下载链接】syzkaller
syzkaller is an unsupervised coverage-guided kernel fuzzer
本文面向 syzkaller 内核模糊测试器的开发与研究者,系统讲解"伪系统调用"(pseudo-syscalls)的设计动机、适用边界与完整接入流程。通过本文,读者将掌握在executor/common*.h中编写syz_前缀 C 函数、在linuxSyscallChecks中注册可用性检查、生成描述并配套sys/OS/test测试的完整方法,同时理解为什么官方在多数场景下不推荐使用伪系统调用。原文(docs/pseudo_syscalls.md)及其中文翻译(docs/translations/zh_CN/pseudo_syscalls.md)为本篇的主体骨架,仓库内执行器、系统调用描述与测试源码作为实现层面的佐证。
什么是伪系统调用
除了常规系统调用之外,系统调用描述文件(即 syzlang 描述的*.txt文件)中还可以包含伪系统调用。伪系统调用本质上是定义在执行器(executor)中的 C 函数:当测试程序使用某个伪系统调用时,执行器会在生成的 C 程序中直接内联该伪系统调用的函数代码。
伪系统调用的存在,使测试程序可以携带一段"执行特定操作的特定代码块",而不是只能逐条调用裸系统调用。同时,它们也经常被用作对底层原始系统调用的、更利于模糊测试的包装器(wrapper)。典型例子见下文 io_uring 伪系统调用:它把io_uring_setup与一系列mmap操作打包成一个函数,让模糊器不需要自行拼出正确的 mmap 长度参数。
为什么官方不推荐使用
使用伪系统调用通常是不被建议的,原因在于它破坏了声明式描述(syzlang)的所有优势:
- 声明性:系统调用描述是纯声明式的,行为一目了然;
- 简洁性:描述文件简洁,无冗余 C 代码;
- 模糊器全方面控制:所有参数、资源、依赖关系都由模糊器按描述生成,而非埋在 C 代码里;
- 全局逻辑改进的可能性:例如资源类型、校验和、长度字段等可以统一优化;
- 静态检查:描述可以被编译器静态检查;
- 更少的 bug:描述越少,出错面越小。
此外,伪系统调用还带来了维护负担、不可复用性(每个目标系统都要单独维护),并会使C 重现器(C reproducers)更长(重现代码必须包含整个伪系统调用函数体)。
不过,syzlang 的表达能力并不足以覆盖所有可能场景,因此是否使用伪系统调用需要根据具体情况逐一权衡:额外收益有多大、代码量有多少、是否有扩展 syzlang 本身来覆盖该场景的可能性等。
如何向执行器添加伪系统调用
第一步:确定代码放置位置
首先,考虑伪系统调用的适用范围以及它涉及的系统和子系统。执行器包含一组固定的 C 头文件executor/common*.h,伪系统调用的代码全部存放在这些头文件中。在创建新文件之前,先检查新伪系统调用是否可以放进现有的某个文件。例如,如果新的伪系统调用是 Linux 特有的,那么 executor/common_linux.h 就是合适的放置位置;同理,BSD 系列头文件、Fuchsia、Windows 等平台各有对应的common_*.h。仓库中实际的头文件集合包括common.h、common_linux.h、common_bsd.h、common_fuchsia.h、common_windows.h、common_openbsd.h等(见 executor 目录)。
第二步:编写函数体
一个真正的伪系统调用函数可能看起来像下面这样:
#if SYZ_EXECUTOR || __NR_syz_mycall /* Add all the necessary #include and #define headers */ static long syz_mycall(volatile long a0, volatile long a1) { /* Function body */ } #endif编写时需要满足以下硬性要求:
- 函数名必须以
syz_开头; - 参数个数可变,但每个参数的类型必须是
volatile long,返回类型必须是long; - 确保函数满足所有编译要求,能够成功编译;
- 用条件编译宏
#if SYZ_EXECUTOR || __NR_syz_mycall包裹,其中__NR_syz_mycall由make generate生成,用于保证仅在执行器构建或描述引用该调用时编译这段代码。
为什么参数和返回类型必须是long?因为伪系统调用最终会被转换成接受long的函数指针来调用,使用long可以避免潜在的调用约定问题。
为什么需要volatile?这一点很有意思:许多 libc 函数都用各种参数约束做了注解(例如"此参数不应为NULL"、"该参数必须是有效的文件描述符")。C 重现器可能用常量参数调用这些函数,而编译器可能发现某些约束被违反(比如把NULL传给non-NULL参数,或者把-1作为文件描述符传入),从而产生错误或警告。volatile能防止这类误报。
第三步:在linuxSyscallChecks中注册可用性
为了正确处理伪系统调用,需要更新 pkg/vminfo/linux_syscalls.go 中的linuxSyscallChecks,为该系统调用添加特定情况,并在必要时启用它。
linuxSyscallChecks是一个从系统调用名映射到检查函数的表,其底层调用链位于 pkg/vminfo/linux_syscalls.go 的(linux) syscallCheck:它先查表取出对应检查函数,若表中不存在则回退到默认的"实际执行系统调用探测"逻辑(在目标 VM 中运行一个只关心ENOSYS的测试程序来探测支持情况);若检查函数返回非空字符串,则说明该系统调用在当前环境不受支持。
如果你想无条件启用某个伪系统调用,直接为它使用alwaysSupported即可——该函数在 pkg/vminfo/syscalls.go 中实现,简单返回空字符串表示"总是支持"。仓库中实际大量使用了这一模式,例如:
"syz_io_uring_setup": alwaysSupported, "syz_io_uring_submit": alwaysSupported, "syz_io_uring_complete": alwaysSupported, "syz_clone": alwaysSupported, "syz_clone3": alwaysSupported, "syz_open_pts": alwaysSupported, "syz_execute_func": alwaysSupported, "syz_create_resource": alwaysSupported,完整列表见 pkg/vminfo/linux_syscalls.go。如果伪系统调用只在特定硬件或内核特性存在时才可用,则需要像syz_kvm_setup_cpu(linuxSyzKvmSupported)、syz_usb_connect(linuxCheckUSBEmulation)、syz_mount_image(linuxSyzMountImageSupported)那样编写条件检查函数,返回""表示支持、返回非空字符串表示不支持的原因。
第四步:运行make generate并编写描述
最后,在仓库根目录运行make generate,重新生成执行器代码与系统调用描述所需的常量(包括__NR_syz_mycall等)。此后,就可以在系统调用描述文件中像使用普通系统调用一样使用它了:
syz_mycall(arg0 pid, arg1 const[0])以仓库中的 io_uring 伪系统调用为例,其描述位于 sys/linux/io_uring.txt:
syz_io_uring_setup(entries int32[1:IORING_MAX_ENTRIES], params ptr[inout, io_uring_params], ring_params_ptr ptr[out, ring_params_ptr], ring_ptr ptr[out, ring_ptr], sqes_ptr ptr[out, sqes_ptr]) fd_io_uring对应的 C 实现在 executor/common_linux.h 中,它完成了io_uring_setup调用、根据内核返回的sq_off/cq_off偏移计算环形缓冲区大小、执行两次mmap映射 SQ/CQ 环形区与 SQEs,并初始化 SQ array。正如描述文件中的注释所说:io_uring_setup之后紧跟着用mmap映射 ring 和 sqes,模糊器很难用自己生成的 mmap 长度参数拼出正确程序,因此这个包装器保证了长度的正确计算——这正是"伪系统调用作为更测试友好的原始系统调用包装器"的典型例证。每个参数用注释与 syzlang 描述一一对应,例如:
// syzlang: syz_io_uring_setup(entries int32[1:IORING_MAX_ENTRIES], params ptr[inout, io_uring_params], ring_params_ptr ptr[out, ring_params_ptr], ring_ptr ptr[out, ring_ptr], sqes_ptr ptr[out, sqes_ptr]) fd_io_uring // C: syz_io_uring_setup(uint32 entries, struct io_uring_params* setup_params, void** ring_params_ptr_out, void** ring_ptr_out, void** sqes_ptr_out) // returns uint32 fd_io_uring同一个伪系统调用也可以注册多个变体(以$后缀区分),例如syz_io_uring_modify_offsets$generic与syz_io_uring_modify_offsets$flags(见 sys/linux/io_uring.txt),分别用不同的 flags 集合约束偏移量取值范围,帮助模糊器更容易生成合法程序。
外部依赖限制
伪系统调用的实现不能使用任何外部库或外部头文件,除了一些最基本、最标准的库(例如<unistd.h>和<sys/mman.h>)。特别是:
- 不能依赖附加软件包安装的库或头文件;
- 不能依赖最近新增内核子系统的头文件。
外部依赖已被证明是脆弱的,很容易导致构建中断,因为模糊测试器上任何构建与运行、以及任何 C 重现器,都需要所有依赖项。举例来说,软件包或头文件可能在某些发行版上缺失、命名不同、版本错误、损坏,或与其他头文件冲突。不幸的是,目前没有办法可靠地为 C 程序指定这类依赖和要求。
因此,如果伪系统调用需要某些结构体、常量或辅助函数的定义,应尽可能简短地在执行器代码中自行描述(这些定义会成为 C 重现器的一部分)。从仓库实现中可以看到这一原则的实际应用:例如 executor/common_linux.h 中 io_uring 实现仅引入<sys/mman.h>、<unistd.h>,并自行#define IORING_OFF_SQ_RING 0ULL、IORING_OFF_SQES 0x10000000ULL,而非依赖外部头文件。
测试要求
每个新的伪系统调用应该在sys/OS/test中至少有一个测试(OS替换为目标系统名,如sys/linux/test)。测试本身就是一段"检查系统调用返回值"的程序。具体要求如下:
- 至少有一个测试包含使用该伪系统调用的**"主要成功场景"**(main successful scenario);
- 这类测试至关重要,因为它们能确保伪系统调用的代码不包含"愚蠢"的错误(例如每次都因 NULL 解引用而崩溃),确保模糊器能够组合出成功场景(伪系统调用与周围描述的搭配),并保证该功能在未来持续可用。
仓库中的 sys/linux/test/io_uring 是一个很好的范例,它完整走了一遍"创建实例 → 修改标志 → 提交 SQE → 进入内核等待完成 → 取出 fd → 关闭"的成功路径:
# Create an io_uring instance r0 = syz_io_uring_setup(0xF00, ...) # Set IORING_CQ_EVENTFD_DISABLED. Has no side-effect for the test, # only tests syz_io_uring_modify_offsets(). syz_io_uring_modify_offsets$flags(r1, r2, 0x38, 0x1) # Write an openat2 operation to the submission queue syz_io_uring_submit(r1, r2, r3, &AUTO=@IORING_OP_OPENAT2={...}) # Notify the kernel about the submission and wait until completion io_uring_enter(r0, 0x1, 0x1, 0x1, 0x0, 0x0) # Get the resulting fd from the completion queue r4 = syz_io_uring_complete(r1, r2) # Close the file close(r4)同类测试还有 sys/linux/test/io_uring_large(大容量 SQ 配置场景)以及基于同一组伪系统调用的 sys/linux/test/ublk。这些测试文件也印证了文档中"测试只是检查系统调用返回值的程序"的说法:它们用#注释描述每个步骤的意图,并用返回值变量(如r0、r4)串联后续调用,任何一步返回异常值都会导致测试失败。
关于这些测试的详细机制(例如它们如何被执行、如何检查返回值),请参见 系统调用描述的测试 一节。
完整接入流程速查
综合以上内容,向 syzkaller 添加一个伪系统调用的完整步骤为:
- 评估必要性:确认 syzlang 无法表达该场景,且额外收益大于维护成本与 C 重现器膨胀的代价。
- 选择位置:根据目标系统,把 C 实现放入合适的
executor/common*.h(Linux 放 executor/common_linux.h)。 - 编写函数:以
syz_开头,参数volatile long、返回long,用#if SYZ_EXECUTOR || __NR_syz_xxx包裹,只依赖标准库头文件。 - 注册可用性:在 pkg/vminfo/linux_syscalls.go 的
linuxSyscallChecks中为该调用添加条目;无条件支持用alwaysSupported,有条件支持则编写专用检查函数。 - 生成与描述:运行
make generate,然后在sys/linux/*.txt描述文件中按 syzlang 语法声明该伪系统调用及其参数/资源类型。 - 编写测试:在
sys/linux/test下新增至少一个包含"主要成功场景"的测试文件,确保调用链可稳定运行。 - 验证:确保执行器与描述均能编译、测试通过,且实现无外部依赖。
按此流程新增的伪系统调用即可被模糊器当作普通系统调用参与程序生成与变异,同时具备经得起长期运行的测试保障。
- 网络安全
- 开发工具
- 质量保障
【免费下载链接】syzkaller
syzkaller is an unsupervised coverage-guided kernel fuzzer
相关推荐
syzkaller 伪系统调用(Pseudo-syscalls)完全指南:从 executor 实现到 syzlang 描述与测试
syzkaller 伪系统调用(Pseudo syscalls)完全指南:从 executor 实现到 syzlang 描述与测试 伪系统调用(pseudo s
网络安全开发工具质量保障syzkaller KVM 模糊测试实战:x86/amd64 SYZOS 虚拟化约束与伪系统调用编写指南
syzkaller KVM 模糊测试实战:x86/amd64 SYZOS 虚拟化约束与伪系统调用编写指南 本文是 syzkaller 仓库中面向 LLM Age
网络安全开发工具质量保障syzkaller 贡献指南:从系统调用描述到 Pull Request 的完整实战流程
syzkaller 贡献指南:从系统调用描述到 Pull Request 的完整实战流程 本篇指南围绕 syzkaller 官方贡献文档展开,系统梳理了一个外部
网络安全开发工具质量保障
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考