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 个字符,如果 src 比 n 短,用 \0 填充剩余空间。听起来很安全,对吧?但问题藏在细节里。
第一个问题:如果 src 的长度大于等于 n,strncpy 不会在 dest 末尾添加 \0。调用者拿到的是一个没有终止符的字符数组,后续任何以 strlen、strcmp 为代表的字符串操作都会越界读出,轻则泄露内核内存,重则触发可利用的安全漏洞。内核文档对此的描述直白到近乎残忍:“Use of strncpy() does not guarantee that the destination buffer will be NUL terminated.”
第二个问题:当 src 比 n 短时,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”机制在内核中构建了一层编译时的缓冲区溢出检测。当代码使用 strcpy、memcpy 等函数时,__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 层面构建。strscpy、strtomem、fortify、__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.”
这也是 strtomem 和 strscpy 并行存在的根本原因——内核维护者承认有些场景下”不 NUL 终止”原本就是预期行为,强行引入 NUL 终止反而可能掩盖更深层的边界问题。公平地讲,strncpy 被移除的过程是一次谨慎的、逐场景审计后的分类处理,虽改动了大量代码,但每处都经过了独立判断。
结语
362 个补丁,70 个人,211 个来自 Justin Stitt。
六年听起来很长。但如果看内核的演进节奏——从 Git 引入、到设备模型重构、再到 Rust 的初步集成——六年恰好是内核完成一次深度基建翻新的典型周期。strncpy 的移除了结了一整类在内核里潜伏了二十多年的内存安全问题,产生的影响远不止一个函数名的消失。
就工程实践而言,这件事给了一个可验证的结论:在足够大的系统里,移除一个错误的 API 比引入十个新 API 更难。而真正的安全性,往往藏在那些无声的清理工作里。
以上分析基于目前的公开信息和社区讨论。如果有不同视角或补充信息,愿意交流。