KernelGpt的部分尝试

让规则跟着子系统走:四类 Linux 子系统的 Syzkaller 规则演进记录

一条 Syzkaller 规则从"能编译"到"真的在跑",中间不是一个步骤,而是四级台阶。

每级台阶卡住的地方,由子系统的接口形态决定 ------ 不是由你的耐心决定。

本文按子系统组织:Bluetooth、内存管理(userfaultfd)、进程文件系统(pidfs),

以及还没动手的设备类。每次尝试都放在对应子系统的演进链里当证据。

内核:7.3.0-rc7-00032-g0fcb05b72344(2026-10-14)。


零、先立一把尺子:规则的四级台阶

后面的所有讨论都需要这把尺子,否则"规则跑通了"这句话毫无信息量。

级别 含义 怎么验证 常见的假通过
L0 描述编译通过 make descriptions 不报错 规则里没有符号常量时,include 不会被真正编译
L1 通过机器探测,调用被允许生成 syscalls : N/M 里 N 合理 N=1 也是"已启用"
L2 真实命令值真的被执行到 corpus 里搜得到 0xc058ff0b 这类具体值 看到 0x0 指针就以为没执行
L3 到达目标状态 fd 有效、对象已注册、前置协商完成 调用返回 0 但对象没进状态机
L4 覆盖饱和 覆盖率曲线走平 跑太短就以为饱和

L1 和 L2 最容易混,下面单独展开------它们是整篇文章里出现频率最高的两个卡点。

L1 具体是什么

一句话:Syzkaller 认定"这个调用可以出现在生成出来的程序里"。

syzkaller.sh 启动 syz-manager 后,manager 会先做一次 machine check :把

enabled_calls 里的每个调用用零参数 跑一遍,看内核是否认它。返回"不支持"类错误的

(ENOSYS、EPROTONOSUPPORT、EAFNOSUPPORT、ENODEV 等)会被标记禁用;

然后对资源做传递闭包------如果某个资源的创建者全被禁用了,依赖它的调用也被禁用。

所以日志里会出现两段,含义完全不同:

复制代码
# 段一:自己跑不通
disabled the following syscalls:
syz_init_net_socket$KGPT_bt_iso  : syz_init_net_socket$KGPT_bt_iso(0x1f, 0x5, 0x8)
                                   failed: protocol not supported

# 段二:自己被连累(方括号里是"能创建这个资源的调用",它们都被禁了)
transitively disabled the following syscalls (missing resource [creating syscalls]):
ioctl$KGPT_PIDFD_GET_INFO        : fd_pidfd [openat$pidfd openat$thread_pidfd
                                            syz_clone3 syz_pidfd_open]

最后一行 syscalls : N/M 是结果:N = 还剩多少调用可用,M = 这棵树里描述了多少调用。

读这一行有几个要点:

  • M 会变 。注入的规则不同,M 就不同(我们三次运行分别是 1/8376、14/8367、24/8375)。
    只有 N 在同一套规则下才有可比性。
  • N=1 也是"已启用" 。ISO 那轮 N=1(只剩 readlink),日志看起来一切正常,
    实际上我们的 16 个调用全没了。
  • N 少了不一定是规则写错。可能是资源链断了(段二),这时要看方括号里列出的创建者。

L1 通过 ≠ 调用会成功。 探测用的是零参数:ioctl(fd, cmd, 0)、socket(0,0,0)。

它只证明"内核对这个调用号/命令字有反应",不证明"参数对了就能进状态机"。

这正是 L1 和 L2 必须分开的原因。

另外,L1 失败有两种完全不同性质的原因,处理方式也不同:

性质 例子 能不能靠改规则解决
参数写错 proto const[0](L2CAP)配 SOCK_STREAM 能,改成 BTPROTO_RFCOMM
内核有门 ISO 未注册(要 mgmt 命令先开) 不能,得改 guest 启动流程

L2 具体是什么

一句话:一个真实携带目标命令值的程序,真的在 guest 里执行过了。

L1 只保证"调用可以被生成",但从"可以被生成"到"真的打到目标 handler"之间,还有三道坎:

坎一:const 参数也会被变异。

Syzkaller 会变异常量参数。所以会出现这种程序:

复制代码
ioctl$KGPT_PIDFD_GET_INFO(r0, 0x5452, 0x0)

0x5452 不是 PIDFD_GET_INFO(0xc058ff0b),早就落到 default: 分支返回

ENOIOCTLCMD 了。调用"执行了",但 pidfs_ioctl 里我们关心的代码一行都没进。

坎二:fd 可能是无效值。

