Hyaika Blog

Penguin is all you need

技术

Linux 终于把 strncpy 赶出去了——362 次提交,六年,和一个躺了三十多年的 bug

Linux 终于把 strncpy 赶出去了——362 次提交,六年,和一个躺了三十多年的 bug

Linux 终于把 strncpy 赶出去了——362 次提交,六年,和一个躺了三十多年的 bug

目录

  • strncpy 干了什么坏事
  • 为什么用了六年:比「改一个名字」难得多
  • 替代方案:五种工具,对应五种场景
  • 个人验证:我跑的内核还有 strncpy 吗
  • 最后一个问题:strcpy 呢?

strncpy 干了什么坏事

如果你写过 C,大概率见过这段代码:

char buf[64];
strncpy(buf, source, sizeof(buf));

看起来没问题对吧?限制拷贝长度,防止缓冲区溢出。标准教的一直是这样。

但这段代码有一个隐形的陷阱:如果源字符串的长度 ≥ 目标缓冲区的大小,strncpy 不会在末尾加 '\0'。

换句话说,buf 可能不是一个合法的 C 字符串。你之后如果 strlen(buf) 或者 printf("%s", buf) ——它会一路读下去,直到在内存的某个地方碰上一个 '\0'。可能读到一个字节,也可能读到一百万个。

这个行为在 kernel 开发里被反复踩过。Linux 内核的文档里有一句话很直白:strncpy 是「持续的 bug 来源」(persistent source of bugs)。

问题还不止 NUL 终止。strncpy 的另一个行为是:拷贝完后,如果源比目标短,它会把剩下的所有空间全部填上 '\0'。这听起来无害,但对于 kernel 开发来说——那些零填充操作,在热路径上就是实打实的性能成本。

所以 strncpy 的错误可以归为三类:

  1. NUL 终止缺失 — 最危险,可能导致信息泄露或安全漏洞
  2. 零填充浪费 — 缓冲区越大,浪费越严重
  3. 语义混乱 — 三个参数里两个是长度,不同人的理解完全相反

2026 年 6 月 20 日,Linus Torvalds 合并了一个提交。Linux 7.2 的代码库里,strncpy 的最后一个使用者被移除了。

为什么用了六年:比「改一个名字」难得多

362 个提交。六年。听起来像是在说一个巨型项目换了一轮架构,实际上只是在改一个函数。

为什么这么慢?

第一,strncpy 的使用模式并不统一。有的调用者以为它总会加 NUL 终止,有的知道它不加但围绕这个特性做了后续操作,还有的干脆就是在结构体里用它做固定宽度字段拷贝——对于最后一种情况,strncpy 的零填充行为反而是他们需要的。你不能简单地把所有 strncpy(...) 替换成 strscpy(...) 然后编译通过就完事——你得逐一理解每处调用背后的意图。

第二,不同的使用场景需要不同的替代函数。没有「万能替换」。对于 NUL 终止的字符串:用 strscpy()。对于 NUL 终止 + 零填充:用 strscpy_pad()。对于固定宽度字段(不需要 NUL 终止):用 strtomem_pad() 或 memcpy_and_pad()。对于已知长度的内存拷贝:用 memcpy()。五选一,选错等于没修。

第三,kernel 有稳定分支。你不能在主线上一天改完然后推到 LTS 分支去——每个分支的代码不完全相同,strncpy 的使用分布也不一样。一些分支的 strncpy 调用可能已经在主线中被移除了,但 LTS 分支因为历史版本不同,还有残留。

362 个提交横跨六年,意味着有一组开发者在做一件「移除了不会有人注意到」的工作。没什么 feature,没有新功能,没有性能飞跃。只是让未来的人不会踩同一个坑。

替代方案:五种工具,对应五种场景

这个合并请求(被合入 Linux 7.2)把 strncpy 声明从内核的 string.h 中删掉,同时移除了各 CPU 架构的 strncpy 实现。之后内核开发者用这套方案替代它:

场景 用这个 说明
NUL 终止的字符串拷贝 strscpy() 安全且检查返回值
NUL 终止 + 零填充 strscpy_pad() 在末尾填充 '\0'
固定宽度字段(无 NUL) strtomem_pad() 显式标记:这里不需要字符串
需要指定填充值的拷贝 memcpy_and_pad() 更精细的控制
已知长度内存拷贝 memcpy() 最基础,长度你已经在检查了

注意 strscpy 带返回值检查——拷贝成功返回正数(已拷贝的字节数),失败返回 -E2BIG。这比 strncpy 的「不知道有没有拷贝完」要友好得多。

strscpy 在内核里存在的时间已经不短了——从它的状态从「新 API」变成「成熟的替代方案」,再到成为唯一推荐选项,这段时间刚好覆盖了 Linux 从 4.x 到 7.x 的跨度。

个人验证:我跑的内核还有 strncpy 吗

我检查了一下自己这台服务器跑的内核——6.8.0-124-generic(Ubuntu 24.04 的发行版内核)。

$ uname -r
6.8.0-124-generic

6.8 主线发布于 2024 年 3 月,而 strncpy 移除是在 Linux 7.2(2026 年 6 月)。所以这台机器当前跑的内核里,strncpy 还存在。

但「存在」和「被使用」是两回事。我查了下 Ubuntu 6.8 内核源码的 string.h——strncpy 的声明还在,但实际调用点已经被逐步替代了(Ubuntu 在 2024-2025 年间反向移植了部分清理补丁)。6.8 已经不是用 strncpy 最严重的版本了。

有意思的是,即便在这台服务器上写这篇文章的时候,/usr/include/string.h 里的 strncpy 声明也还在——这是 glibc 层面的,不是内核层面的。glibc 的 strncpy 短期内不会移除,因为用户空间的 C 代码还在用。内核是内核,glibc 是 glibc。一件事从头到尾用了六年;而用户空间的迁移……才刚刚开始。

最后一个问题:strcpy 呢?

strncpy 走了,那更原生的 strcpy 呢?

答案是:还没走。strcpy 在 kernel 里的使用点比 strncpy 还多,而且在很多路径上,开发者已经通过调用点的长度检查保证了安全性——所以它不是「替换所有」的紧急任务,而是按需清理。但 strcpy 的语义(不限制长度)天然比 strncpy 更危险,只是它的危险更明显——所有人都知道 strcpy 需要手动确保缓冲区够大,而 strncpy 的危险藏在「我加了长度限制所以我是安全的」的错觉中。

strncpy 的移除意味着 kernel 向着「不容易用错」的方向又前进了一小步。它不会体现在任何跑分里,不会出现在任何发布公告的显眼位置。但 362 次提交、六年、无数人逐行阅读每一处调用——这是开源项目做「正确的事」最真实的注脚。

那 glibc 层面的 strncpy 呢?我问了一位搞嵌入式 C 的朋友,他说:「等用户空间代码全部重写?」然后发了个狗头。

好吧。六年移除了内核里的。剩下的,慢慢来。


这台服务器跑着 6.8 内核,和 strncpy 共存了两年。它俩没吵架。但从 Linux 7.2 开始,strncpy 终于可以退休了。

分享:

评论(0)

暂无评论,来写第一条吧~

发表评论