Help us understand the problem. What is going on with this article?

Solarisの/bin/shは標準入力をクローズしてシェルスクリプトを起動してはいけない

More than 3 years have passed since last update.

ちょっとしたデーモンプロセスを perl で書いて、シェルスクリプトを起動させようとしたら、みごとにはまったのその顛末。

魔物が住む cat

問題を切り分けるためにシンプルにしたスクリプトなのだが、このような何の変哲もないスクリプトが、perl で書いたバックグランドプロセスから起動すると、 cat >/dev/null があると、その後の ifthen 節で unexpected end of file が出る。

#!/bin/sh
exec >>/dev/tty 2>&1
set -x

cat >/dev/null 2>/dev/tty

#############################

if [ -d /  ]; then
    echo $0
    date
fi

exit 0

どんなに見直してもスクリプトに問題はなさそう。

スクリプトを手で実行すると起こらない。
perl から fork() || exec(...) で起動すると起こっていて、スクリプトを実行するシェルが bourne shell では起こって、korn shell に変更すると起こらない。

状況をシンプルにしてみるために次のようなラッパープログラムを使ってスクリプトを起動すると、問題が再現する。

cexec.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int stat;
    if (fork()) {
        wait(&stat);
    } else {
        close(0);
        close(1);
        close(2);
        execl("/export/home/komeda/sh-bug.sh", "sh-bug.sh", 0);
    }
}

再現した状況

% ./cexec     
+ cat 
/export/home/komeda/sh-bug.sh: syntax error at line 11: `end of file' unexpected

じつは標準入力をクローズして起動するだけで問題が再現できた。

% ./sh-bug.sh <&-
+ cat 
./sh-bug.sh: syntax error at line 11: `end of file' unexpected

註: プロンプト記号は % であるが、筆者が使っているシェルは zsh である。

魔物の正体

環境は Solaris だったのだが、よくよく調べてみると bourne shell のバグだった。

perl スクリプトは、バックグラウンドプロセスとして起動するため、標準入力、標準出力、標準エラー出力を close している。
これが問題の引き金だった。

if 文のところでエラーが起きていたのは、シェルがスクリプトを 128 バイト単位で読み込むのが、たまたま読み込み境界が if 文のところにあったためである。

なにが起こっているのか

標準入力をクローズした状態でシェルスクリプトを起動すると、スクリプトファイルが、ファイルディスクリプタ 0 でオープンされる。

ここで子プロセスを起動すると、ファイルディスクリプタ 0 は標準入力であるため、そのまま子プロセスに引き継がれる。
子プロセスが標準入力を読む。
それはシェルがスクリプトファイルを読むために使っているファイルディスクリプタなので、この後実行するはずの部分を子プロセスが読んでしまうことになる。この結果ファイルオフセットがずれて、シェルは続きの実行に失敗する。

#!/bin/sh
exec >>/dev/tty 2>&1
set -x

cat >/dev/null 2>/dev/tty

#############################

if [ -d /  ]; then
    echo $0
    date
fi

exit 0

trussで見ると、cat がスクリプトの続き部分を読んでいることが見て取れる。

% truss -f ./cexec |& egrep 'open|dup|fork|read'
+ cat 
/export/home/komeda/sh-bug.sh: syntax error at line 11: `end of file' unexpected
2705:   open("/var/ld/ld.config", O_RDONLY)             = 3
2705:   open("/lib/libc.so.1", O_RDONLY)                = 3
2705:   fork1()                                         = 2706
2706:   fork1()         (returning as child ...)        = 2705
2706:   open("/var/ld/ld.config", O_RDONLY)             = 0
2706:   open("/lib/libc.so.1", O_RDONLY)                = 0
2706:   open64("/export/home/komeda/sh-bug.sh", O_RDONLY) = 0
2706:   read(0, " # ! / b i n / s h\n e x".., 128)      = 128
2706:   open64("/dev/tty", O_WRONLY)                    = 1
2706:   dup(1)                                          = 2
2706:   fork1()                                         = 2708
2708:   fork1()         (returning as child ...)        = 2706
2708:   open("/var/ld/ld.config", O_RDONLY)             = 3
2708:   open("/lib/libc.so.1", O_RDONLY)                = 3
2708:   read(0, " f i\n\n e x i t   0\n\n".., 8192)     = 117
2708:   read(0, 0x080621B8, 8192)                       = 0
2706:   read(0, 0x08076358, 128)                        = 0

スクリプト中で標準入力を exec でつなぎ替えた場合でも、ファイルディスクリプタ 0 は確かに切り替わるが、ファイルディスクリプタ 0 は、そもそもシェル自身ががスクリプトファイルを読むために使っているものである。このファイルディスクリプタが close されてしまい、続きが読めず実行に失敗する。

#!/bin/sh
exec </dev/null >>/dev/tty 2>&1
set -x

cat >/dev/null 2>/dev/tty

#############################

if [ -d /  ]; then
    echo $0
    date
fi

exit 0

実行結果

% ./sh-bug.sh <&-                               
+ cat 
./sh-bug.sh: syntax error at line 10: `end of file' unexpected

truss で見たようす

% truss -f ./cexec |& egrep 'open|dup|fork|read'
2716:   open("/var/ld/ld.config", O_RDONLY)             = 3
2716:   open("/lib/libc.so.1", O_RDONLY)                = 3
2716:   fork1()                                         = 2717
2717:   fork1()         (returning as child ...)        = 2716
2717:   open("/var/ld/ld.config", O_RDONLY)             = 0
2717:   open("/lib/libc.so.1", O_RDONLY)                = 0
2717:   open64("/export/home/komeda/sh-bug.sh", O_RDONLY) = 0
2717:   read(0, " # ! / b i n / s h\n e x".., 128)      = 128
2717:   open64("/dev/null", O_RDONLY)                   = 1
2717:   open64("/dev/tty", O_WRONLY)                    = 1
2717:   dup(1)                                          = 2
+ cat 
2717:   fork1()                                         = 2719
2719:   fork1()         (returning as child ...)        = 2717
2719:   open("/var/ld/ld.config", O_RDONLY)             = 3
2719:   open("/lib/libc.so.1", O_RDONLY)                = 3
2719:   read(0, 0x080621B8, 8192)                       = 0
2717:   read(0, 0x08076358, 128)                        = 0
/export/home/komeda/sh-bug.sh: syntax error at line 10: `end of file' unexpected

Korn shell や POSIX シェルでは起こらない

これらのシェルでは、スクリプトがいったんファイルディスクリプタ 0 でオープンされたあと、もっと大きな番号のファイルディスクリプタが使われるように fcntlF_DUPFD しているためである。

trussで見たようす

1956:   fcntl(0, F_DUPFD, 0x0000000A)                   = 10
1956:   close(0)                                        = 0
1956:   close(62)                                       Err#9 EBADF
1956:   fcntl(10, F_DUPFD, 0x0000003E)                  = 62
1956:   close(10)                                       = 0
1956:   fcntl(62, F_SETFD, 0x00000001)                  = 0
1956:   fcntl(62, F_GETFL)                              = 8192
1956:   llseek(62, 0, SEEK_CUR)                         = 0

まとめ

Solaris では /bin/sh を使用する場合、標準入力を /dev/null につなぎ替えておくのはかまわないが、close して起動してはいけない。

perlスクリプトの方は、exec の前に /dev/nullopen しておくことで問題を回避することにした。

POSIX::open("/dev/null", O_RDONLY, 0644);
exec($script) or die "exec failed $?\n";
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした