- AI 技能
- AI 插件
- 应用安全
- 网络安全
- AI 评测
【免费下载链接】skills
Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows
本篇技术指南聚焦 Trail of Bitsrust-review插件中RAWFD(raw-fd-lifecycle)漏洞发现规则的完整解读:为什么RawFd是一个以约定而非类型承载所有权的危险抽象,from_raw_fd/into_raw_fd/as_raw_fd如何在 Rust 代码中引入 double-close(CWE-672 / CWE-415 类似形态)与 fd 泄漏(表耗尽 DoS),以及审计时应遵循的验证门(Gates)、误报清单(FPs)与修复方案。读完本文,你将掌握一套可复现的原始文件描述符生命周期审计方法论,并了解它在 rust-review 审计流水线中如何被调度、验证与判定。
背景:RawFd是裸i32,所有权只是约定
在 Rust 中,操作系统文件描述符最原始的形态是RawFd——一个裸的i32整数。std::os::fd::RawFd本身不携带任何资源信息:它不拥有底层描述符,不实现Drop,编译器也无法对它进行任何所有权检查。换句话说,谁应该 close 这个 fd,完全靠代码作者之间的默契约定来维持,类型系统不提供任何保障。
这与 Rust 官方倡导的 I/O safety 类型体系形成鲜明对比:
OwnedFd:拥有一个 fd,Drop时恰好 close 一次(RAII);BorrowedFd:借用一个 fd,生命周期由借入者担保,Drop时绝不 close;File/TcpStream等标准库类型:内部持有OwnedFd,同样保证恰好一次 close。
问题恰恰出在两者之间的转换边界。RawFd与上述类型之间通过三个核心函数互相转换:
| 函数 | 语义 | 所有权效果 |
|---|---|---|
from_raw_fd(fd: RawFd) -> OwnedFd | 取得原始 fd 的所有权 | 从此OwnedFd负责 close,原RawFd数字不得再被使用 |
into_raw_fd(self) -> RawFd | 交出所有权,返回裸整数 | 原OwnedFd不再负责 close,调用方必须自行 close |
as_raw_fd(&self) -> RawFd | 仅借用查看 | 不转移所有权,返回的整数仅在当时有效 |
正如关联文档 raw-fd-lifecycle-finder.md 所概括的:RawFd是裸i32——所有权是约定,不是类型。一旦开发者绕过OwnedFd/BorrowedFd直接摆弄裸整数,就同时打开了 double-close 和泄漏两类漏洞的窗口。
两种失败模式:double-close 与 missing-close(泄漏)
RAWFD规则关注两类截然不同、但同样源于"所有权约定被破坏"的失败模式。
模式一:double-close——同一个 fd 被 close 两次
当同一个底层描述符出现两个负责 close 的"所有者"时,第二个 close 命中一个已被回收的 fd 编号,而这个编号此刻可能正指向一个完全无关的新资源(新打开的 socket、日志文件、共享内存句柄……)。其后果通常是关闭无关资源导致的功能中断,严重时可被利用为资源破坏(这正是 CWE-672"操作后释放资源的使用"以及 CWE-415"双重释放"的 fd 侧类比)。
典型触发路径:
from_raw_fd被对同一个 fd 调用了两次——第一次生成了一个OwnedFd,第二次又从某个还活着的来源拿同一个整数重新构造所有者;- 来源
File/OwnedFd仍然存活时,又用它的as_raw_fd()结果喂给from_raw_fd——此时原始所有者与新建的所有者共享同一底层资源; - 借用值被当作所有权转移——把
as_raw_fd()(语义是借用)的结果传给from_raw_fd(语义是取得所有权),借出方稍后 close,借入方也 close。
模式二:missing-close(泄漏)——fd 从未被关闭
当一个确定拥有fd 的值被当成普通整数丢弃、被mem::forget遗忘、或被包进ManuallyDrop而从不触发 close 时,底层描述符就永久泄漏。长期运行的服务若持续泄漏 fd,最终会耗尽 fd 表(进程级RLIMIT_NOFILE限制),此后一切open/accept/connect全部失败——这是经典的资源耗尽型 DoS。
典型触发路径:
into_raw_fd()返回的裸整数被直接 drop(没有后续close/re-wrap);libc::open/libc::dup/libc::pipe等裸系统调用返回的 fd 被当作i32处理而非包进OwnedFd;- 一个
File/OwnedFd被mem::forget或ManuallyDrop包裹,且没有任何其他路径负责关闭它(包括?提前返回和 panic-unwind 路径)。
规则明确声明:同样的逻辑完全适用于 Windows 上的RawHandle/RawSocket,即from_raw_handle/into_raw_handle与句柄/套接字类型。因此RAWFD的审计模板同时覆盖 Unix 和 Windows 两条平台线。
验证门(Gates):确认漏洞成立的三条/两条必要条件
RAWFD规则为两种失败模式分别定义了严格的验证门。审计时所有门都必须通过才能提交 finding,缺一不可。
double-close 的三条 Gates
- 到达所有权接管点:一个
RawFd/RawHandle到达了from_raw_fd/from_raw_handle(该调用语义上取得所有权)。 - 存在第二个所有者:同一个 fd 在别处也"被拥有"——要么存在第二次
from_raw_fd,要么来源File/OwnedFd仍然存活,要么该值来自as_raw_fd()(本质是借用,却被当所有权使用)。 - 两条路径都可达 close:两个所有者在某条可行控制流上都会走到 close,且没有
mem::forget/into_raw_fd中和掉其中一个,使第二个 close 不会发生。
注意第 3 条的关键词是"可行路径":审计必须证明存在至少一条从入口到双 close 的实际可达路径,而不是纯理论推演。
missing-close(泄漏)的两条 Gates
- 产出了拥有的 fd:
into_raw_fd()或裸系统调用(open/dup/pipe)产出了一个确定归属的 fd。 - 所有权被丢弃且无补救:该 fd 被当作普通整数 drop、被
mem::forget遗忘、或其所有者被ManuallyDrop包裹,且在每一条路径上(包括?提前返回与 panic-unwind)都没有匹配的close或 re-wrap(重新包回OwnedFd)。
泄漏门的第 2 条对路径完备性要求很高:即使正常路径上 close 了,只要某条错误路径(?传播、panic)会跳过 close,规则仍然成立。
误报清单(FPs):什么样的代码不该报
RAWFD规则明确给出了四类必须排除的误报形态。这些反例本质上都是"所有权约定没有被破坏"的情形:
- 代码始终停留在类型安全层:fd 一直保存在
OwnedFd/File/BorrowedFd中——RAII 保证恰好 close 一次,借用方永远不会 close。这是最安全、也最常见的形态。 - 所有权已立即移交:
into_raw_fd()的返回值立即交给另一个所有者(例如立刻传给from_raw_fd构造新OwnedFd)——所有权发生了转移而非泄漏。 as_raw_fd()仅作为系统调用实参:借用值只用于调用一次syscall(...),从未被包进任何所有者——借用没有变成所有权,不会产生双 close。mem::forget/ManuallyDrop伴随文档化的 FFI/所有者交接:存在// SAFETY:或等价注释明确说明资源已移交给外部实体(例如 C 库接管后由对方负责释放)——此时"不 close"是契约的一部分,不是泄漏。
这四条反例与 destructor-skip-finder.md(DROPSKIP规则)中"泄漏/跳过是有意为之且已文档化"的 FP 条款相互呼应,共同构成资源生命周期类审计的误报防线。
修复方案(Patch):让 RAII 替你管理生命周期
规则给出的修复指引遵循同一原则:回到类型系统,把"约定"变回"类型"。
- 默认形态:使用
OwnedFd/BorrowedFd/File,让 RAII 恰好 close 一次——不要手动摆弄裸i32。 - 转移所有权:用
OwnedFd传递;BorrowedFd的底层借用语义与AsFdtrait 用于跨函数传递时保持借用关系。 - 必须持有裸 fd 时:将其包裹进恰好一个
OwnedFd,并通过From/Into转换(OwnedFd与RawFd之间的安全转换路径)传递,绝不再从as_raw_fd()重新推导第二个所有者。
一句话概括:如果你不得不用裸 fd,就让"恰好一个OwnedFd"成为唯一的 close 决策者,其他位置一律以借用(BorrowedFd/AsFd)或From/Into转换来引用,而不是再造一个所有者。
在 rust-review 流水线中的位置与调度
RAWFD不是孤立的一条规则,它是 rust-review 插件resource-handling集群(Cluster)的成员。相关证据可以在仓库中直接核验:
集群归属与门控
在 manifest.json 中,resource-handling集群的gate为always,包含两个 pass:
| Bug class | 前缀 | 子提示文件 |
|---|---|---|
raw-fd-lifecycle | RAWFD | raw-fd-lifecycle-finder.md |
destructor-skip | DROPSKIP | destructor-skip-finder.md |
gate: always意味着:无论目标 crate 是否含有unsafe、FFI 或并发代码,该集群都会运行。这是因为 fd/句柄生命周期错误在纯 safe Rust 中同样可能发生(例如into_raw_fd是 safe 函数),因此RAWFD是每次审计的常驻检查项。从 README.md 可以看到,这套 bug-class 覆盖来自对 RustSec 咨询数据库(1,078 条目)与真实审计中经验 bug 形态的研究积累。
集群级 Phase A:搜索种子
resource-handling.md 为该集群定义了统一的 Phase A 库存搜索(以 ripgrep 正则形式下发,worker 需用rg执行):
rg seed: "\b(from_raw_fd|into_raw_fd|as_raw_fd)\b" rg seed: "\b(RawFd|OwnedFd|BorrowedFd)\b" rg seed: "libc::(close|dup|dup2|open|pipe|socket)\b" rg seed: "\b(RawHandle|RawSocket|from_raw_handle|into_raw_handle)\b" rg seed: "\bmem::forget\b|\bManuallyDrop\b" rg seed: "process::exit|libc::exit"这些种子把审计范围锁定在四条线索上:fd 类型与三转换函数、裸 libc 系统调用、Windows 句柄/套接字等价物、以及可能绕过 close 的mem::forget/ManuallyDrop/process::exit。拿到候选后,worker 依据RAWFD的 Gates 逐一验证,而不是见种子就报。
与 DROPSKIP 的去冲突规则
RAWFD与同集群的DROPSKIP在边界上有明确分工(见 resource-handling.md 的 Deconfliction 一节):
- 文件描述符/句柄/套接字的生命周期被错误处理(double-close、泄漏、
from_raw_fd未伴随所有权转移)→ 报RAWFD; - 其他安全相关
Drop被mem::forget/ManuallyDrop/process::exit跳过(密钥清理、数据库连接、事务、锁)→ 报DROPSKIP; - 特别地,当
mem::forget/ManuallyDrop/exit遗弃了一个 fd 支持的值(OwnedFd、File、TcpStream)时,只报RAWFD(fd 泄漏才是精确的 bug),同一位置不得重复报DROPSKIP; - 而 FFI 所有权内存的跨分配器释放不匹配则归属
ffi-cross-language集群的FOREIGNDROP,不是RAWFD/DROPSKIP。
这套去冲突规则保证了同一根因不会被多个 pass 重复报告,后续 dedup 阶段(rust-review-dedup-judge)的合并压力也随之降低。
从候选到 finding:worker 的验证与产出协议
当RAWFDpass 被调度给某个rust-review-worker时,该 worker 遵循 agents/rust-review-worker.md 定义的严格协议:
- 执行 Phase A 库存搜索后,针对每条候选做数据流验证:从攻击者可控的入口(网络、文件、IPC、FFI 输入)追踪到
from_raw_fd/裸 syscall/mem::forget汇点,检查是否存在缓解措施(如// SAFETY:是否真实成立、上游是否保证唯一 close)。 - 确认的漏洞按统一模板写入
{output_dir}/findings/RAWFD-NNN.md,YAML frontmatter 含id、bug_class、title、location(单个path:line)、function、confidence、worker,正文含 Description / Code / Data flow / Reachability trace / Impact / Mitigations checked / Recommendation 七个固定小节。 - 写入 coverage-gate 文件:每 pass 必须有
filed: RAWFD-001或cleared (...)记录,不允许skipped:——即"没搜到任何from_raw_fd/as_raw_fd候选"才能写cleared,未运行搜索就报"无发现"属于 coverage 违规。
worker 提交后,finding 依次经过 dedup-judge 合并重复、fp-judge 判定fp_verdict与 severity(威胁模型REMOTE/LOCAL_UNPRIVILEGED直接影响结论,见 agents/rust-review-fp-judge.md),最终汇入REPORT.md与REPORT.sarif。整套流水线的编排细节(Phase 0–9、worker 波次、重试分类器)可查阅 SKILL.md。
审计清单速查
把上述内容浓缩为一份可直接对照检查的清单,供实际审计RawFd代码时逐项核对:
- 代码中是否出现
RawFd/RawHandle/RawSocket裸类型,或from_raw_fd/into_raw_fd/as_raw_fd三函数的混用? - 每个
from_raw_fd的输入,其 fd 数字是否只来自唯一的所有权来源,且该来源已确认让渡所有权? - 每个
as_raw_fd()返回值是否仅用于瞬时 syscall 参数,从未被包进任何from_raw_fd? into_raw_fd()的返回值是否在每条路径(含?与 panic-unwind)上都有对应的close或 re-wrap?mem::forget/ManuallyDrop是否伴随// SAFETY:文档化的外部交接?如果没有,就是泄漏候选。libc::open/dup/pipe的返回值是否立即被包进OwnedFd,还是当作i32随处传递?- 同一 fd 是否存在两个"都可能 close"的所有者?若是,能否证明其中一条路径被
mem::forget/into_raw_fd中和?
核心结论:RawFd的裸i32形态决定了所有权只能靠约定维持,而约定必然有被打破的一天。RAWFD规则的价值在于把"打破约定"这件事从模糊的代码嗅觉转化为可验证的 Gates——double-close 必须同时满足"到达接管点 + 存在第二所有者 + 双 close 路径可达"三条,泄漏必须满足"产出拥有的 fd + 全路径无 close"两条;再辅以四类 FP 反例和"恰好一个OwnedFd"的修复铁律,你就能系统性地消灭 Rust 代码中最隐蔽的一类操作系统资源错误。
- AI 技能
- AI 插件
- 应用安全
- 网络安全
- AI 评测
【免费下载链接】skills
Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows
相关推荐
rust-review 资源处理集群深度解析:RAWFD 文件描述符生命周期与 DROPSKIP 析构器跳过的审计方法
rust review 资源处理集群深度解析:RAWFD 文件描述符生命周期与 DROPSKIP 析构器跳过的审计方法 导读 本文聚焦 Trail of Bit
AI 技能AI 插件应用安全网络安全AI 评测PhantomData 实战解析:用 BorrowedFd 与 OwnedFd 在 Rust 中建模文件描述符生命周期
PhantomData 实战解析:用 BorrowedFd 与 OwnedFd 在 Rust 中建模文件描述符生命周期 本文是 Google Android 团
文档教程解决Alacritty IPC文件描述符泄漏实战指南
解决Alacritty IPC文件描述符泄漏实战指南 你是否遇到过Alacritty终端长期运行后卡顿、资源耗尽甚至崩溃的问题?本文将深入剖析IPC(Inter
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考