六年362补丁,Linux终斩strncpy

linuxcsecuritykernel

数据源:HN + Lobsters · HN

2026 年 6 月 20 日,Linus Torvalds 合并了一个看起来平淡无奇的 Pull Request。提交信息只有一行:“Merge tag ‘strncpy-removal-v7.2-rc1’“。但这条合并的背后,是 362 个补丁、70 位贡献者、整整六年的持续工作。最终结果:strncpy——这个在 C 语言生态里存活了近半个世纪的函数——从 Linux 内核中彻底消失了。

如果说内核社区有一个公认的”最令人头疼的 API”,strncpy 几乎毫无悬念。用 Phoronix 报道中的原话:它是”persistent source of bugs”。但笔者查阅历史资料后发现,这个函数当年被引入 C 标准库时,本意恰恰是解决安全问题。

strncpy 到底哪里出了问题

strncpy(dest, src, n) 的语义可以概括为:复制最多 n 个字符,如果 srcn 短,用 \0 填充剩余空间。听起来很安全,对吧?但问题藏在细节里。

第一个问题:如果 src 的长度大于等于 nstrncpy 不会dest 末尾添加 \0。调用者拿到的是一个没有终止符的字符数组,后续任何以 strlenstrcmp 为代表的字符串操作都会越界读出,轻则泄露内核内存,重则触发可利用的安全漏洞。内核文档对此的描述直白到近乎残忍:“Use of strncpy() does not guarantee that the destination buffer will be NUL terminated.”

第二个问题:当 srcn 短时,strncpy 会用 \0 把整个 dest 填满。这个”零填充”看似无害,但对于大型固定缓冲区(比如网络包中的字段),无意义地写入几百字节的零值本身就是性能浪费。内核是性能敏感的环境,这种”安全税”交得并不值。

第三个问题:它的返回值是一个指向 dest 的指针,而不是实际复制的字节数。调用者如果要判断是否发生了截断,必须自己算一遍 strlen(src) >= n。这种设计让正确使用变得异常繁琐,于是大量开发者选择了”差不多就行”——而这恰恰是安全漏洞的温床。

Justin Stitt,一位来自 Google 的内核贡献者,在这六年里一个人提交了 211 个补丁,占了全部工作量的 58%。他在 Hacker News 的讨论中被多次提及:“Whenever I’ve been asked to review C code, I always looked for strncpy and always found a bug with it.”

替代方案:一个函数变成了一族函数

内核构建了一套语义清晰的字符串复制工具体系,每种场景都有专用函数:

  • 需要 NUL 结尾的字符串拷贝strscpy()。它始终在目标缓冲区末尾写 \0,返回值是实际复制的字节数(截断时返回负 errno)。这是最常用的替代品。
  • 需要 NUL 结尾 + 零填充strscpy_pad()。适合需要清空敏感数据的场景。
  • 拷贝到不需要 NUL 结尾的固定宽度字段(比如文件系统里的 char 数组)→ strtomem()strtomem_pad()
  • 已知长度的带填充边界拷贝memcpy_and_pad()

这套 API 设计的核心原则是:每个函数的契约都是明确的,不存在”大部分情况下安全”的模糊地带strscpy 保证 NUL 终止,strtomem 保证不写终止符。调用者必须显式选择它要的是什么语义。

从内核文档中的”Deprecated Interfaces”章节可以看到,strncpy 的替代方案经过了多轮讨论才定型。早期还有过 strlcpy,但 strlcpy 也有自己的问题——它会读取整个源字符串(因为返回值要匹配 strlen),即使目标缓冲区很小,这种”过度读取”既低效也可能导致线性越界。所以 strlcpy 本身也被标记为弃用,推荐使用 strscpy

六年,362 个补丁,为什么这么久

如果你以为这只是一次”全局搜索替换”,那说明你还没接触过内核的规模。Linux 内核有数千万行代码,strncpy 散布在从文件系统、网络栈到设备驱动的各个角落。更麻烦的是,很多调用并不是”简单地换一个函数名”就能解决——因为不同调用点对 strncpy 的依赖语义完全不同。

有些地方依赖 strncpy 的零填充行为来清空剩余字节。有些地方依赖它写 NUL 终止符(因为目标字段是非字符串的固定宽度字段)。还有一些地方既用了 strncpy,又手动在后面补了 \0——这种双重保险反而藏了 bug。

移除此类 API 需要逐一审查每个调用点:它到底想要什么语义?然后选择正确的替代函数。如果选错——比如在需要 NUL 终止的场景用了 strtomem,或者在非字符串字段里用了 strscpy——反而会引入新 bug。