复制代码
ioctl$KGPT_PIDFD_GET_NET_NAMESPACE(0xffffffffffffffff, 0xff04)

命令字完全正确,但 fd 是 -1 → EBADF。同样是"执行了"。

坎三:前置状态没到位。

uffd 的 SET_MODE 前面没有 UFFDIO_API,或者 enable=RWP_ASYNC 但 ctx 没协商过

UFFD_FEATURE_RWP ------ 都在第一个 return -EINVAL 就结束了。

所以 L2 的验证方法是搜真实数值,而不是搜符号名(程序里存的是数字):

bash 复制代码
default-tmp/syzkaller/bin/syz-db unpack workdir-default-*/corpus.db /tmp/out
grep -l '0xff04'     /tmp/out/*   # PIDFD_GET_NET_NAMESPACE,18 个程序
grep -l '0xc058ff0b' /tmp/out/*   # PIDFD_GET_INFO,9 个程序
grep -l '0x4010aa0a' /tmp/out/*   # UFFDIO_SET_MODE

这里有个必须记住的不对称:

搜到 = 确定发生过;搜不到 ≠ 没发生过。

因为 corpus 是最小化 后的结果,最小化器会把不影响覆盖率的参数换掉、甚至丢掉整个程序。

我就在这上面判错过一次:看到 ioctl$KGPT_PIDFD_GET_INFO(r0, 0xc058ff0b, 0x0) 里指针是

0x0,判断"没跑起来"------实际上指针是被最小化成 0 的,参数在 fuzzing 时是真实地址。

所以 0x0 指针既不能证明没执行,也不能证明执行了。 判断 L2 只能看两样东西:

真实命令值是否出现、覆盖率是否在引入这条规则后增长。最严格的做法是差分 :

同一套配置跑一遍有规则、一遍没规则,比较覆盖率------这一步我们没系统做,是这一轮的欠缺。

L3 / L4 为什么也要分开

L2 只说明"命令字打到了 handler",L3 要求对象进入目标状态:

子系统 L2 的样子 L3 的样子
uffd UFFDIO_REGISTER 被调用 区间真的注册成功,且 SET_MODE 打的是同一个 ctx
pidfs PIDFD_GET_* 被调用 fd 是从 openat("/proc/self") 拿到的有效 pidfs fd

L4 则是饱和判定。没有 L4,就会把"跑了 15 分钟还在涨"和"3 分钟就平了"当成同一件事。

这把尺子到底有什么用

它把"失败了"这句话变成可定位的结论:

  • 卡在 L0 → 规则本身写错了(常量、尺寸、重名)
  • 卡在 L1 → 资源或门的问题(要么补 --extra-calls,要么改 guest)
  • 卡在 L2 → 调用在跑但没打中(const 变异、fd 无效、缺前置状态)
  • 卡在 L3 → 状态进不去(seed/状态机的问题)
  • 到 L4 → 这个面打透了,该换目标

这次三个做过的子系统分别停在 L1 之前 (Bluetooth)、L4 (uffd)、L4 (pidfs),

每一个卡点都对应到一个具体的接口特性。


一、Bluetooth(net/bluetooth):被"门"封顶的子系统

1.1 这个子系统的接口形态

AF_BLUETOOTH 是单一家族 + 多个协议号的结构:

c 复制代码
#define BTPROTO_L2CAP   0
#define BTPROTO_HCI     1
#define BTPROTO_SCO     2
#define BTPROTO_RFCOMM  3
...
#define BTPROTO_ISO     8

入口都是 socket(AF_BLUETOOTH, type, proto),但三个参数互相约束 :

SOCK_STREAM 只能配 RFCOMM,SOCK_SEQPACKET 才能配 ISO。而且有两道额外的门。

1.2 规则演进链条

版本 规则写法 到了哪级 证据
v1 socket$KGPT_rfcomm(domain const[AF_BLUETOOTH], type const[SOCK_STREAM], proto const[0]) L0 编译通过,机器探测失败
v2 proto const[BTPROTO_RFCOMM](3) L0 同上,错误码从 EPROTONOSUPPORT 不变
v3 syz_init_net_socket$KGPT_rfcomm(...) L0→卡在 L1 那次运行里所有 BT 族仍报 EAFNOSUPPORT
v4(ISO) syz_init_net_socket$KGPT_bt_iso(..., proto const[BTPROTO_ISO]) + 正确类型 L0,L1 前失败 syscalls : 1/8376

1.3 三道门,三个不同的错误码

这不是一个"参数调错了"的故事。v1 到 v3 的每次修改都在对付不同的 一道门,

而错误码是唯一的线索。

门一:协议号与 socket 类型必须匹配。

v1 里 proto const[0] 是 L2CAP,配 SOCK_STREAM 必然 EPROTONOSUPPORT。

这个错误看起来像"协议族没注册",实际上只是参数组合非法。

门二:网络命名空间。

c 复制代码
/* net/bluetooth/af_bluetooth.c:118 */
static int bt_sock_create(struct net *net, struct socket *sock, int proto, int kern)
{
        if (net != &init_net)
                return -EAFNOSUPPORT;

错误码变成 EAFNOSUPPORT(address family not supported)------ 注意它不是 family 不存在,

而是"这个 netns 里不许"。这就是 syz_init_net_socket$ 存在的原因。

这里有个值得记下来的教训:v3 那次改名其实从来没被验证过 。那次运行里所有 BT 族报的

还是 EAFNOSUPPORT(因为规则集里用的是未改名的那批),直到 ISO 才第一次真正过了 netns

检查 ------ 然后撞上第三道门。

门三:实验特性开关。

ISO 修好所有参数、编译通过、16 个调用全进了 enabled_calls 之后:

复制代码
syscalls : 1/8376
syz_init_net_socket$KGPT_bt_iso(0x1f, 0x5, 0x8) failed: protocol not supported

参数完全正确:0x1f=AF_BLUETOOTH、0x5=SOCK_SEQPACKET、0x8=BTPROTO_ISO。

但 EPROTONOSUPPORT 在这里的含义是 bt_proto[8] == NULL ------ 协议族从来没被注册。

iso_init() 全树只有两个调用点:

复制代码
net/bluetooth/af_bluetooth.c:992   iso_exit()
net/bluetooth/mgmt.c:5099          iso_init()   ← MGMT_OP_SET_EXP_FEATURE 处理里

ISO socket 是实验特性,要先发一条 HCI 管理命令才会注册:

复制代码
socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI)
bind(hci_dev = HCI_DEV_NONE 0xffff, hci_channel = HCI_CHANNEL_CONTROL 3)
sendmsg: MGMT_HDR{opcode = 0x004a, index = 0xffff, len = 17}
       + {uuid = 3ee0b4fd-ddd6-8598-6a49-e00588f1ba6f, param = 0x01}

