LWN:strlcpy( ) 的未來!
關(guān)注了就能看到更多這么棒的文章哦~
Ushering out strlcpy()
By Jonathan Corbet
August 25, 2022
DeepL assisted translation
https://lwn.net/Articles/905777/
在內(nèi)核中有那么多必須解決的復(fù)雜的問題,因此人們可能認(rèn)為復(fù)制一個(gè)字符串的問題不會(huì)引起什么注意。哪怕有 C 語言 string 帶來的那些危險(xiǎn),簡(jiǎn)單地移動(dòng)幾個(gè)字節(jié)也不應(yīng)該是那么困難。但是,多年來 string-copy 函數(shù)一直是一個(gè)經(jīng)常爭(zhēng)論的話題,只是有時(shí)反映在這一族操作中的某些變種上?,F(xiàn)在看來,源自 BSD 的 strlcpy()函數(shù)可能最終要退出內(nèi)核了。
起初,在 C 語言中復(fù)制字符串是很簡(jiǎn)單的。編者的《C 語言程序設(shè)計(jì)》第一版的第 101 頁提供了一個(gè) strcpy()的實(shí)現(xiàn):
strcpy(s, t)
char *s, *t;
{
while (*s++ = *t++)
;
}
這個(gè)函數(shù)有幾個(gè)缺點(diǎn),其中最明顯的缺點(diǎn)是,如果 s 字符串太長(zhǎng)了,它就會(huì)寫到目標(biāo) buffer t 之后的位置。在 C 語言中進(jìn)行開發(fā)的人們最終認(rèn)為這可能是一個(gè)問題,所以開發(fā)了其他的一些字符串復(fù)制函數(shù),首先是 strncpy():
char *strncpy(char *dest, char *src, size_t n);
這個(gè)函數(shù)最多可以從 src 復(fù)制 n 個(gè)字節(jié)到 dest,所以,只要 n 不超過 dest 的長(zhǎng)度,那么這個(gè)數(shù)組就不會(huì)被越界寫入。如果 src 短于 n,那么就會(huì)用 NUL 來完整填充 dest,所以它最終總是寫滿數(shù)組。如果 src 比 n 長(zhǎng),那么 dest 就不可能是以 NUL 結(jié)束的——如果調(diào)用者不仔細(xì)檢查返回值的話就會(huì)出問題。返回值是寫入 dest 的第一個(gè) NUL 字符的位置。如果 src 太長(zhǎng),那么,strncpy()返回 &dest[n],這實(shí)際上是超出實(shí)際數(shù)組 dest 的地址,不管是否進(jìn)行了截?cái)啵╰runcation)。因此,要想檢查出是否有截?cái)啵€是有些棘手,而且人們經(jīng)常不做這個(gè)檢查。[感謝 Rasmus Villemoes 指出了我們先前對(duì) strncpy()返回值的描述中的錯(cuò)誤。]
strlcpy() and strscpy()
BSD 針對(duì) strncpy()的問題采用的解決方案是引入了一個(gè)新的函數(shù),叫做 strlcpy():
size_t strlcpy(char *dest, const char *src, size_t n);
這個(gè)函數(shù)也將從 src 復(fù)制最多 n 個(gè)字節(jié)到 dest;與 strncpy()不同的是,它將始終確保 dest 是 NUL 結(jié)尾的。返回值永遠(yuǎn)是 src 的長(zhǎng)度,不管它在復(fù)制過程中是否被截?cái)噙^;開發(fā)者必須將返回的長(zhǎng)度與 n 進(jìn)行比較,來確定是否發(fā)生了截?cái)唷?br>
某種意義上來說,strlcpy()在內(nèi)核中首次使用是在 2.4 穩(wěn)定版。media 子系統(tǒng)有如下幾個(gè)實(shí)現(xiàn):
#define strlcpy(dest,src,len) strncpy(dest,src,(len)-1)
可見,當(dāng)時(shí)對(duì)返回值并沒有進(jìn)行什么檢查。這個(gè)宏很快就消失了,但真正的 strlcpy() 實(shí)現(xiàn)在 2003 年 5 月的 2.5.70 版本中出現(xiàn)了。該版本也將內(nèi)核中的許多調(diào)用位置的代碼都轉(zhuǎn)換為使用這個(gè)新函數(shù)了。在相當(dāng)長(zhǎng)的一段時(shí)間內(nèi),似乎一切都能正常工作。
但在 2014 年,人們開始聽到對(duì) strlcpy()的批評(píng),這導(dǎo)致了一個(gè)長(zhǎng)時(shí)間討論,關(guān)于是否在 GNU C 庫(kù)中增加相關(guān)實(shí)現(xiàn);直到今天,glibc 還是沒有 strlcpy()。內(nèi)核開發(fā)者也開始對(duì)這個(gè) API 感到不太滿意了。2015 年,Chris Metcalf 在內(nèi)核中加入了另一個(gè)字符串拷貝函數(shù):
ssize_t strscpy(char *dest, const char *src, size_t count);
這個(gè)函數(shù)跟其他類似函數(shù)一樣,都是將 src 復(fù)制到 dest,確保不會(huì)超過后者的邊界。和 strlcpy()一樣,它也確保結(jié)果是 NUL 結(jié)尾的。區(qū)別在于返回值上;如果字符串符合要求的話,返回的就是復(fù)制的字符數(shù)(不包括尾部的 NUL 字節(jié)),否則是 -E2BIG。
Reasons to like strscpy()
為什么 strscpy()更好?人們所宣傳的一個(gè)優(yōu)勢(shì)就是返回值,這使得檢查 src 字符串是否被截?cái)嘧兊煤苋菀?。不過還有一些其他的優(yōu)點(diǎn);要了解這些優(yōu)點(diǎn),可以先看一下內(nèi)核對(duì) strlcpy()的實(shí)現(xiàn):
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;
}
這里有一個(gè)明顯的缺點(diǎn),這個(gè)函數(shù)將讀取整個(gè) src 字符串,而不管這些數(shù)據(jù)是否會(huì)被用來復(fù)制。考慮到 strlcpy() 的定義語義,這里的低效做法無法根本解決;沒有其他方法可以返回 src 字符串的長(zhǎng)度。不過,這不僅僅是一個(gè)效率問題;正如 Linus Torvalds 最近指出的那樣,如果 src 字符串不可信,就會(huì)發(fā)生不好的事情,而這個(gè)函數(shù)本來就希望用在這種情況下。如果 src 不是以 NUL 為結(jié)尾的,那么 strlcpy() 就會(huì)繼續(xù)愉快地往下執(zhí)行,直到它找到一個(gè) NUL 字節(jié),這可能會(huì)遠(yuǎn)遠(yuǎn)超出了 src 數(shù)組的范圍,如果它沒有先出現(xiàn) crash 的話。
最后,strlcpy() 會(huì)導(dǎo)致出現(xiàn)一個(gè) race condition。src 的長(zhǎng)度先被計(jì)算出來,然后用于進(jìn)行復(fù)制操作,最終返回給調(diào)用者。但是如果 src 在這個(gè)過程中發(fā)生了改變,就會(huì)出現(xiàn)一些奇怪的事情;最好的情況是,返回值與目的地字符串中的實(shí)際內(nèi)容不完全相同。這個(gè)問題是實(shí)現(xiàn)上細(xì)節(jié)問題,而不是定義方面的問題,因此可以被 fix,但似乎沒有人認(rèn)為值得去 fix。
strscpy() 的實(shí)現(xiàn)避免了所有這些問題,也更有效率。當(dāng)然,它也因此而更加復(fù)雜。
The end of strlcpy() in the kernel?
當(dāng) strlcpy() 第一次被引入時(shí),其目的是取代內(nèi)核中所有的 strncpy() 調(diào)用,完全替代并刪除后者。但在 6.0-rc2 內(nèi)核中,仍然有近 900 個(gè) strncpy()的調(diào)用位置;這個(gè)數(shù)字在 6.0 合并窗口中還增加了兩個(gè)。在引入 strscpy()時(shí),Torvalds 明確表示不希望看到大規(guī)模地把 strlcpy() 調(diào)用批量切換過去的做法。在 6.0-rc2 中,只有 1400 多個(gè) strlcpy()調(diào)用和近 1800 個(gè) strscpy()調(diào)用。
將近七年之后,他的態(tài)度似乎發(fā)生了一些變化;Torvalds 現(xiàn)在說,"strlcpy()確實(shí)需要被淘汰了"。一些子系統(tǒng)已經(jīng)進(jìn)行了轉(zhuǎn)換,自 5.19 以來,strlcpy()的調(diào)用位置數(shù)量已經(jīng)減少了 85 個(gè)。是否有可能完全刪除 strlcpy(),目前還不清楚。盡管 strncpy()的危害是眾所周知的,而且近 20 年前就已經(jīng)決定將其刪除,但它仍然存在著。一旦有什么功能進(jìn)入了內(nèi)核,再把它移除,可能就是一個(gè)很困難的過程了。
不過,這次這個(gè)可能還有希望。正如 Torvalds 在回應(yīng) Wolfram Sang 的一組轉(zhuǎn)換時(shí)觀察到的那樣,大多數(shù)調(diào)用 strcpy() 的地方從未使用過返回值;這些地方都可以被轉(zhuǎn)換為 strscpy()而不會(huì)改變其行為。他建議,所需要的只是有人創(chuàng)建一個(gè) Coccinelle 腳本來完成這項(xiàng)工作。Sang 接受了這個(gè)挑戰(zhàn),并創(chuàng)建了一個(gè)完成轉(zhuǎn)換的 branch。顯然,這個(gè)工作不會(huì)被考慮加到 6.0 中,但可能會(huì)出現(xiàn)在 6.1 的 pull request 中。
這將在內(nèi)核中留下相對(duì)較少的 strlcpy() 使用代碼。這些代碼可以被一個(gè)一個(gè)地清理掉,而且有可能完全擺脫 strlcpy()。這將結(jié)束 20 年來在內(nèi)核中進(jìn)行有邊界的字符串拷貝(bounded string copy)的最佳方式的時(shí)不時(shí)地討論,盡管還有一些剩余的 strncpy()調(diào)用。至少在今后哪位聰明的開發(fā)者想出一個(gè)更好的函數(shù)并重新再來一次之前。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長(zhǎng)按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~
