LWN:內(nèi)核該如何處理argc為0的情況?
關(guān)注了就能看到更多這么棒的文章哦~
Handling argc==0 in the kernel
By Jonathan Corbet
January 28, 2022
DeepL assisted translation
https://lwn.net/Articles/882799/
現(xiàn)在大多數(shù)讀者可能已經(jīng)了解了被稱為 CVE-2021-4034 的 Polkit 漏洞。Polkit 的 fix 方法相對(duì)簡(jiǎn)單,并且正在網(wǎng)絡(luò)上廣泛推行。不過(guò),這個(gè)問(wèn)題的根源來(lái)自對(duì)于程序在 Unix 類似系統(tǒng)上運(yùn)行方式有誤解。這個(gè)問(wèn)題極有可能也存在于其他程序中,所以最好能找到一個(gè)更普遍的解決方案。要解決這個(gè)問(wèn)題,最好是能在內(nèi)核中完成,但看起來(lái)很難在不引起 regression 的情況下來(lái)繞過(guò)這種誤解。
I'd like to have an argument, please
大多數(shù)開發(fā)者都熟悉 C 語(yǔ)言表達(dá)中的 main 函數(shù)的原型(prototype):
int main(int argc, char *argv[], char *envp[]);
在程序被調(diào)用時(shí),命令行參數(shù)會(huì)位于 argv 中,環(huán)境變量在 envp 中。兩者都是指向 char * 字符串類型的數(shù)組的指針,數(shù)組以 NULL 結(jié)尾。argv 中不為空的項(xiàng)目的數(shù)量放在 argc 中。不過(guò)這個(gè) API 是用戶空間看到的情況。當(dāng)內(nèi)核開始執(zhí)行一個(gè)程序時(shí)情況并不是這樣。在 Linux 上,這個(gè)程序本身也作為指針傳遞給 argv 數(shù)組。envp 數(shù)組會(huì)緊跟著 argv 里面的 NULL 值來(lái)開始。因此,C 程序中在進(jìn)入 main()時(shí)下面的語(yǔ)句結(jié)果會(huì)是 true:
envp == argv + argc + 1
按慣例,argv[0] 就是正在執(zhí)行的程序的名稱,許多程序都依賴這個(gè)慣例。然而,這個(gè)約定只是一個(gè)約定而已,并不是一個(gè)保證。argv 的實(shí)際內(nèi)容完全由調(diào)用 execve() 來(lái)運(yùn)行程序的人控制,而且調(diào)用者并不是必須要把程序名放在 argv[0] 位置的。
事實(shí)上,調(diào)用者根本不需要提供 argv[0]。如果傳遞給 execve()的 argv 數(shù)組是空的(或者 argv 指針是 NULL),那么新創(chuàng)建出來(lái)的進(jìn)程代碼中 argv 數(shù)組中的第一個(gè)指針將是 NULL,而 envp 數(shù)組會(huì)緊隨其后。不幸的是,Polkit(或者更精確地說(shuō),是 setuid pkexec 工具)"認(rèn)為" argv[0] 總是存在的,所以它通過(guò)從 argv[1]開始遍歷 argv 數(shù)組來(lái)處理其他參數(shù)。如果完全沒有提供參數(shù)的話,argv[1] 就是 envp 了,所以 pkexec 實(shí)際上是在遍歷所有的環(huán)境變量。還會(huì)對(duì)這里的參數(shù)進(jìn)行修改(pkexec 會(huì)對(duì)它的 argv 數(shù)組重新寫入內(nèi)容),pkexec 就可以被用來(lái)去改寫其環(huán)境變量,從而繞過(guò)了針對(duì) setuid 程序進(jìn)行的對(duì)這些變量的檢查工作。然后一切都完了。
這個(gè)問(wèn)題并不新鮮,人們也早已意識(shí)到這類問(wèn)題。Ryan Mallon 在 2013 年寫了一篇關(guān)于這個(gè)問(wèn)題的文章,指出 "它確實(shí)可以讓 setuid 二進(jìn)制文件產(chǎn)生一些特別行為"。他曾經(jīng)還提交了一個(gè) Polkit patch 來(lái)解決這個(gè)問(wèn)題,但這個(gè) patch 從未被合入。甚至來(lái)到更早的 2007 年的時(shí)候,Michael Kerrisk 就報(bào)了一個(gè) bug,認(rèn)為內(nèi)核的行為是個(gè)錯(cuò)誤,但該報(bào)告幾乎沒有經(jīng)過(guò)什么討論就被關(guān)閉了。因此這個(gè)問(wèn)題一直存在到現(xiàn)在,最終導(dǎo)致管理員們現(xiàn)在忙著修補(bǔ)這個(gè)漏洞。
Toward a more general fix
修復(fù)這個(gè)問(wèn)題本身很是簡(jiǎn)單,只要讓 pkexec 檢查 argc 來(lái)確保至少有一個(gè)參數(shù)就好。但肯定還有其他程序也會(huì)有著類似的假設(shè)。鑒于 argv[0] 含義已經(jīng)是大家的共識(shí)了,那么允許程序運(yùn)行時(shí)的 argv 數(shù)組為空,這種做法是否有意義。也許根本不合理,但當(dāng)前的 API 有長(zhǎng)久的歷史,無(wú)法不經(jīng)深思熟慮就去修改。
Ariadne Conill 用一個(gè) patch 來(lái)開啟了 linux-kernel 中的討論,該 patch 直接禁止 argv 中完全沒有內(nèi)容的情況下調(diào)用 execve()。違規(guī)的調(diào)用者會(huì)得到 EFAULT 錯(cuò)誤返回值。這就會(huì)確保 argv 不為空,從而解決問(wèn)題,但這種做法又有著自己的問(wèn)題。其中之一就是 Kees Cook 所發(fā)現(xiàn)的,實(shí)際上有相當(dāng)多的代碼在調(diào)用 execve()時(shí)的 argv 數(shù)組都是為空。Conill 認(rèn)為這些屬于 "偷懶寫出來(lái)的 test case 代碼,應(yīng)該被 fix",但是哪怕這些代碼無(wú)法正常運(yùn)行了也算是 regression。另外,正如 Heikki Kallasjoki 和 Rich Felker 都指出的,POSIX 標(biāo)準(zhǔn)本身實(shí)際上允許 argv 數(shù)組為空。
Felker 還提出了一個(gè)會(huì)引入更少的 regression 的替代方案:只在特權(quán)切換的位置確保 argv 非空。換句話說(shuō),也就是當(dāng) execve() 調(diào)用是要運(yùn)行一個(gè) setuid 程序的時(shí)候。Cook 認(rèn)為,只要有可能他都希望盡量避免把特權(quán)切換考慮進(jìn)來(lái)。他提出了另一個(gè)解決方案:在全空的 argv 數(shù)組的末尾額外插入一個(gè)空指針,這樣那些試圖直接跳過(guò) argv[0]的代碼就會(huì)看到這里的空指針。不過(guò)事實(shí)證明這個(gè)解決方案也是行不通的:ABI 要求 envp 需要緊接著 argv 開始,而額外加入的這個(gè) NULL 破壞了這一承諾。顯然是有一些程序就依賴于這種排列方式的,如果有變化的話它們肯定無(wú)法正常執(zhí)行。
還有一種方法,首先是由 Eric Biederman 提出的,就是用包含一個(gè)指向空字符串的單一指針的 argv 代替空的 argv。這個(gè)建議得到了一些支持(盡管到現(xiàn)在為止還沒有人進(jìn)行具體實(shí)現(xiàn)),但也引起了一些相關(guān)的擔(dān)憂??赡軙?huì)有一些程序會(huì)對(duì)使用 null 字符串的參數(shù)報(bào)錯(cuò),或者可能會(huì)在 argc 為零時(shí)做一些特別的動(dòng)作。改變 execve() 運(yùn)行的程序所傳遞的參數(shù)數(shù)量看起來(lái)可能還是會(huì)造成一些意外的。
Cook 最終這樣進(jìn)行了總結(jié):
鑒于我們已經(jīng)發(fā)現(xiàn)一些依賴于 NULL argv 的代碼,我認(rèn)為我們很可能無(wú)法直接進(jìn)行修改了,所以我們陷入了這個(gè)奇怪的兩難境地,一方面試圖拒絕我們可以拒絕的東西,另一方面又要為目前存在的問(wèn)題想辦法給出解決。
盡管如此,他還是繼續(xù)聲明他更加偏好最初的改動(dòng)(就是直接禁止沒有參數(shù)的 execve()調(diào)用,不過(guò)換成 EINVAL 返回值而不用 EFAULT),并建議對(duì)一個(gè) Valgrind test 進(jìn)行 fix,這個(gè) test 是已知會(huì)因?yàn)樵撓拗贫黄茐牡?。Conill 提供了新的一版 patch,這次它在對(duì)調(diào)用 execve() 時(shí)因?yàn)?argv 為空而返回 EINVAL 之前會(huì)給出 warning。Cook 接受了這個(gè) patch,說(shuō) "咱們來(lái)試試看會(huì)有什么問(wèn)題";Biederman 表示贊同,說(shuō):"尤其是既然你已經(jīng)表態(tài)愿意 fix 那些 tests 了"。
這就是截至目前的討論情況,但還不確定這個(gè)問(wèn)題最終會(huì)如何解決。最重要的是,這個(gè) patch 還沒有與 Linus Torvalds 商討過(guò),Linus 可能會(huì)因其會(huì)引入的潛在的 regression 風(fēng)險(xiǎn)而表示反對(duì)。畢竟這是一個(gè) ABI 的變動(dòng),很可能會(huì)有一些代碼被這個(gè)變動(dòng)所破壞。事實(shí)上 BSD 系統(tǒng)已經(jīng)禁止 argv 為空了,幸運(yùn)的話,它已經(jīng)幫助解決了大部分的擔(dān)憂。如果不幸真的出現(xiàn)了 regression,那幾乎肯定還需要再找另一個(gè)不同的解決方案了。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長(zhǎng)按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~