而且这个门从 2022-07-22 (ccf74f2390d6 Bluetooth: Add BTPROTO_ISO socket type)就有,

不是 7.3 的新设计。iso.c 在 2026 年被改了 36 次,改的是 BIG/BIS 广播之类的功能,

跟可达性无关。

这里的教训是选目标层面的 :按 churn 排序会觉得 iso.c 很香(36 次改动 + 上游零覆盖),

但 churn 度量的是"代码在动",不是"代码能被跑到"。

1.4 为什么 seed 救不了

Syzkaller 的机器探测发生在 seed 执行之前 。探测失败 → 调用被禁用 → 依赖它的调用被

传递性禁用:

复制代码
transitively disabled the following syscalls:
bind$KGPT_iso_sock_ops        : sock_bt_iso [syz_init_net_socket$KGPT_bt_iso]
connect$KGPT_iso_sock_ops     : sock_bt_iso [syz_init_net_socket$KGPT_bt_iso]
setsockopt$KGPT_BT_ISO_QOS    : sock_bt_iso [syz_init_net_socket$KGPT_bt_iso]
...

seed 里写什么都一样,因为这些调用已经不在程序生成器的候选集里了。

可达性问题是 seed 层解决不了的,必须在规则层或 guest 层解决。

1.5 这个子系统教会规则写作的三件事

  1. proto 常量必须显式写对,且要和 type 匹配
  2. bt_* 一族一律用 syz_init_net_socket$
  3. 类型不能重定义(bdaddr_t、bdaddr_type 上游已有),但 bind() 要求
    bdaddr_type_is_le(),所以 iso_bdaddr_type 必须建成只含 0x01/0x02 的枚举,
    不能用裸 int8

另外还逼出了一组尺寸,必须用编译器量:

复制代码
sizeof(sockaddr_iso)    = 10    ← 不是 9:u16+6+u8=9,对齐 2 补到 10
sizeof(sockaddr_iso_bc) = 40
bind bc 要求 addr_len   = 50    ← 10 + 40
sizeof(bt_iso_qos)      = 60    ← union,取最大成员(ucast 32 / bcast 60)

差一字节的后果很具体:传 9 → EINVAL;传 11 及以上 → 静默 走进 broadcast 分支,

然后被 addr_len != 50 拒掉。两种都是"看起来跑了、其实什么都没发生"。