Kees Cook,内核的维护者之一,在 2026 年 3 月的一条 Mastodon 帖子中分享了一组数据:362 个提交来自 70 位贡献者,其中 Justin Stitt 一人贡献了 211 个。Xu Panda(22 个)、Kees Cook 本人(21 个)、Thorsten Blum(17 个)、Arnd Bergmann(12 个)紧随其后。这是一场持续多年的分布式工程战役,362 个提交分布在六年之间。

在 Hacker News 讨论中,有评论者将这类工作形容为:“the real work of systems engineering”。内核里的大多数新特性在几天内就能合并,但真正难的是移除那些”看起来能用、实际上有毒”的遗留接口。这需要维护者长期跟踪、贡献者持续清理、以及工具链的同步进化。

静态分析工具的关键角色

strncpy 能够在长达六年的时间跨度里被逐步清除,另一个关键因素是内核编译工具的进步。GCC 和 Clang 的 -Wstringop-truncation 警告能够检测到 strncpy 可能不 NUL 终止目标缓冲区的情况;__attribute__((__nonstring__)) 标记让开发者可以明确告诉编译器”这个字符数组不是字符串”。

Kees Cook 主导的”fortify-source”机制在内核中构建了一层编译时的缓冲区溢出检测。当代码使用 strcpymemcpy 等函数时,__builtin_object_size 会在编译时检查目标缓冲区大小是否足够。这套机制反过来也给移除 strncpy 提供了信心——即使有遗漏的调用点,fortify 也能在编译期或运行时捕获问题。

这场迁移的内核版本路线同样值得关注。Kees Cook 先是在 v7.0-rc2 基础上清理了最后 6 个 strncpy 实例,然后逐一移除了每个 CPU 架构(alpha、m68k、powerpc、x86、xtensa)的架构特定 strncpy 实现。最终,strncpy 的通用实现和所有架构实现统一删除,合并进 Linux 7.2。

更大的图景:C 语言的”历史债”

strncpy 的移除在 Hacker News 上引发了远超出这次合并本身的讨论。一条获得大量赞同的评论指出:“the zero terminated string is computing’s biggest mistake。” 尽管这听起来有些绝对,但背后的逻辑是扎实的:以 \0 作为字符串终止符的设计,将字符串的长度信息与数据本身分离,导致任何操作都必须从头扫描才能知道字符串的长度。

Dennis Ritchie 本人早在 1990 年就提议过给 C 语言添加”fat pointer”类型——一个包含指针和长度信息的复合类型。Walter Bright 在 2007 年发表的《C’s Biggest Mistake》进一步阐述了相同思路。但这些建议都没有进入 C99、C11 或 C23 标准。

Linux 内核走的是另一条路:既然语言层面不提供安全抽象,那就自己在内核 API 层面构建。strscpystrtomemfortify__nonstring——这些都不是 C 标准库的一部分,但它们是内核内部的事实标准。代价是工作量巨大,但收益也清晰可见:移除了一个已知的 bug 类别。

争议与保留意见

任何大规模 API 迁移都会面临反对声音。strncpy 的移除也不例外。在内核邮件列表上,有维护者质疑某些替换是否合理。比如在 XFS 子系统中,有补丁将 strncpy 替换为 strscpy 时遭到了反对:“It removes explicit source buffer overrun protection whilst making the incorrect assumption that the callers need to be protected from unterminated strings in the destination buffer.”

这也是 strtomemstrscpy 并行存在的根本原因——内核维护者承认有些场景下”不 NUL 终止”原本就是预期行为,强行引入 NUL 终止反而可能掩盖更深层的边界问题。公平地讲,strncpy 被移除的过程是一次谨慎的、逐场景审计后的分类处理,虽改动了大量代码,但每处都经过了独立判断。

结语

362 个补丁,70 个人,211 个来自 Justin Stitt。

六年听起来很长。但如果看内核的演进节奏——从 Git 引入、到设备模型重构、再到 Rust 的初步集成——六年恰好是内核完成一次深度基建翻新的典型周期。strncpy 的移除了结了一整类在内核里潜伏了二十多年的内存安全问题,产生的影响远不止一个函数名的消失。

就工程实践而言,这件事给了一个可验证的结论:在足够大的系统里,移除一个错误的 API 比引入十个新 API 更难。而真正的安全性,往往藏在那些无声的清理工作里。


以上分析基于目前的公开信息和社区讨论。如果有不同视角或补充信息,愿意交流。