是否允許 shell 忽略腳本中的 NUL 位元組?

是否允許 shell 忽略腳本中的 NUL 位元組?

因為這就是他們中的一些人正在做的事情。

> echo echo Hallo, Baby! | iconv -f utf-8 -t utf-16le > /tmp/hallo
> chmod 755 /tmp/hallo
> dash /tmp/hallo
Hallo, Baby!
> bash /tmp/hallo
/tmp/hallo: /tmp/hallo: cannot execute binary file
> (echo '#'; echo echo Hallo, Baby! | iconv -f utf-8 -t utf-16le) > /tmp/hallo
> bash /tmp/hallo
Hallo, Baby!
> mksh /tmp/hallo
Hallo, Baby!
> cat -v /tmp/hallo
#
e^@c^@h^@o^@ ^@H^@a^@l^@l^@o^@,^@ ^@B^@a^@b^@y^@!^@
^@

這實際上是一些相容性問題嗎必需的按標準?因為它看起來相當危險且出乎意料。

答案1

按照POSIX,

輸入檔應為文字文件,但行長度不受限制^

輸入中的 NUL 字元²使其成為非文本,因此就 POSIX 而言,行為是未指定的,因此sh實作可以做任何他們想做的事情(並且符合 POSIX 標準腳本不得包含 NUL)。

有些 shell 會掃描前幾個位元組是否有 0,並假設您嘗試錯誤地執行非腳本檔案而拒絕執行腳本。

這很有用,因為exec*p()函數、env命令shfind -exec...必需的如果系統傳回 ENOEXEC on ,則呼叫 shell 來解釋命令execve(),因此,如果您嘗試為錯誤的體系結構執行命令,最好取得不會執行二進位文件來自 shell 的檔案錯誤比 shell 試圖將其理解為 shell 腳本的錯誤。

這是 POSIX 允許的:

如果可執行檔不是文字文件,shell 可能會繞過此命令執行。

在該標準的下一次修訂中將更改為

shell 可以應用啟發式檢查來確定要執行的檔案是否可以是腳本,如果確定該檔案不能是腳本,則可以繞過此命令執行。在這種情況下,它應寫入一條錯誤訊息,並應返回退出狀態 126
。文件的前綴。由於 sh 需要接受具有無限行長度的輸入文件,因此啟發式檢查不能基於行長度。

這種行為可能會妨礙 shell 自解壓縮存檔,儘管該檔案包含 shell 標頭,後跟二進位資料。

shellzsh在其輸入中支援 NUL,但請注意,NUL 不能在 的參數中傳遞execve(),因此只能在 的參數或名稱中使用它內建命令或函數:

$ printf '\0() echo zero; \0\necho \0\n' | zsh | hd
00000000  7a 65 72 6f 0a 00 0a                              |zero...|
00000007

(這裡定義並呼叫以 NUL 作為名稱的函數,並將 NUL 字元作為參數傳遞給內建echo指令)。

有些人會剝掉它們,這也是明智的做法。NULs 有時用作填充。例如,它們會被終端忽略(它們有時會被發送到終端,以便有時間處理複雜的控制序列(如回車符(字面意思))。文件中的空洞看起來像是被NUL 填充的,等等。

請注意,非文字不限於 NUL 位元組。它也是在區域設定中不形成有效字元的位元組序列。例如,0xc1 位元組值不能出現在 UTF-8 編碼文字中。因此,在使用 UTF-8 作為字元編碼的語言環境中,包含此類位元組的文件不是有效的文字文件,因此也不是有效的sh腳本³。

實際上,yash這是我所知道的唯一會抱怨此類無效輸入的 shell。


1 在該標準的下一個修訂版中,它將改變

輸入檔案可以是任何類型,但要根據 shell 語法(XREF 到 XSH 2.10.2 Shell 語法規則)進行解析的檔案的初始部分應由字元組成,並且不應包含 NUL 字元。 shell 不應強制執行任何行長度限制。

明確要求 shell 支援以不含 NUL 位元組的語法有效部分開頭的輸入,即使其餘部分包含 NUL,以考慮自解壓縮檔案。

² 和 字元意味著根據區域設定的字元編碼進行解碼(請參閱 的輸出locale charmap),並且在 POSIX 系統上,NUL 字元(其編碼始終為位元組 0)是其編碼包含位元組 0 的唯一字元。話說,UTF-16 不屬於可在POSIX 語言環境中使用的字元編碼。

³ 然而,在腳本內區域設定變更的問題(例如指派LANG// LC_CTYPE/變數時)以及何時變更對解釋輸入的 shell 生效。LC_ALLLOCPATH

答案2

這種行為的原因有點複雜.....

首先,現代 shell 包含對潛在二進位檔案(包含空位元組)的檢查,但此檢查僅驗證檔案的第一行。這就是第一行中的「#」改變行為的原因。歷史上的 Bourne Shell 沒有二進制檢查,甚至不需要“#”來按照您提到的方式運行。

然後,Bourne Shell 使用的特定方法透過mbtowc()簡單地跳過所有空字節來支援多字節字符,因為mbtowc()對於空字節返回字符長度 0,這會導致循環重試下一個字符。

Bourne Shell 在 1988 年左右引入了這種代碼,其他 shell 可能複製了這種行為。

相關內容