☰
深入 rust-review 的 Send/Sync 边界清洗检测:识别绕过线程安全约束的 unsafe 传递
2026/10/10 11:37:05 网站建设 项目流程
  • AI 技能
  • AI 插件
  • 应用安全
  • 网络安全
  • AI 评测

【免费下载链接】skills

Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows

项目地址:https://gitcode.com/gh_mirrors/skills8/skills
点击查看免费下载

导读

本文深入解析 GitHub 加速计划 skills8 仓库(Trail of Bits Claude Code security skills)中 rust-review 插件的send-sync-bounds检测 pass(send-sync-bounds-finder.md)。该 pass 专门定位一类隐蔽的 Rust 线程安全漏洞:非Send(或非'static)的值或闭包穿越线程边界时,约束没有被编译器强制实施,而是被unsafe构造"清洗"(launder)掉了。读完本文,你将掌握该检测的 Bug shape、双门控判定、误报清单、ripgrep 搜索种子与修复策略,并理解它与UNSAFESYNC、STATICMUT等同簇 pass 的边界划分。

一、检测目标:被 unsafe 清洗掉的线程安全约束

Rust 的Send与Sync是两个自动推导(auto trait)的线程安全标记:Send表示类型可以安全地转移到另一个线程,Sync表示类型可以安全地被多个线程共享引用。编译器在安全代码中自动推导并强制这些约束——std::thread::spawn、tokio::spawn、rayon::spawn都要求传入的闭包满足F: Send + 'static,不满足的代码直接编译失败。

send-sync-bounds-finder关注的是那些绕过编译器强制的形态。其核心 Bug shape 是:一个非Send(或非'static)的值/闭包穿过了线程边界,而该约束没有被强制实施——但不是通过直接的std::thread::spawn/tokio::spawn/rayon::spawn(它们本身就要求F: Send + 'static,一个"忘记"写约束的包装器只会编译失败,这不算 bug)。

真正不健全(unsound)的形态,是把约束通过unsafe清洗掉:

  • 裸thread::Builder+mem::transmute处理闭包或生命周期;
  • FFI / 自定义线程创建(绕过标准库的类型检查);
  • std::thread::scope的误用;
  • 用transmute凭空捏造缺失的Send/Sync约束。

而一个裸的、针对非线程安全负载的手动unsafe impl Send/Sync定义则属于UNSAFESYNC,不属于本 pass——两者在簇级提示中有明确的 Deconfliction 规则(详见下文)。

二、pass 在 rust-review 中的注册与触发条件

send-sync-bounds是 rust-review 插件concurrency-data-race簇(concurrency-data-race.md)的五个 pass 之一。簇的注册信息位于 prompts/clusters/manifest.json:

{ "cluster_id": "concurrency-data-race", "consolidated": false, "gate": "has_concurrency", "passes": [ { "bug_class": "atomic-race", "prefix": "ATOMICRACE", "prompt": "prompts/general/atomic-race-finder.md" }, { "bug_class": "unsafe-sync-impl", "prefix": "UNSAFESYNC", "prompt": "prompts/general/unsafe-sync-impl-finder.md", "requires": ["has_unsafe"] }, { "bug_class": "send-sync-bounds", "prefix": "SENDSYNCBOUND","prompt": "prompts/general/send-sync-bounds-finder.md" }, { "bug_class": "shared-memory-race", "prefix": "SHMRACE", "prompt": "prompts/general/shared-memory-race-finder.md" }, { "bug_class": "static-mut-race", "prefix": "STATICMUT", "prompt": "prompts/general/static-mut-race-finder.md", "requires": ["has_unsafe"] } ] }

关键信息:

  • Finding ID 前缀:SENDSYNCBOUND,与ATOMICRACE、UNSAFESYNC、SHMRACE、STATICMUT并列。
  • 簇门控:gate: "has_concurrency"——当 Phase 1 能力探测确认审计范围内存在并发相关代码(std::thread、Atomic*、static mut、UnsafeCell、unsafe impl Send/Sync、mmap 共享内存、信号处理等,具体见 SKILL.md 中的探测正则)时,该簇才被选中。
  • 无requires约束:与同簇的UNSAFESYNC(requires: ["has_unsafe"])和STATICMUT(同样需要has_unsafe)不同,send-sync-bounds没有额外的requires字段——只要has_concurrency=true就会运行。

这一点有测试用例佐证。scripts/test_gating.py 中test_data_race_unsafe_passes_filtered断言:当has_concurrency=true, has_unsafe=false时,concurrency-data-race簇仍然被选中,且SENDSYNCBOUND、ATOMICRACE、SHMRACE都在 pass 列表中,只有UNSAFESYNC与STATICMUT被过滤掉。这背后的逻辑是:Send/Sync边界清洗虽然常与unsafe代码相伴,但其检查点(spawn 原语、transmute 调用)在代码中并不一定以unsafe关键字形式出现,因此不需要用has_unsafe门控。

该 pass 在 SARIF 导出中的简短描述为"Missing or incorrect Send or Sync bounds"(见 scripts/generate_sarif.py),可作为对SENDSYNCBOUND发现项的一行概括。

三、Bug shape 详解:哪些形态真正不健全

按文档(send-sync-bounds-finder.md)的分类,本 pass 只处理"通过 unsafe 清洗缺失约束"的形态,可以归纳为四类:

  1. 裸thread::Builder+mem::transmute:用thread::Builder::new().spawn(...)替代thread::spawn(后者强制Send + 'static),再通过transmute把闭包或生命周期改成可发送/可 'static 的类型。类型检查被transmute整体绕过,原类型的!Send约束随之失效。
  2. FFI / 自定义线程创建:通过extern "C"或自定义的线程库接口把闭包指针传给其他线程,Rust 编译器无法对 FFI 边界做Send/Sync检查。
  3. std::thread::scope误用:scoped 线程允许借用非'static数据,若使用不当(如把本应在 scope 内使用的引用/闭包通过 unsafe 手段送出 scope 生命周期),则'static约束被破坏。
  4. transmute捏造约束:用mem::transmute把非Send/非Sync类型直接改写成携带这些约束的类型,凭空"制造"缺失的 bound。

判断的核心原则是约束是否真的没有被强制:代码之所以能编译,仅仅是因为某个unsafe构造绕过了它。而直接的std::thread::spawn/tokio::spawn/rayon::spawn自身不可能是漏洞点——它强制Send + 'static,因此任何能在它周围编译通过的代码,其实已经携带了该约束。

四、双门控判定:两个 Gate 必须同时满足

文档明确了两个必须同时成立的 Gate:

Gate 1 —— 存在 unsafe 清洗路径。一个值/闭包通过以下方式到达线程或通道边界:unsafespawn 原语(裸thread::Builder+transmute、FFI 线程创建、scoped 线程误用),或一个捏造缺失Send/Sync约束的transmute。注意:手动unsafe impl Send/Sync的定义是UNSAFESYNC的事,不要为 impl 本身提交SENDSYNCBOUND发现项(下方搜索正则找到 impl 的唯一目的是:追踪是否存在其他unsafe 清洗路径把它的值带过了线程)。

Gate 2 —— 缺失的约束确实未被 sink 强制。代码能编译仅仅是因为 unsafe 构造绕过了它。如果 sink 本身就是std::thread::spawn这类强制Send + 'static的原语,则不存在不健全性,因为能编译就意味着约束已经满足。

两个 Gate 的关系是"AND":只找到 unsafe 构造但没有确认约束未被强制,或只发现约束缺失但没有确认清洗路径,都不能提交SENDSYNCBOUND发现项。

五、误报(FPs)清单:哪些场景不该提交

文档明确列出的三个 FP 场景,在实际审计中非常常见:

  1. 标准 spawn 的包装器忘记显式写Send约束:一个包裹裸std::thread::spawn/tokio::spawn/rayon::spawn的包装器省略了显式Sendbound——如果约束不成立它根本无法编译,所以没有 bug。
  2. 线程本地执行器(thread-local executor):tokio::task::spawn_local这类不需要Send的场景。
  3. 内部 trait 约束已隐含Send:例如T: Future<Output: Send> + Send这类写法,Send已经由外部约束保证,不构成缺失。

判断 FP 的核心思路与 Gate 2 一脉相承:只要约束最终是由编译器强制的,无论它被显式写出、被推导出来还是被包装器"忘记",都不构成SENDSYNCBOUND漏洞——真正的问题永远出在 unsafe 清洗路径上。

六、搜索模式(Search seeds):用 ripgrep 定位候选点

文档为审计 worker 提供了一组 ripgrep 语法(\s、\d、\b、\w)的搜索种子,用于在审计范围内定位候选点:

\bspawn\s*[(<] FnOnce|FnMut|Fn\b where|:\s*Send unsafe\s+impl\b.*\b(Send|Sync)\b|thread::Builder|\btransmute\b|thread::scope

从源码结构看,这些种子的设计意图如下:

  • 第一行匹配所有spawn调用(含thread::Builder::spawn、std::thread::spawn、tokio::spawn等),作为穿越线程边界的锚点;
  • 第二、三行匹配闭包 trait 约束(FnOnce/FnMut/Fn)与where子句 /: Send写法,用于确认约束是否被显式声明;
  • 第四行同时覆盖四类清洗路径的关键词:unsafe impl ... Send/Sync(用于追踪,而非直接作为发现点)、thread::Builder(裸 Builder spawn)、transmute(约束捏造)、thread::scope(scoped 线程误用)。

worker 在运行该 pass 时,应通过Bash调用rg执行这些种子(详见 rust-review-worker.md 的"Search withrg"规则:rg缺失时需显式降级为grep -E并转换字符类,绝不能让\s被某些 grep 静默当作字面量s而返回假空结果)。命中后用Read逐一验证:确认值确实跨过线程边界、确认约束确实未被强制、排除上述 FP 场景。

七、同簇 Deconfliction:与 UNSAFESYNC 等 pass 的边界

concurrency-data-race簇的五个 pass 覆盖数据竞争的多个层面,簇级提示(concurrency-data-race.md)明确定义了彼此的去重(Deconfliction)规则:

  • UNSAFESYNCvsSENDSYNCBOUND(与本 pass 最相关):手动unsafe impl Send/Sync的定义(针对非线程安全负载)属于UNSAFESYNC;SENDSYNCBOUND只在不存在的约束被unsafe spawn 原语 / transmute 清洗跨线程时提交(不由手动 impl 本身触发)。当同一个unsafe impl Send for W {}既存在又让它的值跨过线程时,在 impl 位置提交UNSAFESYNC,不要为同一个 impl 重复提交SENDSYNCBOUND。
  • STATICMUTvsUNSAFESYNC:static mut竞争不会被包装器上不健全的Syncimpl 修复,优先在static mut位置提交STATICMUT。
  • STATICMUTvsATOMICRACE:无同步的static mut(或对其的裸访问)vs 错误的Atomic*排序,不同位置可同时提交两者。
  • SHMRACEvsSTATICMUT:跨进程 mmap vs 进程内static mut,信任边界不同。

与SENDSYNCBOUND最需要警惕的是第一条:发现unsafe impl Send时,先判断它本身是否是漏洞(若是,归UNSAFESYNC),再判断是否有其他unsafe 清洗路径利用了它跨线程传递(这才是SENDSYNCBOUND的切入点)。姊妹 pass unsafe-sync-impl-finder.md 对UNSAFESYNC的判定有更详细的展开(非线程安全字段、&self内部变更、// SAFETY:注释不足以证明健全性等),可作为交叉参考。

八、修复建议(Patch):恢复编译期强制

文档给出的修复方向清晰且可操作:

  1. 传播约束:在包装器上显式传播+ Send + 'static,让约束重新被编译器强制。这是首选修复——直接消除"约束缺失"这一根本原因。
  2. 替换清洗原语:把unsafespawn /transmute清洗路径替换为健全的原语(如std::thread::spawn/tokio::spawn/rayon::spawn),让Send/Sync在编译期被强制。这样任何不满足约束的负载会直接编译失败,而不是在运行时产生数据竞争或未定义行为。
  3. 区分修复归属:如果是手动unsafe impl Send/Sync本身定义错误,则在UNSAFESYNC下修复(移除手动 impl 让类型保持!Send/!Sync,或使负载真正线程安全),而不是本 pass。

修复的最终目标是把线程安全责任交还给编译器:非Send数据不应无声地跨线程流动,'static借用不应被 unsafe 手段送出生命周期。

九、发现项在审计流水线中的流转

SENDSYNCBOUND发现项并不是终稿,它会经历 rust-review 的三阶段流水线(架构详见 SKILL.md):

  1. Worker 阶段:运行concurrency-data-race簇的 worker 按 manifest.json 的顺序执行ATOMICRACE→UNSAFESYNC→SENDSYNCBOUND→SHMRACE→STATICMUT,确认的 bug 写入findings/SENDSYNCBOUND-NNN.md(YAML frontmatter 含id、bug_class: send-sync-bounds、location、function、confidence、worker),并在 coverage 文件中记录filed:或cleared状态。
  2. Dedup 阶段:rust-review-dedup-judge按(path, line, bug_class)等键合并重复项,并处理与同簇其他 pass 的交叉重复。
  3. FP + Severity 阶段:rust-review-fp-judge为每个主发现项分配fp_verdict(TRUE_POSITIVE / LIKELY_TP / LIKELY_FP / FALSE_POSITIVE / OUT_OF_SCOPE)与severity/attack_vector/exploitability,最终汇入REPORT.md与REPORT.sarif。

这意味着审计中发现的SENDSYNCBOUND候选会经过误报裁决与严重性评级,最终以标准化的 SARIF 格式输出,便于接入现有安全工具链。

结语

send-sync-bounds-finder是 rust-review 并发数据竞争簇中一个定位精准的检测 pass:它不重复编译器已经完成的工作(标准 spawn 的约束强制),而是聚焦于unsafe清洗路径——thread::Builder+transmute、FFI 线程创建、scoped 线程误用、约束捏造——这些让Send/Sync/'static约束形同虚设的构造。掌握它的 Bug shape、双 Gate 判定与同簇 Deconfliction 规则,就能在 Rust 安全审计中准确识别这类被编译器检查网漏掉的线程安全漏洞,并将修复锚定在"恢复编译期强制"这一正确方向上。

  • AI 技能
  • AI 插件
  • 应用安全
  • 网络安全
  • AI 评测

【免费下载链接】skills

Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows

项目地址:https://gitcode.com/gh_mirrors/skills8/skills
点击查看免费下载

相关推荐

上一篇:dynamic-datasource多模块依赖版本:属性统一终极指南
下一篇:webpack 命名 Chunk 实战解析:同名代码分割点如何合并、Named Chunk 如何生成与输出(examples/named-chunks 源码级拆解)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询