漏洞

第一轮 GC 熄了灯,第二轮还在清场——重走 AF_UNIX 竞态(CVE-2026-53361

CVE-2026-53361 会让排队重跑的 AF_UNIX 垃圾回收 worker 在 gc_in_progress 为 false 时继续分析套接字图,使并发的 SCM_RIGHTS MSG_PEEK 不再通知序列变化;本文从两轮 workqueue 时序出发,追到过期的 SCC 判定、worker 入口修复、反常的稳定分支版本图,以及一支 Linux 机群真正完成修复所需的证据。

手绘技术封面:第一轮 AF_UNIX 垃圾回收班次离开时关闭工作灯,已经排队的第二轮班次进入同一套接字清场区;MSG_PEEK 操作者复制出一枚文件句柄,原信封仍留在双套接字环中。
文章导航

研究依据SOSEC 安全研究 · 2026 年 7 月 17 日本地复核 Linux CNA JSON、2021 年 peek 屏障、2024 年 workqueue 与 SCC 系列、2026 年 3 月和 5 月修复、内核官方文档、四条稳定分支修复及 Debian、Ubuntu 当前状态

来源Linux CNA、固定 Linux 源码对象、内核 workqueue 文档、Linux man-pages、发行版安全跟踪页与 SOSEC 源码复核

1 第一班人走时关了灯,屋里却还有第二班人在清场

想象一间深夜仍在运转的 AF_UNIX 清理室。第一轮垃圾回收刚把一圈套接字队列检查完,离开时顺手关掉写着 gc_in_progress 的工作灯。可同一个 work item 已经排好了第二轮,它随后进门,继续判断哪些文件引用可以回收。此时一名执行 MSG_PEEK 的人朝门口看了一眼:灯是灭的,于是认定屋里没有 GC,没去拨动那只负责报信的序列铃。

这就是 CVE-2026-53361 的完整轮廓。两轮 GC 没有同时闯进同一张图,workqueue 仍把同一个 work item 串行执行。真正错位的是一段状态的归属:调度者提前为未来的重跑写下 true,第一轮回调退出时又把它擦成 false,第二轮实际开始后却没人重新写 true。一个本应描述“此刻正在执行”的布尔量,被交给无法覆盖每一次执行的调用者管理。

灯灭本身还不足以造成后果,另一个协议相信这盏灯,事情才连起来。2026 年 3 月,Linux 引入 unix_peek_fpl()unix_peek_seq:当 SCM_RIGHTS 的 peek 恰好撞上强连通分量的文件引用核对,它要让 GC 放弃这份前后不一致的快照。为了让常见路径更轻,helper 看到 gc_in_progress 为 false 就直接返回。两个月后,Igor Ushakov 发现,真实运行的第二轮 worker 恰好可能落在这个 false 状态里。

5 月的主线修复只移动了三行行为。提交 d82ba05263c6unix_gc() 入口写 true,删掉调度器里的 true 写入,worker 结束时原有的 false 写入继续保留。补丁很小,解释却不能只盯着三行 diff:描述符传递制造引用环,SCC 算法尝试回收它,peek 会悄悄改变 file count,workqueue 又允许正在运行的 work item 取得下一轮 pending 状态。四段历史在这里同时到场。

代码与处置可以先落到一个清楚的决定面:问题位于 net/unix/garbage.c,调度入口是 unix_schedule_gc(),实际回调是 unix_gc(),较老稳定树可能使用 __unix_gc(),peek 协调点则是 unix_peek_fpl()。主线修复为 d82ba05263c6;公开修复下限是 6.6.144、6.12.95、6.18.38 和 7.1,6.1 自 6.1.141 受影响,但 CNA 快照没有给出修复下限。永久处置是安装供应商支持的修复内核并重启到新 boot;窗口内只能暂停不受信任工作负载,或按既有策略限制不需要的描述符传递,不能用防火墙或磁盘里已安装的新包替代运行修复。

把每个子系统单独拿出来看,它们都像是守规矩的。workqueue 让同一 work item 串行回调;seqcount 读者只要看见序列变化就拒绝旧结论;scm_fp_dup() 合法地替 peek 调用者取得文件引用;SCC 也只清理那些引用都能由内部边解释的分量。问题出在那盏把它们串起来的灯:workqueue 产生了一轮不再被调度器早先写入覆盖的执行,peek 又把这段黑暗当成沉默许可。这里没有哪个 API 单独违约,失效的是它们之间那句没有写进类型系统的承诺——每次图分析都必然被 true 覆盖。接下来所有细节,都是在追问这盏灯究竟何时被关、谁有资格再打开。

1.1 六个时刻,让一次 true 写入变成全程为 false 的第二轮

gc_in_progress == false 开始,调度线程 T2、T3 都进入 unix_schedule_gc()。T2 读到 false,通过条件判断,还没来得及写任何值就被切走;T3 随后也读到 false,同样走进分支。普通 C 的 if 一旦通过,恢复执行时不会替线程再问一遍条件。这里不需要撕裂读取、罕见的弱内存结果,也没有第二份 work 结构。

T2 恢复后执行 WRITE_ONCE(gc_in_progress, true),再调用 queue_work(system_dfl_wq, &unix_gc_work)。第一轮回调开始,work item 的 pending 状态已经被消费。T3 此后从先前通过的分支内部继续,它不会看到现在亮着的灯,于是又写一次 true,并在第一轮仍运行时对同一个 work item 调用 queue_work()。此时 workqueue 可以接受请求,把 running item 重新标成 pending,等本轮结束再回调一次。若 T3 早到一步、第一轮尚未启动,额外重跑反倒不会形成。

第一轮 unix_gc() 完成图分析,执行 WRITE_ONCE(gc_in_progress, false)。只从这一轮看,写入没有毛病:它的任务确实结束了。排队中的第二轮随后进场,旧代码却没有 worker 入口 true。T3 先前那次写入已经被第一轮的 false 覆盖。从第二轮入口到最后的清理路径,READ_ONCE(gc_in_progress) 都可能报告 false,尽管 AF_UNIX GC 正在实打实地检查图。这里每次标量读写都能按源码顺序解释,错的是“哪次 true 属于哪一轮”这项配对关系;把事件按 callback ordinal 分组后,第二组只有结束、没有开始。

如果这段时间有人 peek 带 SCM_RIGHTS 的消息,unix_peek_fpl() 先确认复制出来的 file list 存在,并且里面确有 AF_UNIX 描述符,然后读取那盏暗灯。false 让它在 raw_write_seqcount_barrier(&unix_peek_seq) 之前直接返回。调用者已经多拿到文件引用,unix_scc_dead() 却看不到序列变化,便可能把 peek 前读到的 A 和 peek 后读到的 B 当成同一张快照。

正确的归属规则非常朴素:凡是会检查套接字图的回调,都要在自己的入口建立 true,并在自己的出口清成 false。第一轮退出与第二轮入口之间即便出现极短的 false 也无妨,因为那一瞬间没人分析图;这时发生的 peek 没有正在进行的 SCC 快照可供判废,跳过通知符合设计。第二轮跨过入口后,灯必须一直亮到最后一次 SCC 判定结束。这条性质比要求调度器预测一次提交历史会产生几轮回调更直接,也更容易测试。

Linux CNA 给出了几乎相同的三线程顺序,指出受影响文件是 net/unix/garbage.c,也点名 unix_peek_fpl() 对活动状态的依赖。该记录没有填写 CVSS 向量或 CWE,没有声称存在公开完整利用,也没有在野活动证据。因此,第二轮为 false 属于直接确认;把它接到公开的 peek/SCC 失败模式属于源码组合推论;更强的利用结果不在现有证据内。

全文始终把这几层结论分开放置。“CVE 修复直接确认”覆盖暗灯重跑与被跳过的活动前提;“直接前置提交确认”覆盖 A/B 计数时序,以及漏掉 peek 通知时活跃队列会被清理;“组合推论”才是把那套时序放进本漏洞的第二轮。发行版优先级和可能出现的服务症状只用于运维判断。这样读者可以单独质疑某一环,也不会因为 CNA 没有 CVSS 就被迫接受作者补写的分数。

1.2 证据链把源码行为与运行状态分开

公开描述和版本区间以 2026 年 7 月 4 日发布的 Linux CNA JSON 为锚;控制流则由一组固定提交互相衔接:2021 年 peek 屏障 cbcf01128d0a、2024 年 1 月 workqueue 改造 8b90a9f819dc、从 3484f063172d 开始的 SCC 系列、2026 年 3 月序列协议 e5b31d988a41,以及 5 月修复 d82ba05263c6。CNA、提交、内核文档、发行版页面与启动主机证据各自回答不同问题,不能互相替代。

提交对象把主线修复时间钉在 2026 年 5 月:Kuniyuki Iwashima 于 5 月 1 日完成作者提交,Jakub Kicinski 于 5 月 4 日合入主线。7 月 1 日前后的稳定分支回移和 7 月 4 日 CNA 发布都发生在后面。把这些事件混在一起,会凭空多出六周未修复窗口,也会让读者误判各发行版当时继承的是哪一份源码。

官方 workqueue 文档提供 API 前提。queue_work() 遇到仍处于 pending 的 work 会返回 false,可一个已经开始执行、pending 位被消费的 work item,仍能在 running 时取得下一轮 pending。公开时序里,T2 造出首轮执行,T3 恰好在 worker 启动后再次排队。flush_work() 负责等待相关 work 完成,却从未承诺调度器写下的另一个布尔量会和每次回调一一配对。那是 AF_UNIX 自己附加的约定,后来被真实时序打破。

Linux man-pages 提供用户态前提。SCM_RIGHTS 通过 AF_UNIX 辅助数据传递打开文件的引用;MSG_PEEK 返回排队内容,却不把消息移出接收队列。源码再补上隐藏的一步:unix_peek_fds() 调用 scm_fp_dup(),把 file list 复制给接收者,然后交给 unix_peek_fpl() 做协调。于是原消息仍占着队列,进程手里已经多出一份可安装的引用。

发行版页面只回答带日期的产品状态,不负责解释根因。截至 7 月 17 日,Ubuntu 把问题列为 Medium,但 generic、HWE、cloud 和 appliance flavor 的结论不一致;Debian 的 trixie security 已在 6.12.95-1 修复,bookworm 的 6.1.176-1 仍显示 vulnerable。页面会继续更新,所以调查记录还要保存查询日期与当时结论,不能用未来状态覆盖历史暴露窗口。一个“Ubuntu”或“Debian”标签压不住这张表,资产记录必须保留 source package、flavor、构建号和当前启动内核。

下表把不同证据各自能证明的事拆开。提交对象证明控制流,CNA 证明公开区间,包跟踪页证明某个时间点的产品状态,boot ID 才能证明一台机器此刻跑着什么。任何一项都不能悄悄替代另一项。后文涉及影响、检测和处置时,也沿用同样的证据分工。

证据能够确认单独无法确认本文用途
Linux CNA JSON公开描述、文件、Git 区间、修复下限完整利用结果或供应商包状态版本与结论锚点
固定 Git 对象控制流、状态归属、提交日期自定义二进制实际运行的代码机制重建
内核文档workqueue 与 seqcount API 契约AF_UNIX 调用者是否正确并发语义解释
发行版跟踪页带日期的包与 flavor 状态未列构建的等价源码部署查询
启动主机证据运行构建、boot ID、配置没有源码映射时的上游意图最终修复证明
移动端可横向滚动,对照每类证据的适用范围和它真正支持的结论。

2 worker 进门之前,文件描述符早已织成一张图

AF_UNIX 需要自己的垃圾回收器,是因为普通引用计数认不出所有死环。假设进程 P 用 SCM_RIGHTS 通过套接字 B 发送 A,又通过 A 发送 B,随后关闭手里的普通描述符。B 的接收队列持有 A,A 的接收队列又持有 B;两个 file count 都不会归零。进程也许再也够不到它们,队列里的两封消息却能彼此续命,单靠 close 语义无法判断这对引用已经成了垃圾。

Linux 把尚未接收的 AF_UNIX 消息里携带的描述符称为 in flight。file list 既记录总条目,也记录其中有多少个指向 AF_UNIX 套接字。当这样的列表加入接收队列,unix_add_edges() 等 helper 会把关系记进 GC 图:套接字 file 是 vertex,排队描述符是 edge。消息被接收、销毁或列表释放时,相应关系也要从图中撤走,所以 graph state 记录的是随队列生命周期变化的所有权索引,并非从 fd table 拍下的一张静态图。它回答“谁通过哪条队列仍有机会取得谁”,这是一个裸 file count 无法解释的部分。

GC 不能看见环就收。进程可能还握着 A,某个不在 in-flight 图里的套接字可能接收其中一员,也可能有一条边通向另一个仍有外部生路的分量。一个环只有在所有可见 file 引用都能由候选组内部的边解释时,才可能是真正垃圾。换成两点模型,就是 A、B 的每一份引用都能分别由对方队列里的 edge 对账,且不存在第三只手;任一 vertex 多出一次 file count,整个分量都必须留下。代码因此要同时比较拓扑和计数;分析途中若悄悄多出一份外部引用,前一个 vertex 的结论就已经过期。

图中只画两个套接字环和两封信,是为了把算术留在眼前。真实系统里可以有成千上万个 vertex、监听套接字、embryo socket、多条消息和重复描述符。两点环仍保留最关键的事实:A 队列握着 B,B 队列握着 A,图外一只手可以让任意一边继续存活。手松开后它可能可收;GC 检查中途又伸进一只手,昨天的答案就不再描述今天。

沿着一个描述符走一遍,图就不抽象了。发送端的整数只是在 fd table 里选中一项,辅助消息处理会 pin 住底层 file,放入 scm_fp_list。skb 一旦排到对端,发送者即便立刻 close,file list 仍是所有者。AF_UNIX 只把其中的 Unix 套接字关系建图,因为这些 file 还可能绕回其他接收队列。普通 recv 会把能力安装到新的 fd table 槽位并消费消息;peek 则在原队列所有权不动的同时再造一份引用。GC 判断的是这张队列之网还能否通回进程,并非两个整数是否相同。

2.1 SCM_RIGHTS 信封装的是 file 引用,不是一个全球通用的整数

用户态确实把描述符数字放进 control message,可数字只在发送进程自己的 fd table 中有意义。内核把它解析成 struct file 引用,再装进 struct scm_fp_list。接收者最终得到的是由自己 fd table 选择的新数字。若把线上值理解成可跨进程搬运的全局身份,就会漏掉真正改变生命周期的引用增加,也解释不了之后 peek 为什么还能再复制一次。

消息留在 sk_receive_queue 时,排队的 file list 持有这些引用。正常接收会拆下列表、安装描述符,并按套接字语义消费消息;消息销毁则释放未安装的引用。在任一事件发生前,消息本身就是 owner。所以,即使所有进程都关掉曾用的描述符整数,GC 也不能直接释放对应套接字:另一端一封尚未读取的辅助消息仍可能握着它。

图只跟踪混合 file list 里的 AF_UNIX 部分。fpl->count_unix 告诉 helper,列表中有没有值得表示成 vertex 和 edge 的 Unix 套接字。与它同行的普通文件仍遵守自己的生命周期规则,却不会构成 socket-to-socket 引用环。这个区分也让 unix_peek_fpl() 能在复制列表不含 AF_UNIX file 时立即返回,不必为无关的 peek 产生序列流量。

边的方向不能画反。描述符躺在接收者队列里,意味着读取该套接字的人有机会获得被传递的 file。代码保留 predecessor 和 successor 关系,使 unix_vertex_dead() 能判断某个 vertex 是否存在通向当前 SCC 之外的路。successor 为空并非“没有路”,恰好表示接收者不在 in-flight vertex 集合里,因而是一条明确的图外生路。一张方向错了的图依旧可能看着像环,却会把“哪个队列持有哪个引用、哪个外部接收者能救活候选”全部说反。

用户阈值影响何时调度 GC,不改变某条 edge 是否真实存在。整理后的代码用 UNIX_INFLIGHT_SANE_USER——也就是 SCM_MAX_FD * 8——让普通 in-flight 数量不必频繁付出全图扫描成本;已知 cyclic SCC 还能触发 flush 路径。这些条件决定竞态机会,却不会把一个低计数的两套接字环变成错误模型。只要收集已经启动,过期判定的核心算术完全一样。

命名 Unix socket 的文件系统权限保护的是那个入口,并不会禁止本地不受信任进程创建自己的匿名 socketpair()。只要沙箱允许相关 syscall,它仍能构建环并发送辅助数据。暴露面盘点因此要看本地执行信任、是否共享内核、seccomp 与 LSM 策略、以及工作负载是否依赖描述符传递。互联网防火墙根本看不到进程命名空间里的这张所有权图。

调度阈值也不是安全阈值。低于 UNIX_INFLIGHT_SANE_USER 的普通调用者可能暂不请求昂贵 GC,已知 cyclic SCC 则可能让高压调用者通过 flush_work() 等待;这两件事都不能证明小图不存在,更不能说明两点环无害。实验室复现可以主动把图状态和用户压力推到调度分支;机群评估不能因为某次快照里描述符不足阈值,就把主机判成“不受影响”。

手绘 AF_UNIX 引用图:两个深蓝套接字圆环各自向对方发送一封信,构成闭合描述符环;左侧另有一只手握着指向其中一个套接字的外部 file 引用。
图 1:信封代表仍由队列持有的引用。图外那只手让整个分量存活;GC 绝不能接受一张悄悄丢失或新增这只手的快照。

2.2 Tarjan 先找出那只环,file count 再决定它有没有资格消失

提交 3484f063172d 在 2024 年 3 月把 Tarjan 强连通分量算法带进 AF_UNIX GC。深度优先搜索为每个 vertex 分配 index,沿边前进,遇到回边时向上传递更低的 SCC index,最后在根节点处切出一个分量。实现用显式 vertex/edge stack 推进搜索,避免让巨大图把内核调用栈压深;分组仍在 unix_gc_lock 保护下完成。提交 4090fa373f0e 完成算法替换:死 SCC 的接收队列先拼到 hitlist,图锁随后释放,skb 的实际清理由临界区外完成。

强连通分量是这样一组最大集合:里面每个 vertex 都能到达其他任意 vertex。它能指出环,却不能证明垃圾。单个套接字也是 SCC,可能根本无环;多个 vertex 的 SCC 也可能被外部 file 或通向别组的边救活。unix_scc_cyclic() 对多 vertex 直接确认成环;单 vertex 还要检查 self-reference 或 embryo-listener circle,避免把普通孤点送进 cyclic 路径。结构确认之后,unix_scc_dead() 才会判断应用是否还留有一条可用的生路。

对每个 vertex,unix_vertex_dead() 先遍历 edges。successor 为空,表示描述符能被一个非 in-flight 套接字接收;successor 的 scc_index 不同,则表示另一组也有机会接收。两种情况都立即判活。只有所有边都留在当前 SCC 内,函数才取第一条 edge 找回 predecessor socket,用 file_count() 得到 total_ref,再与 vertex->out_degree 比较。相等说明当前引用都能由组内边解释,任何不等都要保守地视为仍有未计入的所有者。这里用的是“不等”而非只检查“更大”,也说明实现把所有无法被当前图精确解释的计数都留给下一轮,不会在异常算术上贸然回收。

单次比较只属于一个 vertex,最后的结论却覆盖整个 SCC。unix_scc_dead() 反向遍历成员并向后传递 scc_dead;一旦某个 vertex 证明存活,后续成员无需继续读 file count,但图的 bookkeeping 仍会走完,末尾的序列复查也不会省略。若所有必要检查都判死且没有失效事件,unix_collect_skb() 才把接收队列拼入 hitlist,之后由 __skb_queue_purge_reason() 销毁消息。A 的旧 true 和 B 的新 true 就可能在这里拼成一次错误清理。

图状态本身也有性能演进。提交 6b6f3c71fe56 把两个 flag 整理成 UNIX_GRAPH_NOT_CYCLICUNIX_GRAPH_MAYBE_CYCLICUNIX_GRAPH_CYCLIC。已知无环可跳过 GC;图发生变化要完整跑 SCC;已经保存 cyclic SCC 的图可走 fast path。CVE 破坏的是围绕死活检查的有效性信号,所以 full 和 fast 两条路径都依赖那盏灯诚实。

正常描述符活动会推动这台三态机。已证明无环的图可以保持低成本,直到新关系让环重新成为可能;full walk 可以保留已知 cyclic 分组供下一次 fast pass 使用;完成收集后,图也可能回到 noncyclic。gc_in_progress 并不编码任何拓扑状态,它只回答“callback 此刻是否正在解释这些状态”。cyclic 图可以闲着,maybe-cyclic 图也可能正被分析;5 月修复只调整执行状态,恰好维持了这两类信息的分工。

unix_gc_lock 能稳定那些明确由图锁保护的关系,却不会自动冻结所有 file-count 变化。MSG_PEEK 通过一条不愿为每次普通 peek 都拿全局 GC 锁的路径复制引用。序列协议正是锁外所有权事件与锁内 SCC 计算之间的桥。活动 flag 一旦说谎,桥会被跳过,即使图遍历本身从头到尾都在正确持锁。

3 Peek 没拿走托盘里的信,却让读者手里多了一把钥匙

MSG_PEEK 常被概括成“读取但不消费”。面对普通字节流,这听起来近乎只读;消息里若带着文件描述符,它就是一次所有权事件。AF_UNIX 进入 unix_peek_fds(),执行 scm_fp_dup(UNIXCB(skb).fp)。复制后的列表为调用者持有更多 struct file 引用,原列表仍挂在排队的 skb 上。队列位置一动没动,GC 正在核对的计数却已经变了。

增加引用是预期行为。用户可以先窥视消息,稍后再正式接收;peek 得到的描述符在安装进 fd table 时必须仍然有效。困难在于观察顺序:GC 也许先读取 A 的 file count,暂时把 A 判成内部所有,随后 peek 在它走到 B 之前添了一名外部 owner。若不让 collector 知道这次插入,它会把两个时刻的结果装进同一份 SCC 结论。

这类冲突并非第一次出现。提交 cbcf01128d0a 在 2021 年修过 GC 与 MSG_PEEK:file count 增加后,peek 路径拿起再放下 unix_gc_lock,用一次锁屏障迫使旧 GC 完整地落在屏障之前或之后。那套做法不要求 peek 改图,只利用同一把锁建立先后关系。2024 年 SCC 算法改变图不变量后,提交 118f457da9ed 移除了这套 lock dance,当时认为新拓扑规则已经不再需要它;3 月提交说明,拓扑虽稳定,逐 vertex 的 file-count 快照仍会被锁外复制打断。

Igor Ushakov 后来证明,file-count 采样仍要协调。提交 e5b31d988a41 在 2026 年 3 月 11 日完成、3 月 12 日合入,引入更轻的信号:只有 file list 确实含 AF_UNIX 描述符且 GC 活跃时,peek 才通知;GC 在一个 SCC 的检查前后比较序列,看到变化就把这一组留到下一轮。它避开常见 inactive 路径的锁开销,也没有在恶意高频 peek 下无限原地重试。

“Peek”这个词掩住了两只时钟。消息时钟没有前进:skb 还在原位置,之后仍能被 recv。所有权时钟却向前走了:复制出的 file list 已经 pin 住文件,准备安装给调用者。GC 通过 file_count() 读取第二只时钟,却在第一只时钟留下的队列关系上行走。seqcount 就是两者之间的回执;它不复制拓扑和计数,只告诉 collector,这份跨过所有权变化拼出来的死活结论不能提交。

3.1 scm_fp_dup() 正好改动 unix_vertex_dead() 正在测的数字

让 sk-A、sk-B 落在同一个 SCC。起初,A 的 file count 是 1,因为 B 的接收队列装着 A。GC 先检查 A:file_count(A) == out_degree(A),于是 A 看起来完全由内部边维持。这个结论在读数瞬间没有错,可 collector 尚未检查 B,也没有一把锁能阻止所有未来的描述符安装在两个 vertex 之间发生。

用户此时 peek B 队列里的消息,得到 A。scm_fp_dup() 把 A 的 file count 从 1 增到 2,B 队列里的原始 A 引用仍然存在。新增引用证明 A 活着;GC 若重新看 A,等式就会失败。算法没有为每个 vertex 都反复回头,因为序列变化本该告诉 unix_scc_dead():整组快照不再一致,当前轮次把它留下即可。

紧接着,用户关闭手里的普通 B 描述符。B 的 file count 从 2 降到 1:原先一份来自 open fd,另一份来自 A 队列里 in-flight 的 B。GC 此后检查 B,又看到 count 与 out-degree 相等。A 是 peek 前以 1 对 1 判的,B 是 close 后以 1 对 1 判的;peek 之后的真实 A 已经是 2 对 1。两个独立读数都能自圆其说,可从未存在一个时刻让整个 SCC 同时死亡。

序列通知不必记下 peek 了哪个 socket,也不关心引用具体变了几次。它只要证明,在分量的开始与结束观察之间,发生过足以让最终 refcount 判断失效的事件。单调变化的 seqcount 很适合承担这件事。GC 丢弃这次 all-dead 尝试,保持队列不动,等后续轮次在新的时间区间重新判断。

3 月提交附带的公开复现明确说,GC 清理了一个仍存活套接字的接收队列。CVE-2026-53361 会在 false 重跑期间重新打开同一类通知缺口。把这两个源码事实接起来,可以得到受控推论:公开的 A/B 顺序若落进第二轮黑暗区间,collector 会再次漏掉判废信号,进而可能清理活队列。这并不表示每次漏信号都会走到 purge,更不等于已经证明任意内存破坏。

Peek 后的 close 同样不可缺。若 A 的 count 上升,而其他所有权事实保持不动,GC 后续仍可能在某次检查中看到不等并留下这一组。公开失败让两侧朝相反方向变化:A 已经检查完才变得更“活”,B 则在轮到它之前失去 open 引用。正因如此,worker 笔记里才会同时出现两个貌似合理的等式。只循环 MSG_PEEK、不安排 B close 的测试可以制造压力,却没有重建 A-before-peek、B-after-close 的快照。

手绘 MSG_PEEK 场景:一封带黄铜句柄的密封描述符信封仍留在 AF_UNIX 接收托盘里,读者手中得到恰好一枚复制句柄,检查用镊子没有移走原消息。
图 2:Peek 不是 dequeue。接收队列继续持有原引用,读者又得到一份副本;队列位置没变,SCC 判定依赖的 file count 已经变了。

3.2 序列铃保护一段 SCC 检查,前提是活动灯必须说真话

unix_peek_fpl() 先做廉价的相关性筛选。file list 为空,或 count_unix 为零,都不会改变 AF_UNIX 图的计数,helper 直接返回。接着它执行 READ_ONCE(gc_in_progress):false 按设计意味着没有 collector 正在取快照,无需判废;true 才会拿起一把很小的私有 spinlock,调用 raw_write_seqcount_barrier(&unix_peek_seq)。私有锁只保证多个 writer 不会同时推进 raw seqcount,barrier 则把 file-reference 变化与读侧最终复查连接起来;两者都没有替活动 flag 判断真伪的能力。

这把锁只把多个 peek 侧 writer 串起来,并不是全局 GC 锁,也不会覆盖整个描述符安装过程。它发布的事件刻意很小:“一场相关 peek 穿过了活动 GC 的分析区间。”读侧的 unix_scc_dead() 在遍历 SCC 前调用 read_seqcount_begin(&unix_peek_seq),完成 vertex 检查后再让 read_seqcount_retry() 比较。序列改变就返回 false,在这里的含义是“本轮别把这组叫作死”。

GC 不需要得知具体哪一个描述符变化,也无需让 peek 等待整次图扫描。只要 bracket 内出现任意相关事件,all-dead 答案就失效。遍历过程中若某个 vertex 已经证明存活,scc_dead 会阻止后续无意义的 refcount 检查,但成员移动与最终 read_seqcount_retry() 仍会执行,图 bookkeeping 不会被短路。协议也不会让 peek lock 一直覆盖 fd 安装,更不会在同一回调里重新跑一遍 Tarjan。分量留在图中,等下一次 collection 使用新一组计数即可。

raw barrier 和 seqcount 规则负责安排并发观察的顺序,却修不好语义错误的活动谓词。3 月协议假设 false 意味着不存在从 read_seqcount_begin()read_seqcount_retry() 的活跃区间;第二轮重跑正好把它倒了过来:reader 正在 bracket 里,writer 却走 inactive 快速返回。writer 甚至没有进入 spinlock,也没有触碰 sequence,读侧看到的“没变”是真实数值,却代表错误事实。之后的所有屏障即使完美执行,GC 仍会接受协议本来要拒绝的结果。CVE 的核心不是 seqcount 算错,是上游状态让合法 writer 从协议中消失。

READ_ONCE()WRITE_ONCE() 也要放回它们真实的能力范围。它们让共享标量访问意图清楚,阻止编译器合并或凭空制造某些读写,却不会把“读 false、写 true、queue work”变成原子事务。T2、T3 可以像提交信息所示同时通过条件。给同一套错误归属再包几层访问宏,第二轮生命周期依旧缺少开始事件。

序列设计选择推迟,没有在 unix_scc_dead() 里反复重试。攻击者或高频服务可以连续 peek;若 worker 每次发现变化都从头再来,可能长时间困在一个 SCC,最终触发 hung-task 报告。本轮保守判活,用延迟回收换安全与前进。垃圾晚一点清掉没有关系,活队列一旦错误 purge 就无法凭空还原。

推迟还把干扰成本控制在一轮以内。worker 完成本轮 bookkeeping、释放锁,让正常描述符活动继续;后续调度可以用新计数再看这一组。它不保证下一轮必然收掉,因为合法 owner 可能依旧存在;它保证的是一份不确定的 all-dead 结论不会进入 hitlist。若高频 peek 一直穿过分析,垃圾可能连续几轮留下,可每轮 callback 都还能结束,系统也不会为追求立即回收困在 retry loop。CVE 没有改坏 unix_scc_dead() 的保守返回值,它让“不确定”这条证据根本没有抵达。

4 同一张工单可以离场两次,调度者却只替未来按过一次开关

AF_UNIX collector 最初并不是今天这副 workqueue 回调形态。2024 年 1 月以前,in-flight 压力一高,多颗 CPU 可能同时调用 GC,随后一起堵在同一把 spinlock 上。超过 16,000 个 in-flight socket 时,发送路径会明显变慢,每个调用者都准备遍历图,其实只有一个能干活。提交 8b90a9f819dc 把 GC 改成 struct work,让一颗 CPU 扫图,其余调用者不再白白烧 CPU 等锁。这个性能目标本身一直成立,5 月修复也没有退回多 CPU 并发扫描;它只把“谁在扫描”的状态从提交动作移回真实 callback。

这次改造解决了真实性能问题,也留下后来关键的一笔。提交特意把 WRITE_ONCE(gc_in_progress, true) 放到 worker 执行之前。提交说明展示了一名等待者:若 callback 已经开始、但入口还没来得及把 flag 设为 true,等待 CPU 可能同时看见 work 不再 pending、GC 也不 active,于是漏掉 flush_work()。提前点灯,能让等待路径及时知道“work 已排或正在开始”。

2024 年的形态里,有一个调度函数和实际扫描的 __unix_gc() 回调;后续图算法与状态整理又改过命名。来到 d82ba05263c6 的父对象,unix_schedule_gc() 会检查 unix_graph_state、用户压力和 gc_in_progress,随后把 unix_gc_work 排到 system_dfl_wqunix_gc() 负责 full 或 fast SCC walk,并在公共出口写 false。

调度前写下的 Boolean 只有在“提交次数与回调次数永远一一对应”时,才可能安全描述未来执行。running item 可以再次 pending,打破了这个隐藏假设。关键并非出现两份 pending work:worker 启动后已经消费 pending 位,T3 可以给仍在运行的同一个对象再挂下一轮。T3 的 true 在时间意义上属于重跑,却早于第一轮的 false;后一个合法写入把前一个覆盖。数据竞争检测器不必看到同一字节同时碰撞,所有值都按合法顺序发生,只是生命周期含义错了。

旁边四种时序都不会形成同一状态,这恰好能指导测试。T3 若在 T2 写 true 后才读,就会跳过分支;若它在首轮尚未开始、work 仍 pending 时排队,queue_work() 会报告已有 pending,不会增加下一轮;若它等第一轮写完 false 才来,旧设计下自己的 true 反而能覆盖新执行;如果黑暗重跑期间没有相关 peek,通知缺口也保持潜伏。漏洞时序占据剩下的窄槽:两名调用者先后承诺进入 false 分支,首轮及时启动并消费 pending,T3 成功把 running item 重新置为 pending,首轮出口再擦掉 T3 的状态。测试若不分别确认这两个状态转换,偶然跑出两次函数调用也说明不了同一根因。

4.1 为了不漏掉等待,调度器意外接管了 worker 状态

原始 workqueue 提交用 work_pending(&unix_gc_work)gc_in_progress 解释动机。worker 入口若太晚才写 true,等待者可能同时看到“既不 pending,也不 active”,在 GC 已经进门时跳过 flush。把 true 提前到 queue_work() 之前,关上了这道缝。那时 flag 主要协调高 in-flight 节流与等待完成,依赖它做序列安全判定的 peek consumer 尚不存在。

归属漂移是几年里一点点发生的。flag 起初表达“已经调度或正在运行”;SCC 算法随后改变图任务的组织;2025 年 11 月的 24fa77dad25c 合并 unix_schedule_gc()wait_for_unix_gc();到了 2026 年 3 月,false 被解释成“绝无活跃 SCC 分析,peek 可以安静通过”。没有一份单独重构声明调度器现在要替每一轮 callback 的每一纳秒担保。

名称会掩住这种扩张。gc_in_progress 听起来像 worker 的直接属性,旧 true 却写在尚未开始 collection、也可能只是安排重跑的代码里;false 则由真正 worker 写下。状态区间的开头和结尾属于两个 actor。审查中只要看到这种不对称,就该枚举 framework 是否会合并、延迟、取消、重试或重跑关闭者。

queue_work() 的返回值适合做诊断,却不是替代修复。测试可以记录 T3 是否真的新排了一轮,并据此确认最难的 running-to-pending 转换;返回 false 的样本只能说明本次没有新增 callback,不能用来证伪漏洞。可调度器就算只在返回 true 时写 flag,写入仍发生在第一轮 false 之前,还是会被擦掉。每个真实图分析 pass 都必经的唯一位置,是 callback 入口。

flush_work() 也不会把灯补亮。它按 workqueue 语义等待相关工作完成;若第二轮在 false 下执行,flush 可以非常忠实地等着这轮错误状态的 callback 结束。同步原语能把一台错误状态机排好顺序,却不能替它纠正含义。验收必须在两次 worker 入口直接观察 flag,不能因为等待者最终返回就推断生命周期正确。

六格手绘时序:两名调度者分别看见 GC 灯未亮,为同一个 work item 贴上两张票;第一轮 collector 运行并关灯,随后排队的第二轮在灯仍熄灭时进入。
图 3:T2、T3 在调度器里竞争,两次 GC 回调仍是串行。缺陷是第二次回调没有自己的状态起点,不是两名图遍历者并行。

4.2 第二轮 worker 确实在运行,false idle 也确实会让报信消失

在第二轮入口,旧 unix_gc() 首先拿 unix_gc_lock。若 unix_graph_state == UNIX_GRAPH_NOT_CYCLIC,它会放锁并跳到公共清状态标签;否则初始化私有 hitlist,对已知 cyclic 图调用 unix_walk_scc_fast(),对需要重新分组的图调用 unix_walk_scc()。walk 结束后才放图锁、逐个把命中 skb 的 file list 标记为 dead,再以 SKB_DROP_REASON_SOCKET_CLOSE purge。false 会覆盖的不只是 DFS 开头,而是从 SCC bracket、队列拼接准备直到 destructive cleanup 之前的整段判断。所有有意义的分支都发生在缺失的入口 true 之后,所以完整 SCC 决策可能在错误信号下展开。

false 本身不保证每次都破坏队列。没有相关 peek、图被快速跳过、或者 SCC 因其他外部 owner 明显存活,重跑都可能平静结束。要落到公开失败,需要同时排好一轮 queued rerun、一组可被所有权变化夹断的 vertex 计数、一次合格的 MSG_PEEK,以及后续 close。很多受影响机器长期没有日志,并不能证明它们免疫,只说明那组时序未被观察到。

时序一旦对上,失败路径很短。Peek 复制一份 count_unix > 0 的列表;helper 走到活动测试,读到 false 后返回;sequence notifier 没有移动。worker 完成 A、B 判断,在 read_seqcount_retry() 看见同一个序列,把整组结果当成稳定快照。漏掉的事件不会自动产生内核警告,也没有可供网络设备匹配的特征。

生产追踪要克制。描述符传递可能暴露敏感进程上下文,事件量也很大。记录计数、适合本地关联的 socket 身份、namespace、cgroup 与时间即可;除非事故调查和制度明确需要,不要复制用户 payload 或整张 file table。探针还应采用采样或明确的触发窗口,预先估算 ring buffer 与丢事件率;一条缺页的高负载 trace 不能被包装成“未发生”。机群层面的最干净判断仍是版本与源码证据,运行探针更适合隔离复现或范围很小的异常调查。

一条够用的聚焦轨迹远少于完整 syscall transcript:callback 入口及 ordinal、入口写点后的 flag、callback 出口、每次被接受的 queue_work()、相关 peek 的活动读取、通知前后序列、SCC begin/retry 值,以及 hitlist admission。对漏洞父对象和固定内核采用同一 schema,差异就会很明确:旧版可以出现 ordinal 2 暗着进门、peek 经过而序列不动;新版必须在 SCC begin 前重亮,相关 peek 随后让 verdict 失效。

修复前的调度函数其实叠着三道门:图已知 noncyclic 时返回;普通用户低于 in-flight 压力时返回;活动 flag 看着为 false 时才排队。之后若调用者面对已知 cyclic SCC,还可能 flush。一个盲目 stress loop 跑几小时都碰不到两轮 callback,往往是图没进入 eligible 状态、压力路径没走到,或 running item 的重排从未成功。确定性测试要先断言每一道门,并记录 queue_work() 的布尔返回,再安排 peek。

手绘 false-idle 重跑:一名 GC worker 在熄灭的工作灯下检查双套接字环,MSG_PEEK 操作者复制句柄,旁边四珠序列通知器保持不动。
图 4:collector 正在运行,暗 flag 却让 peek 走 inactive 快速路径。序列没有变化,SCC 读侧便无法拒绝跨时刻拼接的计数。

5 A 在 peek 前被判,B 在 close 后被判,两张照片从未属于同一现实

影响链最可靠的解释不来自大胆外推,而来自 CVE 修复紧邻的前一个提交。e5b31d988a41 记录了一份复现:GC 清掉了仍存活套接字的接收队列;提交写出 sk-A、sk-B 的 file-count 变化,也说明序列判废为何足够。CVE-2026-53361 再告诉我们,真实重跑期间活动 flag 会压掉这次判废。两条事实拼起来,得到一条具体而且可以标注证据等级的失败链。

开始时,A、B 属于同一个 SCC。A 的普通 fd 已关闭,却仍能从 B 队列收到;B 还有一个 open descriptor,同时它也躺在 A 队列中。GC 进入 component check,先走到 A。此时 A 的 count 恰好等于它的组内 out-degree。对那个瞬间而言,代码没有看到任何外部 owner,于是暂记 A 为 dead。

用户从 B peek 出 A,随后 close B。A 多出一份进程可见引用,B 少掉一份 open 引用。GC 稍后看 B,也可能得到 count 等于 out-degree。worker 手里于是出现两个来自互不兼容时刻的 true。正确的 sequence retry 会把整组结论改成“本轮推迟”;漏掉 sequence change,则让 A 的旧判断存活到足以和 B 的新判断合并。

SCC 一旦被接受为 dead,unix_collect_skb() 会访问每个 vertex,从第一条 edge 找回 predecessor socket,拿接收队列锁,再把 skb 拼入 hitlist。监听套接字还有专门分支:helper 遍历 pending embryo,并在各自锁下拼走 embryo queue;这意味着错误结论影响的不只监听队列表面,还可能触及尚在排队连接的接收内容。释放 unix_gc_lock 后,worker 把命中的 scm_fp_list 标成 dead,调用 __skb_queue_purge_reason(&hitlist, SKB_DROP_REASON_SOCKET_CLOSE)。用户手里的 peek A 证明分量还活着,可消息已经被送进只应服务于不可达环的清理路径。

“先选中、后销毁”的分离让事后诊断更难。队列拼接发生在图锁与各队列锁建立的 collector 视图中,真正 skb 释放则在图锁之外。标成 dead 的 file list 会在随后销毁里释放它携带的引用,应用看到的可能只是一次迟到的 EOF、缺失 control message 或资源交接失败,症状与判错时刻已经相隔很远。等服务发现异常,调度器读取、seq 值和两个 vertex count 往往没有了。purge reason 只说明走过哪条清理路径,不能证明最初判定正确。因此最有价值的实验室断言应放在 unix_collect_skb() 之前:序列变过的分量绝不能进入 hitlist。

5.1 过期 SCC 判定是一张拼接照片,不是某个 counter 读坏了

任何一次 file_count() 都不必返回损坏数字。A 的旧 count 可以完全准确,B 的新 count 也可以完全准确;错误来自把它们宣称成同一时刻的快照。这个区分会改变调试方向:只找撕裂计数或缺失 atomic increment,抓不到问题。sequence bracket 保护的是跨越所有 vertex 读取的整段区间,不是修正某个格式错误的整数。begin 值、A 读数、peek、close、B 读数、retry 值必须按同一时钟排列,缺掉任一环,最终相等都无法证明整组曾经同时成立。

fast walk 与 full walk 用不同准备过程抵达同一危险点。完整 Tarjan 路径先分组,再调用 unix_scc_dead(scc, false);fast path 遍历之前保存的 SCC,调用 unix_scc_dead(scc, true)。布尔参数只改变分组 bookkeeping,不改变死活结论所依赖的序列协议;两者都用同一个 sequence begin/retry 围住 vertex-dead 检查。活动 flag 为 false 时,依赖这条 writer 通知的两种模式都会失聪,所以只测试 full walk 仍不足以验收修复。

一旦 sequence 变化,unix_scc_dead() 即使已让每个 vertex 返回 true,也会返回 false。在这个 API 里,false 是保守结论:现在不收。调用者只有收到 true 才会进入 unix_collect_skb(),所以判废信号直接挡在 hitlist 之前,不依赖后续清理再自我纠错。下一轮可能看见读者仍握着 A,于是继续留下 SCC;也可能等这份引用后来关闭后,合法地把它清掉。推迟不是永久泄漏,它是在所有权视图不稳定时拒绝不可逆操作。

取证既要留拓扑,也要留时间。只看最终 file count 的 crash dump,无法还原 A、B 被检查时有哪些引用。实用 debug 输出会记录 SCC 成员、每个 vertex 的 out-degree 与 total_ref、begin/retry 的 sequence、worker run ordinal,以及 peek/close 时间。这些值能把本 CVE 的时序与普通引用泄漏、另一路队列销毁问题分开。

run ordinal 往往是通用 trace 最容易漏的一列。两次 callback 共用同一个 unix_gc_work 地址,第二轮还可能在第一轮结束后换 CPU 执行。入口不编号,分析者可能把一次 exit 和后来的 graph walk 误读成一个超长调用,也可能把 T3 的 queue 请求当成另一份 worker。把 work 地址、ordinal、flag、sequence bracket 放在一起,黑暗区间才成为可证伪事件:第二轮进门,开始 SCC sampling,相关 peek 出现,序列仍未改变。

四格手绘 SCC 判定:GC 在 peek 复制第一只套接字句柄前标记它,第二只套接字又在较晚检查前失去外部句柄,最后两封信被错误推向清理区,复制句柄仍由活读者持有。
图 5:每次 vertex 计数在读取时都可能正确。错误来自把 A-before-peek 与 B-after-close 合并,并漏掉本该判废这对结果的事件。

5.2 公开材料能支持什么,负责任的推论在何处停下

已经确认的事实窄而关键:排队重跑的 unix_gc() 可以在 gc_in_progress 为 false 时执行;unix_peek_fpl() 在通知 GC 前依赖这枚 flag;合格 peek 因此可以不推进 unix_peek_seq;直接前置提交又表明,A/B 时序若漏掉通知,GC 会清理活跃接收队列。这里不必发明额外攻击能力。

公开材料没有展示任意内核内存访问、稳定本地提权、容器逃逸或远程代码执行。源码支持的是一次所有权核算完整性破坏,随后接上不可逆队列操作:被归入不可达 SCC 的消息可以在读者仍证明可达时被拆下并销毁。直接表象可能是辅助数据丢失、peer 意外看到关闭、broker 丢掉待交接资源,或服务稍后使用本应收到的能力时失败;这些是合理的后果类别,不是统一且必现的症状,也不能借用其他生命周期漏洞的结果来抬高本案。

可达性是本地的。不受信任进程需要能够创建或使用 AF_UNIX socket,通过 SCM_RIGHTS 传递套接字句柄,调用 MSG_PEEK,搭出循环所有权,并制造重叠调度窗口。容器共享宿主内核,所以具备这些能力的容器工作负载要进入暴露面盘点;这项事实本身仍不能推出容器逃逸。

远程服务可能间接驱动本地描述符 broker,可没有一种通用网络包直接对应这张图。防火墙特征看不到进程内部匿名 socketpair。检测产品也不该为内核生命周期竞态编造 IP、域名、哈希或 packet IOC。相关上下文是 CVE、函数、提交、kernel build、socket 活动、workqueue 时序、cgroup、namespace 和本地进程归属。

复杂时序会降低复现率,不会让修复变成可选项。调度竞态要求 T2、T3 通过同一 false 检查,T3 成功把 running work 再排一次,peek/close 又落到 false 重跑的 component decision 内。高描述符 churn、多用户负载、测试 harness 和服务 broker 会增加机会。安静日志或一次失败复现都证明不了受影响构建是安全的。

截至复核时,来源集里没有公开在野利用证据。CNA、提交和发行版跟踪页都没有点名活动、战役或完整 exploit。事件团队仍应调查受影响内核上无法解释的本地服务崩溃,尤其是当时存在描述符传递工作负载;结论要保留置信度:受影响构建加兼容症状只是一条线索,不会自动变成蓄意利用归因。

归因还要多走几步:可疑进程得有能力搭套接字环,失败附近出现重复 ancillary send 与 peek,调度压力也要容得下重跑,还要解释这些行为属于谁、为什么发生。若只证明主机版本受影响和服务崩溃,最多能写“机制兼容”;加入 false second-run trace 才能写“条件出现”;再把触发序列与 actor 活动绑定,才有资格讨论蓄意。即使全都对上,正常 broker 与压力测试也可能使用相同原语。没有网络 IOC 不会削弱源码层漏洞结论,只说明事件判断必须依靠本地执行、时间和所有权证据。

6 修复把开关交给每一轮必经的那扇门

提交 d82ba05263c6 没有在调度器外再套一把锁。diff 把 WRITE_ONCE(gc_in_progress, true) 插到 unix_gc() 最前面的状态动作,位置早于 unix_gc_lock 和 graph-state 检查;调度器里的 true 与花括号被删掉,只剩 if (!READ_ONCE(gc_in_progress)) queue_work(...)。false 仍位于 skip_gc 标签之后,所以普通 walk 与 no-cycle 快速退出都会清掉本轮状态。每一次真实 callback 从此自己写完整生命周期。

这个位置正好修住重跑。第一轮入口写 true,结束时写 false;第二轮入口又立刻写 true,随后才触碰任何 SCC。两轮之间的空闲缝里,peek 可以不推进序列,因为那时没有 collector 采样计数;第二轮一进门,peek 就会看见 true 并发出通知。即使第二轮马上发现 UNIX_GRAPH_NOT_CYCLIC,它也先建立区间、再走公共出口配平,未来新增 early exit 时也有明确审查锚点。状态再也不用从 T3 的调度时刻穿过 T1 的首轮结束,艰难保存到未来。

flag 的含义也被收紧。它不再承诺“某位调用者试图排过 GC”,而是明确表示“callback 正处于分析生命周期”。调度代码仍可把它当作抑制重复提交的提示,却不再要求每次 scheduling attempt 预先承担一段未来执行。凡是 framework 会合并、延迟或重跑 callback,描述执行的状态都应尽量由执行上下文打开,这是这次移动留下的通用教训。

WRITE_ONCE() 依旧合理,因为 peek reader 不拿 unix_gc_lock 就要观察这个标量;额外顺序由 sequence barrier 协议承担。补丁没有把 Boolean 变成 mutex,也没有把所有 unix_schedule_gc() 调用者串起来。它只修复现有协议一直假定的状态覆盖,同时保住“一份 work item、一颗 CPU 扫图”的性能设计。

修复证明可以分三条 worker 路径写清。no-cycle 路径入口 true,graph-state 检查跳过分析,再由公共标签清 false,快照从未跑出区间;full path 的 true 覆盖 Tarjan 分组、每次 unix_scc_dead() bracket、hitlist 选择直到最终清除;fast path 也用同一入口覆盖保存的 cyclic 组。任何路径结束后都可以再排一轮,可下一轮会从自己的第一行重新证明。调度器怎样交错,都无法擦掉尚未发生的入口写入。

6.1 每个 callback 都为自己的区间点灯,running 时排来的重跑也不例外

源码验收要找行为,不能只找一模一样的 hunk。主线实际回调叫 unix_gc(),true 位于 spinlock 之前;较老稳定树里,真正 callback 可能叫 __unix_gc(),而 unix_gc() 仍是调度或等待 helper。两种形态的要求一致:凡是会进入图分析的 callback,都在入口建立 true,并在最后一次判定后清成 false。

回归测试必须强迫难路径出现。第一轮开始后暂停 callback,让两名调度线程制造一次成功 pending rerun,再放开首轮,并在第二轮入口采样。full、fast 两种 SCC walk 开始前都要断言 flag 为 true;两轮里执行相关 peek,还要断言 unix_peek_seq 改变。只在 work idle 时连调两次 scheduler,往往根本进不了覆盖写入的时序。

skip path 也要测试。第二轮进入后若发现 UNIX_GRAPH_NOT_CYCLIC,它仍然先写 true,再在 skip_gc 清 false,这个短区间必须配平。未来重构若增加 early return,不能绕过清理。一个入口写和一个公共出口最容易审;若采用结构化 cleanup,也要逐分支证明都能抵达。

true 位于 spinlock 之前并非排版细节。unix_peek_fpl() 不拿图锁,只用 READ_ONCE(gc_in_progress) 观察活动状态;如果 callback 先开始、甚至先拿到 unix_gc_lock,随后才把 flag 点亮,peek 仍能在这段窗口读到 false 并跳过序列通知。补丁把 true 设为 callback 的第一项状态动作,因此锁继续保护图结构,flag 则完整描述从任何图相关动作开始到公共出口结束的执行区间。

单 CPU 与多 CPU 揭示的弱点不同。受控单 CPU 更容易复现 pending/running 转换和抢占顺序;多 CPU 会给原始 T2/T3 scheduler race 和可见性更大压力。两种配置都要记录 work item 身份和 callback ordinal。测试若只在一种拓扑偶然通过,可能验证的是调度运气,不是状态性质。

KCSAN 能找到意外并发访问,可公开顺序是一场逻辑竞态,代码还使用了有意的 READ_ONCE()/WRITE_ONCE(),未必出现传统 data-race 报告。确定性 checkpoint 与状态断言更强。KASAN 可能捕捉错误 SCC purge 后的生命周期损伤,但没有 KASAN 输出,也不能证明 peek 通知确实送达。

等价补丁审查还要戒掉符号名匹配。旧稳定树可能把 __unix_gc 注册成 work callback,留下 unix_gc 做公共调度 helper;供应商树还会插入 trace 或 throttling 分支。应从 DECLARE_WORK(unix_gc_work, ...) 出发,沿实际函数指针找到 workqueue 真正调用的回调,检查它最早的图相关动作,再枚举所有能抵达 false 写入的出口。验收记录要明确写出“入口 true 早于图状态读取、full/fast 两路均受覆盖、skip 与正常出口都清 false”,不能只贴一枚提交哈希;运行测试还要留下第二轮入口的实测状态。这样既能接受上下文不同的正确 backport,也能拒绝只落在闲置 helper 里的“同名修复”。

四格手绘修复:同一名 GC worker 在两次串行运行的入口各自开灯,分别在亮灯下检查套接字环,两段绿色活动区间中间只有真正空闲的短缝。
图 6:修复不要求 true 穿过两次 callback 永不熄灭;它要求每轮 callback 在自己的图工作之前先建立 true。

6.2 稳定树为节流保留旧写入,6.1 在这份快照里没有公开修复下限

稳定分支 backport 并非逐行等同主线。公开补丁 82c17e13d404591f1ac217420cfa78c05066 在真实 __unix_gc() 入口补 true,同时保留旧的 scheduler-side true。回移说明写得很直白:旧写入服务于 wait_for_unix_gc() 的 over-limit throttling。那套布局仍需早期信号让超额发送者等待,可早期写入不能代替 callback 真相。worker 入口会重新建立 peek 所需区间,所以两个写入可以共存;验收还要确认所有 worker exit 最终清 false,避免只补开灯却在 skip path 永不熄灭。机械要求“调度器所有 true 都必须消失”反而会错杀正确 backport。

Linux CNA 公布的修复下限是 6.6.144、6.12.95、6.18.38 和主线 7.1;它还写出 6.6 的受影响起点 6.6.93,以及 6.1 的独立起点 6.1.141。6.6 线的回移引入点是 328840c93bd6,它说明“早于主线 6.9”不能直接判安全。这些事实来自默认值不同的两组 version block:一组默认 unaffected,枚举被回移引入的旧分支岛;另一组默认 affected,描述主线派生范围并切出固定下限。6.1 在快照里只有 bare affected commit ceb8bd6c69c1,没有对应修复 floor;语义区间停在 6.2 之前只是在描述该分支,6.2 从未被宣布为修复版。

这两组 product 也解释了表面重叠。只记“主线从 6.9 引入”,会漏掉 6.6.93 和 6.1.141;只比较 major/minor,又会漏掉供应商回移的引入与修复。尤其是 6.1,CNA 明确给出 affected start 却没有 fixed floor,自动扫描器应输出“需供应商或源码确认”,不能擅自拿下一个上游 minor 填空。版本号在这里不是一条直线,而是多条维护线各自携带不同提交历史。

可靠的版本判断按四个问题走:这份包来自哪条源码线?该线是否获得了脆弱 workqueue 行为,包括供应商 backport?它是否获得 callback-entry 修复或等价代码?当前 boot ID 对应的真是这份包吗?上游 floor 只替公开维护线回答前几问,供应商 changelog 与源码负责改造产品,运行资产负责最后一问。跳一步,就会出现“6.1 早于 6.9 所以安全”或“修复包躺在磁盘上所以已修”的假阳性。

Debian 很好地展示了差异。截至 7 月 17 日,bullseye 因脆弱代码不存在而 not affected;trixie security 的 6.12.95-1 已 fixed;bookworm security 的 6.1.176-1 在跟踪页仍是 vulnerable;sid 的 7.1.3-1 已 fixed。同一个发行版家族同时出现“代码从未进入”“稳定线已经修”“6.1 仍待供应商处理”三种状态,恰好证明 release 名称无法替代 source package 判断。包版本反映 Debian 源码状态,可以和上游 tag floor 长得不同,产品判断应回到官方 tracker。

Ubuntu 的矩阵更碎。同日页面显示,26.04 与 24.04 的 generic linux 仍 vulnerable,22.04 generic not affected;22.04 HWE 6.8 以及多个 cloud flavor 又有各自状态。机群报告必须保留 HWE、AWS、Azure 等 flavor 名,再核对正在运行的包。把所有“Ubuntu 22.04”主机压成一个状态,正好会擦掉制造本 CVE 反常版本图的 backport 历史。

手绘五轨 Linux 版本图:主线 6.9 到 7.1、6.18.y 到 6.18.38、6.12.y 到 6.12.95、6.6.93 到 6.6.144;红色 6.1.141 轨道以问号结束,没有修复门。
图 7:Linux CNA 明确给出四条修复下限;6.1 明确从 6.1.141 受影响,但 7 月 4 日快照没有公布它的修复 floor。
维护线公开受影响起点公开修复下限验收说明
主线6.9 映射7.1确认 d82ba05263c6 或等价 callback-entry 行为
6.18.y主线派生的受影响代码6.18.38供应商构建可能更早携带等价 backport
6.12.y主线派生的受影响代码6.12.95Debian trixie security 使用 6.12.95-1
6.6.y6.6.936.6.144由稳定回移 328840c93bd6 引入
6.1.y6.1.141CNA 快照未列必须由供应商或源码确认;不能把 6.2 当作修复
移动端可横向滚动,对照受影响起点、修复下限与源码验收要求。

7 一份安静的日志,回答不了 worker 的灯有没有撒谎

CVE-2026-53361 没有网络 IOC,也没有保证出现的 crash signature。flag 可以在第二轮为 false,却刚好没有坏 peek;peek 可以漏通知,可 SCC 因别的原因仍被判活;过期判定也可能清掉消息,直到很久以后才表现成本地服务失败、意外 descriptor close 或队列异常。机群检测因此以版本证据为主,运行证据只回答聚焦的事件问题。

第一步要看正在执行的 kernel,不是软件仓库。采集 uname -r、供应商可验证的 build identity、包 NEVRA 或 Debian version、boot ID、镜像来源和 live-patch 状态,并在同一采集时刻把它们绑定。后来把 repository version 抄进资产表,会把 installed state 偷换成 running state。修复包与旧内核并排躺在磁盘上,漏洞仍活跃;版本字符串看着更老的供应商 kernel,也可能已经携带等价补丁。自定义和 appliance kernel 要沿注册回调确认入口行为。

接着映射真正会使用描述符图的工作负载:多用户主机、共享一颗内核的容器节点、桌面 portal、本地 RPC broker、service activation、sandbox launcher、build worker,以及传递 socket 或 file 的 agent。再检查 seccomp、LSM 对 sendmsgrecvmsg、ancillary data、MSG_PEEK 的策略。这份清单用来排优先级,不等于宣称每个产品都已有公开触发链。

事件调查要保存 kernel log、pstore、vmcore、可比 staging 的 KASAN/KCSAN 输出、服务 core dump、task 与 cgroup 身份、namespace 映射和 socket 诊断快照。栈中重点找 unix_gcunix_scc_deadunix_vertex_deadunix_collect_skbunix_peek_fpl、接收队列销毁和 file-list release。出现一个函数名只是上下文,worker/peek 的顺序才是机制。

可疑节点重启前先收集易失证据,但这条调查线不要挡住全机群 patch。vmcore 或 trace 可以解释一场兼容的服务丢失,却不是决定受影响运行构建要不要升级的前提;反过来,安装记录只有在新 boot 后才能关闭暴露问题,也解释不了此前 crash。取证快照至少要封存旧 boot ID、运行 build、可疑 workload placement 与时间校准信息,修复记录则从新 boot ID 重新起算。把“事件时间”和“修复时间”分成两只钟,可以防止更新后的干净状态覆盖症状发生时的代码与工作负载上下文。

7.1 五只证据托盘,把暴露、条件、后果和恢复逐层分开

第一只托盘装 boot state:实际启动的是哪一份 kernel,安装修复后是否重启。把它绑定节点身份和 placement history,才能知道暴露期间哪些容器或用户共享这颗内核。节点可能跨过多次包发布一直不重启,也可能从 fallback entry 启动,只看镜像标签远远不够。

第二只托盘装源码行为。在 vendor tree 里找到 DECLARE_WORK 或等价注册的真实 callback;旧树的 unix_gc()__unix_gc() 分工不同,名字会骗人。确认 true 早于 unix_graph_state 检查、所有出口最后都有 false、unix_peek_fpl() 的 sequence path 仍在,并记录 commit 或 patch 来源。供应商历史里没有上游 hash,不代表行为不存在;advisory 写了 CVE,也不能自动证明等价。

第三只托盘装 workqueue 时间。在隔离 debug kernel 上记录 scheduler read、queue_work() 返回、pending/running 转换、callback ordinal 和 flag 写入,证明 running work 确实再次 pending,下一次入口也确实点灯。探针本身可能改变抢占与 cache 行为,所以复现要同时保存 CPU 拓扑、preemption model 和插桩点,并在无探针压力版本上重复功能结论。生产上只有聚焦调查才适合收这些细节,长期追踪全局 default workqueue 会扭曲时序并吞掉大量存储。

第四只托盘装图算术。针对实验室 SCC,保存 vertex 身份、predecessor/successor、out_degreefile_count()、begin sequence、peek、close、retry sequence 和 hitlist membership。这些值足以证明 dead 结论是否混用了两个时间段,不必把传递文件内容写进日志。所有权数字与时序已经能验证协议。

第五只托盘装服务恢复。启动固定内核后,逐项测试 systemd socket activation、容器 runtime control socket、桌面 portal、credential 或 secret broker,以及应用自己的 fd handoff;普通 recv 与 peek 都要覆盖,资源关闭和服务重启也要验证。一份底层 race 补丁即使通过 synthetic test,若破坏合法 descriptor passing,也不能直接扩大全量。

时间戳质量决定这些托盘能否拼在一起。workqueue probe 使用单调内核时间,服务日志可能写 wall clock,容器日志还会缓冲,reboot 又会重置一些计数。每条 trace 都要带 boot ID,保留 clock offset、缓冲区丢事件计数与探针启停时刻,采集后再转换;丢事件的空白区不能当作没有 peek。应用日志与 kernel warning 恰好显示同一秒,仍不足以证明 peek 落在 SCC bracket;强关联来自同一 trace clock 或显式同步标记,再叠加 process、cgroup、namespace、socket 和 work ordinal。

问题最低证据强证据不能拿来替代
主机是否暴露?运行构建与分支映射等价 callback-entry 源码复核只有已安装包
CVE 条件是否发生?worker 活跃时 flag 为 false成功重跑加被跳过的 peek sequence泛化 AF_UNIX 流量
是否形成过期 SCC 判定?混合 vertex-count 时序begin/retry sequence 与 hitlist 关联一个最终 refcount 快照
后果是否出于蓄意?进程和工作负载归属可复现触发与 actor 活动绑定受影响版本加 crash
修复是否完成?固定运行内核与 boot IDrace 回归和服务 handoff 验证只更新仓库或镜像
移动端可横向滚动,对照每个结论所需的证据强度和常见错误替代物。

7.2 确定性回归把不可见顺序变成可观察断言,不必伤害生产

测试围绕可控 checkpoint 搭建。先造 AF_UNIX descriptor cycle,把 graph state 推到可收条件,在第一轮 callback 入口后暂停;安排 T2、T3 在任一调度 body 完成前都通过初始 false 读取。让 T2 排队并启动首轮,再让 T3 在 callback 活跃时成功请求重跑。每一步都断言,避免依赖 scheduler 运气。

放开第一轮并记录它的 false 写入。在第二轮第一个状态 checkpoint,固定内核应看到 true,漏洞父对象应看到 false。让第二轮停在 read_seqcount_begin() 之后、final retry 之前,执行合格 peek,确认序列变化;随后 close,并验证 read_seqcount_retry() 把暂定 all-dead 转成 defer。还要保存 begin/after 序列、A/B 每次 file_count 与 out_degree,确认 defer 来自真实判废,不是另一个 vertex 早已判活。测试同时在“重跑没排成”“peek 不含 Unix socket”“checkpoint 落在 bracket 外”时失败,防止偶然绿灯。

加入负控:peek 一封没有 AF_UNIX 描述符的消息,序列不应变化;没有 GC callback 活跃时 peek,应该走 inactive 快速路径;只跑一轮普通 GC,flag 区间要配平;把图标成 UNIX_GRAPH_NOT_CYCLIC,skip path 的 entry/exit 仍要成对。每项负控都应单独报告触发条件和预期序列值,不能用一个总退出码遮住未执行分支。这样才能区分精确修复与“所有 peek 都无条件响铃”的错误 harness。

unix_walk_scc()unix_walk_scc_fast() 都要压。前者需要 UNIX_GRAPH_MAYBE_CYCLIC 的变化图,后者需要已经分组的 UNIX_GRAPH_CYCLIC。再分别构造有外部 edge、file-count mismatch,以及 A/B peek-close 时序的 SCC。断言失败时保存拓扑与所有计数,维护者才能判断哪一条不变量回归。

每条支持中的 vendor backport 都要跑,尤其是仍在服役的最老分支。函数名和 throttling 写入可以不同,可观察规则不能变。记录编译器、config、CPU 数、preemption model、sanitizer 状态和 commit 身份。为等待保留 scheduler true 的稳定补丁,仍必须在 __unix_gc() 入口重亮;测试要允许额外写入,同时拒绝暗灯 callback。

手绘证据工作台:一台 Linux server 连到五只托盘,分别放启动内核身份、源码对比、双票 workqueue 时序、套接字引用计数和三项本地服务回归。
图 8:没有一只托盘能独立结案。版本、callback 行为、时序、所有权算术和服务验证共同让修复可审计。

8 修好共享内核,重启每一个落点,让第二班亲手证明灯已亮

重启后,在 staging 跑 descriptor-handoff 服务测试与聚焦 race 回归;canary 观察本地服务稳定后再扩大。旧 kernel artifact 按事件策略保留,但自动 fallback 不能回到受影响镜像。修复的终点是每一个可调度落点都执行固定行为,不是第一台机器成功接受 package。

发布顺序按共享内核的 blast radius 安排。每种 flavor 先 drain 一台代表节点,保存维护前身份,安装并启动候选,证明 callback 不变量,再逐步放回真实使用 descriptor broker 的工作负载。kernel 与服务检查都通过后才扩组。若功能回归迫使 rollback,回退镜像自身也必须含等价修复;退回已知受影响 kernel 是一项重新暴露决策,需要补偿控制和新的限期,不能藏在普通运维操作里。

8.1 六步响应把版本修复与事件调查分开推进

  1. 第一,冻结一份带日期的资产快照。记录节点、发行版、kernel flavor、source package、build ID、boot ID、架构、工作负载信任级别、容器租户和 descriptor-passing 依赖;每个 range 判断附上来源。这样后续包更新不会改写调查窗口里曾经运行过什么,优先级所依据的主机集合也能追溯。

  2. 第二,把暴露与事件怀疑拆开。所有受影响 running build 都进 patch queue;只有出现兼容 crash、队列丢失、可疑本地进程或 race trace 的机器进入 incident lane。运维可以广泛修复而不宣称 compromise,调查者也能保护那一小组真正可能回答“条件是否被蓄意触发”的主机。

  3. 第三,只在制度允许处减少机会。暂停把新不受信任租户调度到受影响节点,对不需要 descriptor passing 的 workload 使用既有 sandbox 规则限制相关 syscall。不要全局封锁导致系统服务一起损坏。这些动作只能缩短暴露时间,无法改变旧 worker 的状态归属,且必须绑定 reboot campaign 的到期时间。

  4. 第四,以 canary 发布。先确认 vendor package 包含 callback-entry 行为,启动代表节点,跑 AF_UNIX service activation 与 broker 测试,再在隔离等价内核执行确定性重跑。记录两轮 ordinal、成功 requeue、第二轮入口写入、sequence 变化与 deferred SCC,防止测试只是没撞到 race 却显示绿色。通过标准还要包含普通 recv/peek 没有回归、fd handoff 后引用能正确关闭,以及节点重启后的 build/boot 证明;缺一项就只完成了源码验证。generic、HWE、cloud、appliance 的源码历史不同,要按 flavor 扩大。

  5. 第五,重新认证所有容量。更新 base image、golden snapshot、PXE entry、rescue kernel、autoscaling template 和 migration target。活跃池都更新了,故障时仍可能把 workload 挪到老备用节点。对休眠容量也要实际启动抽样,核对 boot entry、当前构建和服务回归,不能只相信镜像清单;启动后再销毁的临时节点同样要留下证明。admission 与 scheduler label 应来自 executing-build 证据;reboot debt 要和 package compliance 出现在同一张表,避免 installed-only 主机冒充绿色。

  6. 第六,以证据包结案。保留资产快照、advisory 映射、源码等价说明、canary 结果、boot 证明、回归日志、服务检查和 incident findings;明确写出是否观察到 false-worker 区间或过期 SCC 后果;6.1 的残余不确定性要标供应商与 owner。保留未知项的 closure package,比把所有无法解释 crash 一律写成本 CVE 更可靠。

8.2 留下来的设计原则:执行状态,应由真正执行的人负责

第一条是生命周期归属。scheduler 可以说“我请求了工作”,只有 callback 能说“这一轮已经开始”。framework 一旦可能合并、推迟、重试或重新排同一 work item,跨越真实执行的状态就应由该次执行打开和关闭。若开头、结尾分属不同上下文,reviewer 必须把它们之间每一种 framework transition 画出来。

第二条是优化谓词会在演化中变成正确性谓词。gc_in_progress 最初帮助避免冗余工作并协调等待;unix_peek_fpl() 后来拿它判断一次 file-reference 变化是否需要发布。对调度“差不多够用”的 flag,忽然成了快照协议的精确证据。任何新 consumer 给旧状态赋予更强含义时,都要重新验证 producer 真正保证了什么。

第三条是按区间审查并发。标量访问原子,不代表多步状态机描述了现实。把区间 opener、closer、observer、retry、early exit 和 duplicate execution 全列出来,在 callback 入口与最终决定前设断言。还要把 framework 允许的 pending、running、requeue、flush 转换画进同一张图,不能只沿业务函数的直线控制流阅读。T2/T3 时序之所以能抓住本案,是因为每次读写都属于明确 ordinal;只搜索“未保护赋值”,看到的会是有意访问宏,合法却错误的覆盖顺序反而漏掉。

第四条是保守回收。任何事件若可能改变可达性,就先推迟。unix_scc_dead() 在序列变化时把本轮 SCC 当作 live,延后垃圾,避免销毁队列。socket、file、memory 与分布式 lease 的 collector 都遵守同一优先级:无法确认图外 owner 已消失,就不能把不确定性转成不可逆清理。

第五条落在运维:backport 会制造非单调版本故事。主线 6.9 引入的功能可以出现在 6.6.93 和 6.1.141,另一个更高 minor 也可能因为分支历史不同而未受影响或已单独修复。产品 package、flavor、source 与当前 boot 才组成事实单位。对长寿命 kernel 只比版本数字,风险尤其高。

最后再回到那间清理室。第一班做完自己的工作,当然可以关灯;下一班不需要上一盏灯永远亮着,它只要进门时亲手再打开。d82ba05263c6 之后,每次 unix_gc() callback 都这样做。Peek 看见 collector,序列铃移动,不稳定的 SCC 留到下一轮,活着的读者也终于能保住托盘里的那封信。

研究记录

9证据、对象与来源

下面保留本文实际使用的标识、时间和原始材料,便于继续调查。

9.1研究对象

报告涉及的产品、行为者、技术、受影响对象和控制点。

CVECVE-2026-53361

Linux AF_UNIX workqueue 重跑状态竞态

组件net/unix/garbage.c

AF_UNIX 文件描述符引用图垃圾回收器

函数unix_gc

每轮入口都必须建立 gc_in_progress 的 worker 回调

函数unix_schedule_gc

两名调用者可能同时通过 false 检查的调度器

函数unix_peek_fpl

MSG_PEEK 序列通知快速路径

修复d82ba05263c69fa2437fe93e4e561cc40f4c03af

主线 worker 入口状态修复

引入8b90a9f819dc2a06baae4ec1a64d875e53b824ec

把 true 写入移到调度上下文的 workqueue 改造

前置条件e5b31d988a41549037b8d8721a3c3cae893d8670

依赖活动标志的 MSG_PEEK seqcount 协调

触发环境SCM_RIGHTS + MSG_PEEK + queued unix_gc rerun

本地描述符环与调度条件

9.2事件时间

  1. AF_UNIX 首次让 GC 与 peek 串行

    提交 cbcf01128d0a 在 MSG_PEEK 复制文件引用后执行 GC 锁屏障。

  2. GC 变成一个 work item

    提交 8b90a9f819dc 在把回收迁入 workqueue 时,把 true 写入放到调度路径。

  3. peek 开始用 seqcount 判废 SCC 决策

    提交 e5b31d988a41 加入 unix_peek_fpl,使“GC 是否活跃”进入安全协议。

  4. 每轮回调开始声明自己的状态

    提交 d82ba05263c6 把 true 写入移到 unix_gc worker 入口。

  5. Linux CNA 发布 CVE-2026-53361

    记录公布三线程时序和四条上游修复下限。

  6. SOSEC 完成源码重建

    SOSEC 将固定源码对象、版本区间、workqueue 时序、发行版状态与修复验证接入同一条证据链。

9.3来源与材料

  1. CVE.org:CVE-2026-53361 官方记录https://www.cve.org/CVERecord?id=CVE-2026-53361
  2. CVEProject:Linux CNA 原始 JSONhttps://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/53xxx/CVE-2026-53361.json
  3. Linux:主线 worker 入口修复 d82ba05263c6https://git.kernel.org/linus/d82ba05263c69fa2437fe93e4e561cc40f4c03af
  4. Linux stable:修复 82c17e13d404https://git.kernel.org/stable/c/82c17e13d404f686e164590483fd6c1abaa675d0
  5. Linux stable:修复 591f1ac21742https://git.kernel.org/stable/c/591f1ac217428a6d2b32a8ac14aac0fab44f155a
  6. Linux stable:修复 0cfa78c05066https://git.kernel.org/stable/c/0cfa78c050662784fc8e3ab26dbfd1dc632b2082
  7. Linux:单 CPU workqueue 改造 8b90a9f819dchttps://git.kernel.org/linus/8b90a9f819dc2a06baae4ec1a64d875e53b824ec
  8. Linux:MSG_PEEK 插入时推迟 SCC 的修复 e5b31d988a41https://git.kernel.org/linus/e5b31d988a41549037b8d8721a3c3cae893d8670
  9. Linux:2021 年 GC 与 MSG_PEEK 屏障 cbcf01128d0ahttps://git.kernel.org/linus/cbcf01128d0a92e131bd09f1688fe032480b65ca
  10. Linux:Tarjan SCC 检测 3484f063172dhttps://git.kernel.org/linus/3484f063172dd88776b062046d721d7c2ae1af7c
  11. Linux:AF_UNIX GC 算法替换 4090fa373f0ehttps://git.kernel.org/linus/4090fa373f0e763c43610853d2774b5979915959
  12. Linux:移除旧 peek 锁舞步 118f457da9edhttps://git.kernel.org/linus/118f457da9ed58a79e24b73c2ef0aa1987241f0e
  13. Linux:三态 GC 图状态简化 6b6f3c71fe56https://git.kernel.org/linus/6b6f3c71fe568aa8ed3e16e9135d88a5f4fd3e84
  14. Linux:GC 调度与等待合并 24fa77dad25chttps://git.kernel.org/linus/24fa77dad25c2f55cc4615c09df2201ef72c66f4
  15. Linux 文档:workqueue API 与重排语义https://www.kernel.org/doc/html/latest/core-api/workqueue.html
  16. Linux 文档:seqcount 与 seqlock 规则https://www.kernel.org/doc/html/latest/locking/seqlock.html
  17. Linux 文档:内存屏障包装器https://www.kernel.org/doc/html/latest/core-api/wrappers/memory-barriers.html
  18. Linux man-pages:AF_UNIX 与 SCM_RIGHTShttps://man7.org/linux/man-pages/man7/unix.7.html
  19. Linux man-pages:recv 与 MSG_PEEKhttps://man7.org/linux/man-pages/man2/recv.2.html
  20. Linux man-pages:辅助控制消息解析https://man7.org/linux/man-pages/man3/cmsg.3.html
  21. Ubuntu:CVE-2026-53361 包与 flavor 状态https://ubuntu.com/security/CVE-2026-53361
  22. Debian Security Tracker:CVE-2026-53361https://security-tracker.debian.org/tracker/CVE-2026-53361
  23. Debian:DSA-6381-1 Linux 安全更新https://www.debian.org/security/2026/dsa-6381
  24. Red Hat:CVE-2026-53361 产品状态入口https://access.redhat.com/security/cve/CVE-2026-53361
  25. SUSE:CVE-2026-53361 产品状态入口https://www.suse.com/security/cve/CVE-2026-53361.html
  26. Netdev 补丁:worker 入口状态修复讨论https://patch.msgid.link/[email protected]
  27. Netdev 补丁:MSG_PEEK 序列协议讨论https://patch.msgid.link/[email protected]
  28. Netdev 系列:单 CPU AF_UNIX GC work itemhttps://lore.kernel.org/r/[email protected]
  29. Netdev 系列:强连通分量算法https://lore.kernel.org/r/[email protected]