1.6 结论

这个子系统的演进被门封顶在 L1 之前 。规则留在

spec-gen/eval-input/iso-7.3/,将来若在 guest 启动脚本里补上那条 mgmt 命令,

可以立刻复用。


二、内存管理(mm/userfaultfd):把 fd 从系统调用手里接过来

2.1 这个子系统的接口形态

和其它子系统最大的不同:没有设备节点、没有 socket 族 ,fd 来自 userfaultfd() 系统调用。

三个特性叠在一起,使它成为最难写的规则:

  1. fd 由系统调用创建,而那个系统调用上游已经描述过了 → 规则里不能重新声明
  2. 强状态机 :UFFDIO_API 协商 → UFFDIO_REGISTER 绑 VMA → UFFDIO_SET_MODE 才有效
  3. 参数里有 vma64:指向一个真实 mmap 出来的区域,不是普通指针

2.2 为什么选它

7.3 的 UFFDIO_SET_MODE 和 UFFDIO_RWPROTECT 是 2026-08-04 新增的,

而且上游 uffd.txt 里恰好缺这两条(其余 UFFDIO_* 全有)。

userfaultfd_set_mode() 的逻辑正好是边界密集的:

c 复制代码
if (mode.enable & mode.disable)                     return -EINVAL;   // 互斥
if ((mode.enable | mode.disable) & ~toggleable)     return -EINVAL;   // 掩码
if ((mode.enable & RWP_ASYNC) && !(features & RWP)) return -EINVAL;   // 前置协商
...
for_each_vma(vmi, vma)
        if (vma->vm_userfaultfd_ctx.ctx == ctx)
                vma_start_write(vma);        // 排空在飞的 page fault
WRITE_ONCE(ctx->features, (ctx->features | enable) & ~disable);

2.3 规则演进链条

版本 变化 级别 证据
v1 只加 UFFDIO_SET_MODE + UFFDIO_RWPROTECT L0 ---
v1 补 "resources": {} L0 否则 output_spec() 抛 KeyError: 'socket'
v2 加 --extra-calls 'userfaultfd,mmap' L1(勉强) syscalls : 14/8367,但程序里全是空指针
v3 再加 ioctl$UFFDIO_API,ioctl$UFFDIO_REGISTER,ioctl$UFFDIO_UNREGISTER L2→L3 API+REGISTER 同程序 0 → 104
v4 热启动 30 min L4 coverage 10145,末段走平

2.4 卡点一:L1 通过不等于跑到了(--extra-calls 的由来)

v1/v2 的第一次 10 分钟跑,数字看起来正常:

复制代码
coverage 8569   corpus 254   crashes 0

但日志里有一行被我第一遍忽略的:

复制代码
WARNING generate_seeds:345 - seed generation failed for gpt4_userfaultfd_fops...:
no programs generated

看程序形态才明白发生了什么:

复制代码
ioctl$KGPT_UFFDIO_REGISTER(r0, 0xc020aa00, 0x0)
ioctl$KGPT_UFFDIO_SET_MODE(r1, 0x4010aa0a, 0x0)   ← 前面也没有 UFFDIO_API
ioctl$KGPT_UFFDIO_API(r2, 0xc018aa3f, &(0x7f0000000180))   ← 空结构体,api != 0xAA

根因:fd_uffd 来自上游 userfaultfd(),规则里不能重新声明(跨文件重名是硬错误),

而 --merge 的默认 enabled_calls 只有 read*/write*/close*。fuzzer 拿不到可用的 fd。

补上之后立刻变化:

指标 v2 v3
coverage 8569 9210
含 UFFDIO_API 的程序 少 152
含 UFFDIO_REGISTER 的程序 15 119
API + REGISTER 同一程序 0 104

程序里开始出现真实的 mmap 区域:

复制代码
r1 = userfaultfd(0x80801)
ioctl$UFFDIO_API(r1, 0xc018aa3f, &(0x7f0000000140)={0xaa, 0x140})
ioctl$UFFDIO_REGISTER(r1, 0xc020aa00,
    &(0x7f0000000180)={{&(0x7f0000004000/0x2000)=nil, 0x2000}, 0x1})

&(0x7f0000004000/0x2000)=nil 是 Syzkaller 自己建的 mmap 区域,0x1 是

UFFDIO_REGISTER_MODE_MISSING。这才叫 L2。

2.5 卡点二:手写 seed 想直接跳到 L3,失败了

gen_seeds 对这类规则产不出程序(没有 open/socket 前驱),所以手写了一条链:

