What is shebang (#!/bin/sh) in POSIX shell script

この記事はPOSIX原理主義Advent Calendarの13日目だ。

シェルスクリプトに限らず,Perl,Python,Rubyといったスクリプト系言語において,プログラム(スクリプト)の1行目に以下のような記述をする。

#!/bin/sh
#!/usr/bin/env python

この#!の文字列をshebang(シバン)と呼ぶらしい。名前の由来はshell bang(!)などいくつかの説があるとのことだ。Shebangに続けてそのスクリプトを実行するインタープリターを絶対パスで指定することができる。このように記述すると,スクリプトファイルの実行時にshebangで指定したインタープリターによりスクリプトが実行される。

ほぼ全てのシェルスクリプトの1行目にこのような記述があり,もはや暗黙のルールと思えるこの記述だが,恥ずかしながら意味をよく理解していなかった。そこで,POSIXにおけるこのshebangの解釈について調査したのでまとめる。

コマンドの実行の流れ

まず,shebangについて説明する前に,コマンドがどのように実行されるかについてみておく。

POSIXにおけるコマンドの実行の流れは,XCUのCommand Search and Executionに書かれている。自作のシェルスクリプトやコマンドは,1.e.i.bか2のどちらかに分類される。この2種類はPATH変数から探すか,相対パスで実行するかの違いであり,中身はほぼ同じだ。以下に1.e.i.bの第1段落を抜粋する。

b. Otherwise, the shell executes the utility in a separate utility environment (see Shell Execution Environment) with actions equivalent to calling the execl() function as defined in the System Interfaces volume of POSIX.1-2008 with the path argument set to the pathname resulting from the search, arg0 set to the command name, and the remaining execl() arguments set to the command arguments (if any) and the null terminator.
1.e.i.b – Command Search and Execution – XCU

ここで書かれている通り,シェルスクリプトやコマンドはexecl()に相当する関数が呼び出されることで実行される。以下にexecl()の定義示す。

int execl(const char *path, const char *arg0, ... /*, (char *)0 */);

シェルスクリプトやコマンド実行時には,pathにシェルスクリプト・コマンドの絶対パス,arg0にシェルスクリプト・コマンドの名前,arg1以降に残りのコマンド引数が適用されて実行される。

execl()実行後の挙動

execl()のPOSIXのぺージによると,execl()が呼び出された後はpathに指定されたファイルによって処理が異なってくる。

execl()にコマンドとその引数が引き渡されて実行されるとき,正常な実行可能バイナリーファイルであればそのまま実行される。しかし,それ以外ではファイルの種類によって以下2通りの処理が行われる。

  1. コマンドが認識できない形式(ENOEXEC)か,認識できてもシステムが未対応(EINVAL)の場合
  2. ファイル先頭の2バイトが文字列#!である場合
コマンドが実行できない場合

1.の場合は,単純なテキストファイルに実行権限が付いた場合が該当する。この場合,execl()はインタープリターとしてshコマンドを使って,まるで以下の形式で実行した環境かのようにexeclp()execvp()を実行する。

execl(<shell path>, arg0, file, arg1, ..., (char *)0);

ここで,<shell path>はshの絶対パス,fileはプロセスのイメージファイル(ファイル名?)arg0,arg1は引数となる(arg0はfileと同じ?)。

There are two distinct ways in which the contents of the process image file may cause the execution to fail, distinguished by the setting of errno to either v[ENOEXEC] or [EINVAL] (see the ERRORS section). In the cases where the other members of the exec family of functions would fail and set errno to [ENOEXEC], the execlp() and execvp() functions shall execute a command interpreter and the environment of the executed command shall be as if the process invoked the sh utility using execl() as follows:

execl(<shell path>, arg0, file, arg1, …, (char *)0);

where <shell path> is an unspecified pathname for the sh utility, file is the process image file, and for execvp(), where arg0, arg1, and so on correspond to the values passed to execvp() in argv[0], argv[1], and so on.
execl() – XSH

つまり,実行可能なバイナリーファイル以外はすべてshによるシェルスクリプトとみなされる。

この仕様のため,シェルスクリプトの1行目にはshebang(#!/bin/sh)がなくても,shで起動される。試しに以下の内容のhi.shというスクリプトを作って実行すると,shebangがなくてもshによって実行されていることがわかる。

ps -p $$ -o comm=
ps -p $$ -p args=
sh
/bin/sh ./hi.sh
Shebang(#!)がある場合

2.の場合は,一般的なシェルスクリプトのパターンだ。これは,ファイルの先頭2バイト#!であれば,ファイル1行目の#!以降の残りの文字を実行するコマンドインタープリターとみなす。

Another way that some historical implementations handle shell scripts is by recognizing the first two bytes of the file as the character string “#!” and using the remainder of the first line of the file as the name of the command interpreter to execute.

execl() – XSH

つまり,以下のようなシェルスクリプトでは,#!以降で指定される/bin/shによりファイルが実行される。

#!/bin/sh
ps -p $$ -o comm= ps -p $$ -p args=

ファイルの先頭2バイトをチェックしているので,UTF-8やUTF-16などでBOM(Byte Order Mark)が付いている場合,shebangは認識されず1.のルートでスクリプトが実行される。その場合,BOMがコマンドとみなされて,当然ながらそんなコマンドはないので,line 1: command not foundというエラーが出る。

一応,以下のようにエラー出力を捨てるコードを1行目に書けば,BOMありでも実行できるシェルスクリプトは作れる。しかし,こんなことをするくらいなら最初からBOMなしで書いたほうがよいだろう。

: 2>-

Shebangによりシェルスクリプトを実行する際に,一つだけ問題がある。それは,以下で記載されている通り,sh側で#!から始まるファイルを読み込んだときの挙動が不定なことだ。

1. The shell reads its input from a file (see sh), from the -c option or from the system() and popen() functions defined in the System Interfaces volume of POSIX.1-2008. If the first line of a file of shell commands starts with the characters “#!”, the results are unspecified.
2.1 Shell Introduction – Shell Command Language

つまり,大昔のOSなどでは#!/usr/bin/perlから始まるファイルをshで起動した場合,/usr/bin/perlが起動することもありえる。

そのため,真の意味で可搬性が高いといえるのは,shebangを書かない1.の方法だ。ただ,shebangは広く普及しており,現在の実装では#!から始まるファイルをshが読み込んでも,その行は単にコメントとして無視される場合がほとんどだ。また,POSIXのshコマンドのページにおいても,shebang #!が書かれたシェルスクリプトを意識した記述もある。

このことから,わかりやすさと実状を考慮して,shebang行に#!/bin/shを書いても問題ないだろう。

Shebangに関する問題点

ここまででshebangがPOSIX上でどのように規定されているかを説明した。ここからは,shebangに関する問題点について説明する。

Shebangに指定するパス

まずは,shebang行に何を書けばよいかという議論だ。一般的に,シェルスクリプトでは以下のように/bin/shで指定することが多い。

#!/bin/sh

ここで,はたして本当に/bin/shと書いて問題ないのだろうか?実のところ,POSIXでは/binというディレクトリは定義されていない。

さらに,POSIX規格ではsh/bin/shまたは/usr/bin/shにあるとせずに,自分でgetconf PATHコマンドでPATHを取得するように書かれている。

Applications should note that the standard PATH to the shell cannot be assumed to be either /bin/sh or /usr/bin/sh, and should be determined by interrogation of the PATH returned by getconf PATH, ensuring that the returned pathname is an absolute pathname and not a shell built-in.

sh – XCU

ただ,このようになってしまうと結局shebangの指定に互換性がなくなり,環境ごとにshebangの書き換えが必須となってしまう。

ではどう考えるか。これはFilesystem Hierarchy Standard (FHS)への準拠を考える。FHSはLinuxを含むUNIX形OSでの主なディレクトリとその内容を定めたものである。したがって,FHSで定義されているコマンドのパスは前提としてよいと考える。

FHSで存在することが必須とされているコマンドは/bin配下のものが大半であり,残りの/usr/bin配下などに含むコマンドはほとんどオプションだ。/bin配下に存在することが要求されているコマンドは以下の35コマンドだ。

cat, chgrp, chmod, chown, cp, date, dd, df, dmesg, echo, false, hostname, kill, ln, login, ls, mkdir, mknod, more, mount, mv, ps, pwd, rm, rmdir, sed, sh, stty, su, sync, true, umount, uname, test, [

3.4. /bin : Essential user command binaries (for use by all users) Filesystem Hierarchy Standard

この中に,shコマンドが存在している。したがって,FHSに準拠しているOSでは/bin/shは存在することが保証されているので,shebangとして#!/bin/shと書いても問題ない

ここで注意したいのはFHSではbashcshenvコマンドの存在は必須ではなかったり,定義されていないことだ。したがって,#!/bin/bash#!/usr/bin/envから始まるスクリプトは可搬性が低い。実際のところ,/bin/bashは存在することが多いのだが保証はできない。

/usr/bin/envが定義されていないというのは重要だろう。というのも,シェルスクリプト以外のスクリプト言語(Perl,Python,Ruby)では,#!/usr/bin/env pythonのようにenvコマンドを経由して実行させるshebang行がよく書かれるからだ。

スクリプト言語のインタープリターはPOSIX規格でも定義されておらず,どこにインストールされているかは全く不明だ。一応,FHSでは/usr/binにperlとpythonが存在することがオプションで定義されているのだが,あくまでオプションだ。それに,これらのコマンドではシステムにインストールされたものではバージョンが古いため,ユーザーが独自にインストールしたものを優先して使いたい場合がある。

そこで,envコマンドを経由してユーザーのPATHを使うことで,インストール場所の問題を回避することがよくなされる。FHSでは/binはシステム管理者とユーザーの両方に使われるコマンドが含まれる。envコマンドはシステム管理者には不要なので,/usr/binenvコマンドが存在することが多い。

大半の場合/usr/bin/envは存在するが,存在しない実装もある。そこで,確実に実行させるために/bin/shを経由してスクリプト言語のインタープリターを実行する方法がある。

シェルスクリプトのコメントと,スクリプト言語のコメント文字が違うことを利用して,スクリプト言語のコメント内にシェルスクリプトでexecコマンドを記述して,スクリプト言語のインタープリターを起動するというトリックを使っている。

個人的にはそこまでする必要はないと考える。PerlもPythonもRubyもPOSIXに存在しないし,そもそもインストールされていなくてPATH変数に存在しないかもしれない。あまり気にしなくてもいいのではないか?

基本的には,#!/usr/bin/envで記述しておき,万が一envコマンドが無かったり,スクリプト言語のインタープリターが存在しなければ,自分でインストールしたりシンボリックリンクを貼ればよいだろう。

Shebangにおける引数

以下のようにshebang行にオプションや引数を書くことがある。

#!/bin/sh -eu
#!/usr/bin/env python

ここで問題になるのが,これらのオプションや引数がどう解釈されるかだ。

POSIX規格では,ファイルの先頭2バイトが#!である場合,ファイル1行目のそれ以降の文字は「インタープリター名として扱う」としか記載されておらず,オプションや引数がある場合にどうなるかは書かれていない

例えば,shebangに以下のように書いた場合を考える。

#!/bin/sh -e -u

この場合,以下の5パターンが考えられる。

Shebangの引数の解釈
IDPathArg1Arg2
1/bin/sh -e -u

2/bin/sh-e-u
3/bin/sh-e -u
4/bin/sh-e
5/bin/sh

期待するのは全てオプションとして認識されること(表の2番目)だ。しかし,Ububntu 16.04では3番目の挙動をした。つまり,引数-e -uが空白を含めた1個("-e -u")として認識されてしまった。

この挙動については,Linux Programmer’s Manualのexecveのマニュアルに書かれている。以下のコマンドでも閲覧できる。

man execve
Interpreter scripts
An interpreter script is a text file that has execute permission
enabled and whose first line is of the form:

#! interpreter [optional-arg]

The interpreter must be a valid pathname for an executable file. If
the filename argument of execve() specifies an interpreter script,
then interpreter will be invoked with the following arguments:

interpreter [optional-arg] filename arg…

where arg… is the series of words pointed to by the argv argument
of execve(), starting at argv[1].

For portable use, optional-arg should either be absent, or be
specified as a single word
(i.e., it should not contain white space);
see NOTES below.
execve(2) – Linux manual page

さらに,上記マニュアルのNOTESを確認すると以下の記載がある。

The semantics of the optional-arg argument of an interpreter script
vary across implementations. On Linux, the entire string following
the interpreter name is passed as a single argument
to the
interpreter, and this string can include white space. However,
behavior differs on some other systems. Some systems use the first
white space to terminate optional-arg. On some systems, an
interpreter script can have multiple arguments, and white spaces in
optional-arg are used to delimit the arguments.
execve(2) – Linux manual page

つまり,Linuxでは引数を全体で1個として扱う(表の3番目)が,他の実装では最初の空白を終端(表の4番目)とみなしたり,複数の引数を受け付ける(表の2番目)こともありえる。

元々,POSIX規格にはshebang行の引数については言及がないので,表の1番目の#!以降が空白を含めて丸ごとインタープリターとみなしてしまっても問題ないように解釈できる。このため,どの環境でも動作させるには,基本的にはshebangには引数を書かず,書いたとしてもshebang行における引数は1個までにすべきだろう。

ショートオプションであれば,グループ化できるので以下のようにまとめればよい。

#!/bin/sh -eu

スクリプト言語をenv経由で実行する場合は,envコマンドの引数としてスクリプト言語のインタープリターを指定してしまっているので,残念だがオプションの指定は諦めるしかない。

#!/usr/bin/env python     # OK
#!/usr/bin/env python -v # NG

本の虫: Shebangという謎な事実上業界標準について

Shebang直後の空白

シェルスクリプトなどを見ていると,以下のようにshebang#!の直後に空白が置かれているshebang行を見かけることがある。

#! /bin/sh

あまりみかけない記述なので,ただの打ち間違いか,Webブラウザの表示や書籍の組版の文字間隔の調整でそうみえているだけだと思っていたのだが,どうやらこのように書く流派が存在するらしい。

このことについては以下のページでの議論とそこから辿れるURLからことの経緯がよくわかる。

unix – #! /bin/sh で、 ! の後にスペースを空けることに理由はあるか – スタック・オーバーフロー

発端としては,GNU autoconfのチュートリアル文書において,「4.2BSDでは先頭4バイト”#! /”をマジックナンバーとして読むのでスペースが必要」と記載されていたためだ。しかし,この記述は誤りで,実際は空白はオプション扱いであり,新しい版のGNU autoconfの文書では修正されている。

さらに,問題の4.1 BSDにおいてもソースコード上でshebangの直後に空白が必要となる修正は入っておらず,空白がなければ動作しない実装が存在したという事実も確認できない。Shebangの直後に空白を入れなければならないというのは,あくまで噂や都市伝説でしかない。

これらの信頼性の欠ける情報に従うべきでないという理由とともに,POSIX規格の観点からも,#!の直後に空白をいれるべきでない

理由は,#!以降の文字を空白を含めてインタープリターのパスとして認識してもPOSIX定義上の動作としてなんらおかしくないからだ

まず,POSIXにおけるshebangによるインタープリターの起動メカニズムはexecl()のマニュアルの以下の1文のみで定義されている。

Another way that some historical implementations handle shell scripts is by recognizing the first two bytes of the file as the character string “#!” and using the remainder of the first line of the file as the name of the command interpreter to execute.

execl() – XSH

ここでは,ファイルの1行目の#!以降の文字をインタプリターのファイル名とみなすとだけ書かれており,POSIXではこれ以上に詳しいルールはなく,引数の扱いがどうなるかなども実装依存となる。

さらに,以下の事実からPOSIXの定義だけだと,Shebangの直後に空白をおくと,相対パス" /bin/sh"でインタープリターが指定されたと解釈することもできる。

  1. Shebangは相対パスも有効,実際にshを./testに配置してShebangを相対パス#!test/shで書いたシェルスクリプトに記述したコマンドps -p $$ -o args=で確認可能。
  2. 名前が空白1個のディレクトリも有効なパス(mkdir ' ')。

実際には,空白を含むパスを指定するとコマンドが見つからない。

bash: ./test.sh: /test/bin/sh: bad interpreter: No such file or directory

しかし,POSIXの定義上,#!以降の残りを空白や引数丸ごと単一コマンドとみなしても,挙動として問題ない。

このPOSIX規格の解釈上の曖昧さをなくすために,shebangの直後に空白をいれるべきでない。常に#!/bin/shのように#!の直後に空白を入れずにshebang行を書こう。

シェルスクリプトへのsetuid

最後の問題はセキュリティに関するものだ。ファイルのアクセス権限として,セットユーザーID(setuid)がある。この属性のついたファイルはそのファイルの所有者(通常はroot)の権限で実行できる。

setuidの利点はroot権限のパスワードを教えることなくroot権限でしか行えない作業を行える点だ。例えば,passwdコマンドで自分のログインパスワードを変更する場合や,mountコマンドでデバイスを接続する場合は,本来ならroot権限のファイルを操作する必要がある。しかし,これのコマンドにはsetuid属性があるので,root権限のパスワードをしらなくても通常ユーザーのまま実行できる。

setuidをファイルに設定するには以下のコマンドで行う。

chmod u+s file

処理内容が限定される場合には便利だが,一時的にroot権限を与えてしまうため,セキュリティ上の危険性がある。

実際にシェルスクリプトにsetuid属性がある場合の脆弱性を狙った攻撃が過去には行われたらしい。

このことは,以下で言及されている。

上記のUnix FAQで説明されている2個の事例を紹介する。

ファイル名を-iにする問題

これは,root権限で対話シェルが起動してしまうという問題だ。

setuidのついた/etc/setuid_scriptというスクリプトのshebangが以下であるとする。

#!/bin/sh

ここで以下の順序でコマンドを実行する。

cd /tmp
ln /etc/setuid_script -i
PATH=.
-i

この場合,最終的に以下のコマンドが実行されてしまう。

/bin/sh -i

/etc/setuid_scriptの所有者であるroot権限で対話シェルを起動することになってしまう。これは,ファイル名-ishのオプションと誤認されてしまうことによる問題だ。

これを防ぐのは簡単であり,以下のようにshebang行の最後にオプションの終わりを示す-をつければよい。

#!/bin/sh -

この問題については,POSIXのshのページでも以下のように言及がある。

On systems that support set-user-ID scripts, a historical trapdoor has been to link a script to the name -i. When it is called by a sequence such as:

sh –

or by:

#! usr/bin/sh –

the historical systems have assumed that no option letters follow. Thus, this volume of POSIX.1-2008 allows the single <hyphen-minus> to mark the end of the options, in addition to the use of the regular “–” argument, because it was considered that the older practice was so pervasive. An alternative approach is taken by the KornShell, where real and effective user/group IDs must match for an interactive shell; this behavior is specifically allowed by this volume of POSIX.1-2008.
Note:
There are other problems with set-user-ID scripts that the two approaches described here do not resolve.
sh

ただ,この問題は手元のUbuntu 16.04では再現できなかった。実際に上記の手順を行っても,ファイル名の部分が./-iとなっており,きちんとファイルとして認識されている。

さらに,上記の最後に書いてあるとおり,shebang行に#!/bin/sh -と書いても他に重要な問題があるのであまり意味がない。例えば,それは次の問題だ。

実行ファイルの差し替え

これは,root権限で任意のコマンドが実行されてしまうという問題だ。

以下の手順でコマンドを実行する。

cd /tmp
ln /etc/setuid_script temp
nice -20 temp &
mv my_script temp

ここで,3番目のコマンドは以下のように解釈される。

nice -20 /bin/sh - temp

nice -20によりコマンドの実行優先順位を遅くできるので,shtempを読み込む前に,tempファイルを別のファイルで上書きすることができる。つまり,任意のファイルを管理者権限で実行できてしまう

対策

こうしたsetuidに関する問題に対応するには以下の2通りのアプローチがある。

  1. setuidを付与する実行ファイルはC言語などのバイナリーにする。
  2. シェルスクリプトではsetuidを使わずに,管理者権限でないと実行できないようにする。

1.がより確実だろう。バイナリーにしてしまえば,間にshなどがファイルを読み込む隙がないので安全だ。

2.の方法について,管理者権限での実行が必要な場合は,setuidではなくそもそも管理者権限でないと実行できないようにしたほうがいいだろう。

具体的には,以下のコードで管理者権限として実行されているか判定できるので,冒頭に記述しておいてroot権限でなければ終了すればよい。

if [ $(id -u) != 0 ]; then
echo "Please run as root user!" >&2
exit 1
fi

My Future Sight for Past: How to check root user in POSIX Shell script

なお,Linuxを始めとした現代のOSではシェルスクリプトへのsetuidは無視するようになっているので,シェルスクリプトにsetuidを付与しても機能しない。

Linux ignores the set-user-ID and set-group-ID bits on scripts.
execve(2) – Linux manual page

セキュリティ上の問題になるのですべきではないが,参考までにシェルスクリプトに付与したsetuidを有効にして実行したい場合の方法を示す。これは,POSIX範囲外になるが-pオプションを付けることでできる。

#!/bin/sh -p

Conclusion

ここまででシェルスクリプトのshebangについて説明してきた内容を以下にまとめる。

  • シェルスクリプトのshebang行には,FHSで保証される#!/bin/shを書く。
  • 基本的にshebang行にオプションや引数を付けない。つけたとしても最大1個までにする。
  • シェルスクリプトにsetuid属性は付与させない。

POSIX規格に厳密に従うならば,shebangを書かないほうがいい。shebangを書かなければ,「#!から始まるファイルがshで結果が不定になる」ことについて悩まなくて済む。

このような曖昧さがPOSIXに残ったのは,POSIX自体が既存の実装ありきだったためだろう。既に存在する実装をカバーできるように,こうした曖昧さをあえて残したのだと思われる。

できることなら,次回のPOSIX規格の改定時にここの曖昧さがなくなってくれたら嬉しい。具体的には,現在2.1 Shell Introductionで「If the first line of a file of shell commands starts with the characters “#!”, the results are unspecified.」となっているところを,以下のツイートのように#!から始まる場合にどのように処理すべきか明記してほしい。

Kazuho Okuさんのツイート: “#! について、例えば「○○のように処理すべき、あるいはコメントとして無視しろ」という規定があれば #! /bin/sh は POSIX においてポータブルだと言えるんだろうと思うんだけど”

この記事は元々2016-03-23にまとめた以下のTogetterの記事が頭に残っていたので,これをもとにさらに調査したものだ。

POSIXにおけるShebangの解釈についてのシェルショッカーとの議論まとめ – Togetterまとめ

今回の記事はシェルスクリプトを書く上ではほとんど役に立たず,教養を身に付けるような内容となった。ただ,今後おそらく一生見ることになるので,シェルスクリプトの1行目のshebangとは何であるか,どう書くべきなのかをはっきりさせておきたかった。

これで,これからは胸を張ってシェルスクリプトを書くことができるだろう。

What is shebang (#!/bin/sh) in POSIX shell script” に対して1件のコメントがあります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です