LWN:strlcpy( ) 的未来!

共 3602字,需浏览 8分钟

 ·

2022-09-17 16:11

关注了就能看到更多这么棒的文章哦~

Ushering out strlcpy()

By Jonathan Corbet
August 25, 2022
DeepL assisted translation
https://lwn.net/Articles/905777/

在内核中有那么多必须解决的复杂的问题,因此人们可能认为复制一个字符串的问题不会引起什么注意。哪怕有 C 语言 string 带来的那些危险,简单地移动几个字节也不应该是那么困难。但是,多年来 string-copy 函数一直是一个经常争论的话题,只是有时反映在这一族操作中的某些变种上。现在看来,源自 BSD 的 strlcpy()函数可能最终要退出内核了。

起初,在 C 语言中复制字符串是很简单的。编者的《C 语言程序设计》第一版的第 101 页提供了一个 strcpy()的实现:

strcpy(s, t)
char *s, *t;
{
while (*s++ = *t++)
;
}

这个函数有几个缺点,其中最明显的缺点是,如果 s 字符串太长了,它就会写到目标 buffer t 之后的位置。在 C 语言中进行开发的人们最终认为这可能是一个问题,所以开发了其他的一些字符串复制函数,首先是 strncpy():

char *strncpy(char *dest, char *src, size_t n);

这个函数最多可以从 src 复制 n 个字节到 dest,所以,只要 n 不超过 dest 的长度,那么这个数组就不会被越界写入。如果 src 短于 n,那么就会用 NUL 来完整填充 dest,所以它最终总是写满数组。如果 src 比 n 长,那么 dest 就不可能是以 NUL 结束的——如果调用者不仔细检查返回值的话就会出问题。返回值是写入 dest 的第一个 NUL 字符的位置。如果 src 太长,那么,strncpy()返回 &dest[n],这实际上是超出实际数组 dest 的地址,不管是否进行了截断(truncation)。因此,要想检查出是否有截断,还是有些棘手,而且人们经常不做这个检查。[感谢 Rasmus Villemoes 指出了我们先前对 strncpy()返回值的描述中的错误。]

strlcpy() and strscpy()

BSD 针对 strncpy()的问题采用的解决方案是引入了一个新的函数,叫做 strlcpy():

size_t strlcpy(char *dest, const char *src, size_t n);

这个函数也将从 src 复制最多 n 个字节到 dest;与 strncpy()不同的是,它将始终确保 dest 是 NUL 结尾的。返回值永远是 src 的长度,不管它在复制过程中是否被截断过;开发者必须将返回的长度与 n 进行比较,来确定是否发生了截断。

某种意义上来说,strlcpy()在内核中首次使用是在 2.4 稳定版。media 子系统有如下几个实现:

#define strlcpy(dest,src,len) strncpy(dest,src,(len)-1)

可见,当时对返回值并没有进行什么检查。这个宏很快就消失了,但真正的 strlcpy() 实现在 2003 年 5 月的 2.5.70 版本中出现了。该版本也将内核中的许多调用位置的代码都转换为使用这个新函数了。在相当长的一段时间内,似乎一切都能正常工作。

但在 2014 年,人们开始听到对 strlcpy()的批评,这导致了一个长时间讨论,关于是否在 GNU C 库中增加相关实现;直到今天,glibc 还是没有 strlcpy()。内核开发者也开始对这个 API 感到不太满意了。2015 年,Chris Metcalf 在内核中加入了另一个字符串拷贝函数:

ssize_t strscpy(char *dest, const char *src, size_t count);

这个函数跟其他类似函数一样,都是将 src 复制到 dest,确保不会超过后者的边界。和 strlcpy()一样,它也确保结果是 NUL 结尾的。区别在于返回值上;如果字符串符合要求的话,返回的就是复制的字符数(不包括尾部的 NUL 字节),否则是 -E2BIG。

Reasons to like strscpy()

为什么 strscpy()更好?人们所宣传的一个优势就是返回值,这使得检查 src 字符串是否被截断变得很容易。不过还有一些其他的优点;要了解这些优点,可以先看一下内核对 strlcpy()的实现:

size_t strlcpy(char *dest, const char *src, size_t size)
{
size_t ret = strlen(src);

if (size) {
size_t len = (ret >= size) ? size - 1 : ret;
memcpy(dest, src, len);
dest[len] = '\0';
}
return ret;
}

这里有一个明显的缺点,这个函数将读取整个 src 字符串,而不管这些数据是否会被用来复制。考虑到 strlcpy() 的定义语义,这里的低效做法无法根本解决;没有其他方法可以返回 src 字符串的长度。不过,这不仅仅是一个效率问题;正如 Linus Torvalds 最近指出的那样,如果 src 字符串不可信,就会发生不好的事情,而这个函数本来就希望用在这种情况下。如果 src 不是以 NUL 为结尾的,那么 strlcpy() 就会继续愉快地往下执行,直到它找到一个 NUL 字节,这可能会远远超出了 src 数组的范围,如果它没有先出现 crash 的话。

最后,strlcpy() 会导致出现一个 race condition。src 的长度先被计算出来,然后用于进行复制操作,最终返回给调用者。但是如果 src 在这个过程中发生了改变,就会出现一些奇怪的事情;最好的情况是,返回值与目的地字符串中的实际内容不完全相同。这个问题是实现上细节问题,而不是定义方面的问题,因此可以被 fix,但似乎没有人认为值得去 fix。

strscpy() 的实现避免了所有这些问题,也更有效率。当然,它也因此而更加复杂。

The end of strlcpy() in the kernel?

当 strlcpy() 第一次被引入时,其目的是取代内核中所有的 strncpy() 调用,完全替代并删除后者。但在 6.0-rc2 内核中,仍然有近 900 个 strncpy()的调用位置;这个数字在 6.0 合并窗口中还增加了两个。在引入 strscpy()时,Torvalds 明确表示不希望看到大规模地把 strlcpy() 调用批量切换过去的做法。在 6.0-rc2 中,只有 1400 多个 strlcpy()调用和近 1800 个 strscpy()调用。

将近七年之后,他的态度似乎发生了一些变化;Torvalds 现在说,"strlcpy()确实需要被淘汰了"。一些子系统已经进行了转换,自 5.19 以来,strlcpy()的调用位置数量已经减少了 85 个。是否有可能完全删除 strlcpy(),目前还不清楚。尽管 strncpy()的危害是众所周知的,而且近 20 年前就已经决定将其删除,但它仍然存在着。一旦有什么功能进入了内核,再把它移除,可能就是一个很困难的过程了。

不过,这次这个可能还有希望。正如 Torvalds 在回应 Wolfram Sang 的一组转换时观察到的那样,大多数调用 strcpy() 的地方从未使用过返回值;这些地方都可以被转换为 strscpy()而不会改变其行为。他建议,所需要的只是有人创建一个 Coccinelle 脚本来完成这项工作。Sang 接受了这个挑战,并创建了一个完成转换的 branch。显然,这个工作不会被考虑加到 6.0 中,但可能会出现在 6.1 的 pull request 中。

这将在内核中留下相对较少的 strlcpy() 使用代码。这些代码可以被一个一个地清理掉,而且有可能完全摆脱 strlcpy()。这将结束 20 年来在内核中进行有边界的字符串拷贝(bounded string copy)的最佳方式的时不时地讨论,尽管还有一些剩余的 strncpy()调用。至少在今后哪位聪明的开发者想出一个更好的函数并重新再来一次之前。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~



浏览 76
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报