复制代码
userfaultfd → API(RWP|RWP_ASYNC) → mmap → REGISTER(RWP)
            → SET_MODE(enable=RWP_ASYNC) → RWPROTECT(RWP)

syz-db merge 接受,syz-db print 能看到完整程序。跑完却被最小化器丢了 ------

用 0x60000(RWP|RWP_ASYNC 的特征值)搜索 corpus,0 个程序。

第二版改成复制一个已存活程序的地址布局(避免 MAP_FIXED 撞车),API 阶段只协商 RWP,

RWP_ASYNC 留给 SET_MODE。同样被丢(0xaa, 0x20000 签名 0 个)。

结论:这类程序没带来新覆盖,最小化器有权丢弃。

seed 只值一次验证的代价 ------ 跑完先搜特征值确认它活着,再谈它有没有用。

2.6 卡点三:一次看起来像 bug 的 crash

改造前的一轮出现过唯一一次 crash:

复制代码
17:20:59  VM 1: crash: no output from test machine
report0 : 空

诱惑很大 ------ 我们刚打的正是 mmap_write_lock + vma_start_write 排空逻辑,

VM DIAGNOSIS 的寄存器快照还正好落在:

复制代码
RIP=ffffffff819c06d0 → lock_release   kernel/locking/lockdep.c:5951

但证据不支持:

检查 结果
BUG: / WARNING: / Kernel panic 0
oom-kill / Killed process 0
blocked for more than(hung task) 0
FAULT_INJECTION: forcing a failure 大量(末段全是 failslab 的 dump_stack)
ERROR: Out of memory at tomoyo_realpath_from_path 144

那些 Call Trace 是 fail-injection 自己的 dump_stack(),不是崩溃栈。

单变量对照(同 corpus、同 6144 MiB、只关故障注入):

开故障注入 关故障注入
结果 VM 失联 15 min 正常结束
FAULT_INJECTION 大量 0
crash 1 0
coverage 9717(被截断,不可比) 9818

根因:--merge 基础集里的 write* 会顺带启用 write$fault_inject,而它能往

/sys/kernel/debug/failslab/probability 写 "100" ------ 每次 slab 分配都失败。

判据一句话 :没有任何内核自诊断输出 + 大量 FAULT_INJECTION = 基础设施故障。

两轮的 coverage 不可比 (崩溃那轮 VM 5 分钟就死了,剩下 9 分钟靠单台机器跑)。

能比的只有「有没有 crash」------1 比 0。

2.7 这个子系统教会的

  • fd 来自上游系统调用时,必须用 --extra-calls 显式启用那个系统调用
  • 状态机顺序(API→REGISTER→SET_MODE)不能指望 fuzzer 自己凑,gen_seeds 也凑不出
  • vma64 参数意味着必须让 mmap 参与
  • 故障注入和长跑不可混在一起

三、进程文件系统(fs/pidfs):目录 fd 上一组"无参数"ioctl

3.1 这个子系统的接口形态

pidfs 是 procfs 里进程目录被拆出来的新文件系统。它的 fd 是目录 fd ,

ioctl 分两类:

c 复制代码
#define PIDFS_IOCTL_MAGIC 0xFF
#define PIDFD_GET_CGROUP_NAMESPACE            _IO(0xFF, 1)
...
#define PIDFD_GET_UTS_NAMESPACE               _IO(0xFF, 10)
#define PIDFD_GET_INFO                        _IOWR(0xFF, 11, struct pidfd_info)

10 个 namespace ioctl 是 _IO(无参数 ),作用是返回一个新的 nsfs fd ;

PIDFD_GET_INFO 是 _IOWR,带一个 size-versioned 的 88 字节结构体。

选它的理由很直接:13 条命令上游一条都没有,而且不需要 mmap,避开了 uffd 那套内存压力。

3.2 规则演进链条

版本 变化 级别 证据
v1 12 条 ioctl(10 个 namespace + INFO + FS_IOC_GETVERSION) L0 编译通过
v1 只加 --extra-calls 'syz_pidfd_open' 卡在 L1 syscalls : 1/8375
v2 改成 --extra-calls 'openat$pidfd,...' L2 syscalls : 24/8375,corpus : 24 (24 seeds)
v3 跑 15 min L4 coverage 6030,约 3 分钟饱和

3.3 卡点:fd_pidfd 的所有创建者都没启用

v1 的冒烟测试里,12 个调用全被传递性禁用:

复制代码
ioctl$KGPT_PIDFD_GET_INFO : fd_pidfd [openat$pidfd openat$thread_pidfd
                                       syz_clone3 syz_pidfd_open]

