是否允许 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 则调用 shell 来解释命令execve(),因此,如果您尝试为错误的体系结构执行命令,最好获取不会执行二进制文件来自 shell 的文件错误比 shell 试图将其理解为 shell 脚本的错误。

这是 POSIX 允许的:

如果可执行文件不是文本文件,shell 可能会绕过此命令执行。

在该标准的下一次修订中将更改为

shell 可以应用启发式检查来确定要执行的文件是否可以是脚本,并且如果确定该文件不能是脚本,则可以绕过此命令执行。在这种情况下,它应写入一条错误消息,并应返回退出状态 126。
注意:拒绝不能是脚本的文件的常见启发式方法是在固定长度内的 <newline> 字节之前定位 NUL 字节文件的前缀。由于 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 可能复制了这种行为。

相关内容