Linux 7.2 合并窗口期,一条看似平淡的提交引起了 Phoronix 的注意:内核中所有 strncpy() 调用已被移除。这项工作的标题是"Linux Finally Eliminates The strncpy API",耗时六年,涉及 360 多个补丁,触及内核的数百个位置。
对于非 C 程序员来说,这听起来像一次无害的重构。但对于了解 C 语言字符串处理历史的人来说,这是一场审判的终局判决。
strncpy 的问题源于设计,不是一个意外 bug
strncpy(dst, src, n) 的行为描述只有一句话:从 src 复制最多 n 个字符到 dst。如果 src 的长度小于 n,剩下的空间用 \0 填充。如果 src 的长度大于或等于 n,dst不会被 null 终止。
第三条是最致命的。它意味着任何在 strncpy 之后直接使用 dst 的代码——比如 strlen(dst)、printf("%s", dst)——都在赌 src 的长度严格小于 n。这个赌注在内核中尤其危险:内核中的缓冲区大小通常由配置、硬件描述符、或用户输入决定,长度约束在编译期不可见。
更隐蔽的问题是"填充直到 n"行为。如果缓冲区是 4KB,而 src 只有 16 个字节,strncpy 会把剩余的 4080 个字节全部写成 \0。这在热路径上是纯浪费,在内核中可能意味着一次不必要的 TLB flush。
这两个问题都源于设计——它们是 API 行为定义的一部分。strncpy 最初的设计场景是复制到固定宽度的、以空格填充的文本字段(Unix 早期文件系统的目录条目)。当 C 标准委员会在 1989 年把它纳入 ANSI C 时,这个 API 已经被大量代码复用到了它从未被设计过的场景中。
从 strlcpy 到 strscpy:两次纠正,二十年
第一次纠正发生在 1998 年。OpenBSD 的 Theo de Raadt 和 Todd C. Miller 设计了 strlcpy(dst, src, size),它保证 null 终止、不填充、返回 src 的总长度(用于检测截断)。Linux 内核在 2.6 时期广泛引入了 strlcpy。
但 strlcpy 有一个微妙的问题:它读取 src 的全部长度来计算返回值,即使只复制前几个字节。如果 src 指向一个不受信任的、非 null 终止的字符串,这本身就是一次越界读取。
第二次纠正是 Linux 自己的解决方案:strscpy(dst, src, count)。它在 2015 年被合入内核,限制读取长度为 count、保证 null 终止、返回复制的字节数或 -E2BIG(截断错误)。这是 C 语言在现有类型系统内能做到的最安全的字符串复制。
从 strscpy 被引入到 strncpy 被完全消灭,中间经历了六年。瓶颈不在于技术——strncpy 到 strscpy 的转换在大多数情况下是机械的——而在于审查。每一处替换都需要确认:原来的 strncpy 是否依赖了"填充到 n"的副作用?下游代码是否隐含地假设了 dst 的末尾填充?是否有代码读取到了填充区域并期望它全是 \0?
360 多个补丁,每一个都在回答这些问题。最慢的不是写代码,是证明代码没有悄悄依赖那个要被删除的 API 的未定义行为。
Rust 的镜像:这些 bug 在编译期就死了
这场迁移最尖锐的旁注来自 Rust。在 Rust 中,字符串切片 &str 永远是有效的 UTF-8、永远以长度标记而非 null 结束符为边界。不存在"复制 n 个字节后忘记 null 终止"的余地——类型系统不允许。也不存在"忘记检查截断"的余地——copy_from_slice 的源和目标长度不匹配时是编译错误。
Rust 同样有安全问题——unsafe 块中的裸指针操作和 C 一样危险。但区别在于:在 C 中,整个内核都是 unsafe 块。在 Rust for Linux 中,安全边界被收缩到了明确定义的 FFI 接口和少数核心抽象中。
Linux 内核社区对 Rust 的接纳速度很慢——截至 2026 年中,Rust for Linux 仍然只覆盖了少数驱动子系统。但 strncpy 的消灭提供了一个具体的参照点:一个 API 在引入 37 年后才被彻底移除,期间产生了无数安全漏洞。如果 Rust 中一个等价的错误模式被编译器拒绝,那么用 Rust 写的内核代码就不会有这种延迟。
对于中国操作系统生态(鸿蒙、欧拉、龙蜥),这个参照点的含义更直接:这些系统基于 Linux 内核构建,继承了它的 C 代码库。当上游花六年时间消灭一个不安全的 API 时,下游如果不跟进同步——或者更根本地,不在新模块中转向 Rust 等内存安全语言——就是把已知的安全债推迟到下个十年。