方括号里是"能创建这个资源的系统调用",而它们全都没被启用。

syz_pidfd_open 需要 pid 资源,pid 又来自 syz_clone*;链条太长。

正确解法是找到最短路径:openat$pidfd 直接从 /proc/self 拿 pidfs fd:

c 复制代码
openat$pidfd(fd const[AT_FDCWD], file ptr[in, string["/proc/self"]],
             flags flags[open_flags], mode const[0]) fd_pidfd

这是这类子系统的通用技巧:找资源的最短创建路径,不要顺着依赖链往上追。

3.4 到达 L2 之后

复制代码
openat$pidfd 建出 fd 的程序    43 / 167
PIDFD_GET_NET_NAMESPACE 真实 cmd 0xff04      18 个程序
PIDFD_GET_INFO 真实 cmd 0xc058ff0b            9 个程序

0xc058ff0b = _IOWR(0xFF, 11, 88),说明 88 字节的结构体布局算对了

(PIDFD_INFO_SIZE_VER3)。

3.5 为什么 3 分钟就饱和

覆盖率轨迹:0 → 4205 → 5015 → 5769 → ... → 6021 → 6030。

pidfs_ioctl 的分支本来就少:10 个 namespace ioctl 共用一套逻辑

(get_pid_task → ptrace_may_access → get_nsproxy → ns_ref_get → 返回 fd),

只有 switch 的 case 不同;GET_INFO 就是一次拷贝。

分支数决定覆盖率上限,而覆盖率几分钟就能摸到那个上限。 加时长买不到新东西。

3.6 一个误判

corpus 里看到 ioctl$KGPT_PIDFD_GET_INFO(r0, 0xc058ff0b, 0x0),我判断"指针是 NULL,

所以没跑起来"。

错的。 corpus 最小化会在覆盖率不变时把指针参数换成 0。判断有没有执行要看覆盖率增长

和真实命令值,不能看保留下来的程序长什么样。


四、还没动手的设备类子系统

按 churn × 上游盲区排序,剩下的候选集中在一类形态上:字符设备 / virt 设备。

目标 命令数 上游覆盖 最新的命令 fd 来源
vduse_ctrl_fops 6 零 VDUSE_SET/GET_FEATURES(2026-08-19) /dev/vduse/control
eventpoll_fops 2 零 EPIOCSPARAMS(2024-02) epoll_create1()

这类子系统的特征是 L1 容易过(设备节点存在就能 open),难点转移到"设备需要什么样的前置状态":

vduse 需要能创建虚拟 vDPA 设备,fuse 需要 mount + 用户态服务端。

它们的规则演进大概率会停在 L2/L3 之间 ------ 调用能执行,但设备状态进不去。这是下一阶段要验证的。


五、四个子系统放在一起看

维度 Bluetooth userfaultfd pidfs 设备类(待做)
fd 来源 socket() userfaultfd() openat("/proc/self") openat("/dev/xxx")
需要 init_net 是 否 否 否
需要 mmap 否 是(vma64) 否 否
前置状态机 无 三级 无 设备初始化
尺寸敏感 强 (addr_len 精确值) 中 中(size-version) 未知
有"门" 是(实验特性) 否 否 未知
上游覆盖度 部分(缺 ISO) 高(缺 2 条) 零 零
本次到达的级别 L0(L1 前失败) L4 L4 未做

从这张表能读出一件事:"上游覆盖度低"和"能跑到"是两回事。

pidfs 上游覆盖为零,但 3 分钟就饱和;Bluetooth ISO 上游覆盖也为零,连 L1 都过不了。

真正的决定因素是「fd 好不好拿 + 状态好不好进」。


六、演进的通用阶梯(沉淀)

把三个子系统的经验抽象一下,规则演进是被迫按顺序做四件事:

第一步:让规则编译(L0)

  • LLM 给的常量逐条对头文件(BTPROTO_ISO 被写成 SCO 的 2)
  • 尺寸用编译器量,别推算(sockaddr_iso 是 10 不是 9)
  • union 不是 struct(bt_iso_qos 是 60 字节,不是 92)
  • 跨文件重名是硬错误,上游已有类型只能复用
  • 纯 ioctl 增量规则要补顶层 "resources": {}

第二步:让调用通过探测(L1)

先看 syscalls : N/M 的 N:

N 的表现 含义 下一步
只有 1 fd 造不出来 查被传递性禁用的列表,找资源的最短创建路径
有一半 部分规则参数非法 查 disabled the following syscalls,逐条看错误码
全部 通过 进第三步

EAFNOSUPPORT(netns 不对)和 EPROTONOSUPPORT(协议族没注册)要分清,

后者往往是"门",seed 救不了。

第三步:让调用真的执行(L2)

  • fd 来自上游系统调用 → 必须 --extra-calls
  • 通配带进来的 write$fault_inject → 必须 --disable-calls
  • 判断执行看覆盖率增长 + 真实命令值,不看 corpus 快照

第四步:让状态到位(L3)与饱和判定(L4)

  • 状态前置写进 syscall_specs,别指望 fuzzer 凑
  • gen_seeds 只支持"建对象 → 调一次"的形状,链式状态得自己补(而且补了要验证存活)
  • 覆盖率走平就换目标,别加时长

七、回头看:KernelGPT 在这条链里发挥了什么

一个必须诚实回答的问题:这套为 KernelGPT 建起来的流水线,在这三个子系统的演进里到底起了什么作用?

结论先说:它的核心能力(LLM 生成规则)这次基本没发挥;真正承重的是它的分析器和执行框架。

7.1 实际分工

组件 这次具体做了什么 是否关键路径
analyzer(Clang AST → *.jsonl) 340k 函数索引、371 个带 ioctl 的 fops 是 ------ 目标排序工具直接跑在 ioctl.jsonl 上
func.jsonl 跨文件 handler 查找、一层转发追踪 是 ------ find_recent_surface.py 和 backfill_usage.py 都依赖它
gen_spec.py(LLM) 只对 iso_sock_ops 跑过 1 次,产出 4 处硬伤 否
output_spec.py 三套规则都走它渲染;include 顺序修复是关键 是
eval_spec.py / run-specs.py 整个测试床;这次扩展了 --extra-calls / --disable-calls 是
gen_seeds.py socket 类规则有效(bind 前驱、双 socket 配对);uffd/pidfs 产出 0 部分

三个目标里,跑到 L4 的两个(uffd、pidfs)规则是手写的,LLM 完全没参与。

7.2 为什么它不适合站 C 位

不是模型不行,是任务形态不匹配 。gen_spec.py 有两个隐含假设,这次都被打破了。

假设一:每个 fops 都需要从零生成一条完整规则。

这次三个目标里有两个是"给上游已描述的对象补几条命令" ------ uffd 上游已有 8 条 UFFDIO_*,

我们只补 2 条;pidfs 则完全不涉及已有对象。全量重生成会立刻撞两堵墙:

  • 重名 :重新定义 bdaddr_t、bdaddr_type,sys/linux/*.txt 是一起编译的,直接炸
  • 丢 fd 来源 :fd_uffd 来自 userfaultfd()、fd_pidfd 来自 openat("/proc/self"),
    都是上游系统调用。规则里不能重新声明,生成器也不会替你把它们塞进 enabled_calls

这是一个结构性尴尬:生成器擅长"从无到有",而这类任务需要的是"增量补丁"。

假设二:规则的正确性可以用编译器判定。

这条对 L0 成立,对后面三级不成立:

级别 谁能判定 pipeline 有反馈吗
L0 编译 syz-extract + make descriptions 有(规则能否生成)
L1 通过探测 guest 里跑一次 socket() / open() 没有
L2 真实执行 corpus 里搜真实命令值 没有
L3 到达状态 只有覆盖率/日志能看出来 没有

evaluate() 目前只返回 success / fail,不返回"卡在哪一级、被哪道门挡住"。

而 machine check 打出的 disabled the following syscalls 列表已经是最现成的信号,

只是没人把它接回 pipeline。

7.3 LLM 最弱和最擅长的地方(这次实测)

ISO 规则里 4 处硬伤,全部集中在可以被机械校验的那一类:

硬伤 正确值 性质
BTPROTO_ISO = 2 8 常量查表
bdaddr_t 重新定义 上游已有 命名空间冲突
bt_iso_qos 当 struct(92 字节) union,60 字节 布局计算
socket$ syz_init_net_socket$ 运行时约束

没有一处是"语义理解错了"。它把 sockaddr_iso { sa_family_t; bdaddr_t; __u8; } 正确翻译成了

类型树,setsockopt 的 5 个选项也枚举对了。

所以分工应该是 :LLM 只做语义推断(这段源码在干什么、哪些字段是一组、哪个参数是输出),

凡能手算或机算的(常量值、结构体尺寸、名字冲突、netns 约束)一律交给工具。

7.4 但要公平地说:它在"广度"上是有效的

不能因为这次没发挥就否定它。在更早的阶段,gen_spec.py 批处理生成过 19 条规则 ,

那轮 fuzz 的实测结果是:

复制代码
executed 90461   cover 12242   crashes 9(5 个唯一标题)

虽然那 5 个都是已知的 syzbot 问题,但说明当目标是"一批没人描述过的 handler"时,
这条流水线是能出货的
。

差别在于两种任务要的东西完全相反:

广度模式(KernelGPT 的本职) 深度模式(这次)
目标数量 几十~几百 3
每个目标的上游覆盖 零 大部分已有
主要成本 生成 + 批量校验 可达性分析 + 状态构造
LLM 的价值 高(规模化的唯一手段) 低(每个都要人工判断)

这次是拿广度工具去做深度活,所以看起来"没发挥"。

7.5 让它真正发挥,需要补四件事

  1. 增量模式。 只生成"上游缺的那几条命令 + 它们需要的类型",输出 syscall_specs
    片段而不是完整规则。判据很机械:拿 ioctl.jsonl 的 case 常量减去上游 *.txt 里的常量,
    差集就是该生成的。
  2. 把四级台阶变成 pipeline 的一等公民。 evaluate() 返回
    {level, blocked_by, evidence}。L1 的 disabled 列表、L2 的命令值、L4 的覆盖率曲线
    都是现成的。
  3. 资源可达性求解。 给定一个 resource(fd_pidfd),自动从 sys/linux/*.txt 里找
    最短创建路径(这次是 openat$pidfd 而不是 syz_pidfd_open),生成 --extra-calls。
    纯机械工作,不需要 LLM。
  4. 常量与尺寸前置校验。 生成完立刻用 syz-extract 求值 + 编译一段 sizeof 打印,
    把 LLM 的常量错误挡在 L0 之前。

第 3 点这次是手工做的,而且做错过一次(先试了 syz_pidfd_open,它需要 pid 资源,

链条比 openat$pidfd 长得多)。这类"最短路径"问题完全应该工具化。


八、结论

累计约 2 小时、四类子系统、0 个内核 bug。

不是工具链的问题 ------ 每条规则都验证过"真实命令值真的在执行"。原因有三层:

  1. 补的是边角。 uffd 补 2 条、pidfs 补 13 条,上游主体覆盖早就存在。
  2. 边角路径很薄。 一次 copy_struct_from_user、一次 refcount 增减。分支数决定覆盖上限。
  3. 真正 bug 密集的是并发和生命周期 ,需要状态持续存在 + 多线程竞争 ------
    而现在的 seed 都是"建对象 → 调一次 → 退出",这在演进阶梯的 L3 之后就断了。

再往上追一层,是任务形态和工具形态的错位 :这一阶段做的是"深度"(3 个目标,

每个都要做可达性分析和状态构造),而 KernelGPT 的强项是"广度"(一批没人描述过的 handler,

批处理生成 + 批量校验)。同样的流水线在广度模式下出货过 19 条规则、9 个 crash;

在深度模式下只能当分析器和测试床用。

所以下一阶段有两个方向,选哪个取决于目标是"补覆盖面"还是"找 bug":

  • 要补覆盖面 :给 gen_spec.py 加增量模式(只生成上游缺的命令),回到广度模式,
    让 LLM 重新承重。
  • 要找 bug :把 L3 做扎实 ------ 让对象活到另一个线程去动它。gen_seeds 的链式能力是
    真正的瓶颈(build_pair() 目前只做了两个 socket 的配对)。

相关推荐
aibo86030842 分钟前
基于 Spring Boot + Vue3 的餐饮食材采购与成本分析系统设计与实现
java·spring boot·后端
Geek-Chow44 分钟前
Spring Boot: Integrating Swagger / OpenAPI With springdoc-openapi
java
Rain的Java大神之路1 小时前
面了上百人,我最爱问的LeetCode 1「两数之和」:能背代码的很多,能讲清“先查后存”的没几个
java·后端·算法
薛之谦_1 小时前
Java 整合 RocketMQ 实战:从入门到复杂场景落地
java·rocketmq·java-rocketmq
aibo8603081 小时前
基于 Spring Boot + Vue3 的菜谱分享网站设计与实现
java·spring boot·后端
凤山老林1 小时前
Spring Boot 集成 TOTP 双因素认证与恢复码机制
java·spring boot·后端
小冯不疯1 小时前
物料主数据同步实战:从金蝶云星空到小满OKKICRM的单一策略深度拆解
java·大数据·数据库
vipxieliang1 小时前
Spring Boot 测试策略:单元测试、切片测试与集成测试
java·spring boot·后端
the局外人1 小时前
用 TRAE 把 Markdown文档 搬到微信公众号
前端