仙石浩明の日記

2009年12月4日

apache httpd が 「(22)Invalid argument: alloc_listener: failed to get a socket for (null)」 エラーを出して立ち上がらない理由

私は普段持ち歩いているノートPC (レッツノート CF-R7) で coLinux を常用している。 Windows マシンで Linux を使いたい場合、 QEMU や VMware などの PC エミュレータ (完全仮想化) を用いる方法もあるが、 coLinux などの準仮想化の方がパフォーマンス的に有利なので、 日常的に使用する (私の場合、Windows マシンを使っていながらほとんどの作業は Linux の中で完結させている) なら coLinux の方が便利だと思う。

もちろん、 準仮想化であるから標準的なカーネルをそのまま走らせることはできず、 パッチをあてる必要がある。 残念ながら最新カーネル用のパッチはまだ作られていないようで、 現時点では Linux kernel 2.6.26.8 用のパッチが最新。

私は自分で管理している Linux マシン (自宅と職場合わせて 10台以上ある) は、 この coLinux なマシンも含めてハードディスク (正確に言えば Linux のパーティション) の内容を同一にしている。 すなわち、 マスタマシン (senri.gcd.org) の内容を定期/不定期的に rsync を使って各マシンへ同期させている。 マスタマシンのカーネルは Linux 2.6.31.6 なので、 マスタでビルドしたソフトウェアの中には、 Linux 2.6.26.8 ベースである coLinux 環境で動かないものも当然でてくる。

先日 apache httpd 2.2.14 を (マスタマシンで) ビルドしたら、 coLinux 環境で動かなかった:

# uname -a
Linux ikeda.gcd.org 2.6.26.8-co-0.8.0 #1 PREEMPT Sat Nov 14 19:23:55 JST 2009 i686 GNU/Linux
# /usr/apache2/bin/httpd -t
[Sun Nov 29 17:13:41 2009] [crit] (22)Invalid argument: alloc_listener: failed to get a socket for (null)
Syntax error on line 32 of /usr/apache2/conf/httpd.conf:
Listen setup failed

「Syntax error on line 32」 ということだが httpd.conf の 32行目は、

Listen 80

となっているので、 少なくとも 「Syntax error」 ではないことは明らか。 「Listen 80」 の代りに 「Listen localhost:80」 などと書けば、 「failed to get a socket for (null)」 というエラーメッセージが 「failed to get a socket for localhost」 に変わる。

このエラーメッセージを手がかりに apache httpd 2.2.14 のソースを探すと、 エラーを出しているのは server/listen.c の以下の部分:

        status = apr_socket_create(&new->sd, new->bind_addr->family,
                                    SOCK_STREAM, 0, process->pool);
        ...
        if (status != APR_SUCCESS) {
            ap_log_perror(APLOG_MARK, APLOG_CRIT, status, process->pool,
                          "alloc_listener: failed to get a socket for %s",
                          addr);
            return "Listen setup failed";
        }

つまり apr_socket_create 関数でソケットを生成しようとしてエラーが起きている。 APR つまり Apache Portable Runtime の中でのエラー。 真面目に APR のソースを追ってもいいのだが、 エラーメッセージに 22 (= EINVAL) と出ていることから socket(2) システムコールが EINVAL (Unknown protocol, or protocol family not available) エラーを返しているのだろうとあたりをつけて、 strace コマンドを使って手っ取り早く確認してみた:

# strace /usr/apache2/bin/httpd -t
execve("/usr/apache2/bin/httpd", ["/usr/apache2/bin/httpd", "-t"], [/* 28 vars */]) = 0
brk(0)                                  = 0x80a6000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/usr/etc/ld.so.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=66428, ...}) = 0
old_mmap(NULL, 66428, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7ffe000
close(3)                                = 0
open("/usr/lib/libm.so.6", O_RDONLY)  = 3
        ...
socket(PF_INET, 0x80001 /* SOCK_??? */, IPPROTO_IP) = -1 EINVAL (Invalid argument)
        ...

確かに socket(2) が EINVAL を返している。 socket の第1, 3 引数はエラーになりようがないので、 第2 引数 「0x80001」 が原因だろう。 この値は、 「SOCK_STREAM | SOCK_CLOEXEC」 という意味なので、 Linux 2.6.26.8 の socket(2) が 「SOCK_CLOEXEC」 フラグをサポートしていないのだろうと推測できる。 実際、 カーネルのソースを確認してみると SOCK_CLOEXEC をサポートしているのは Linux 2.6.27 以降だった。

CLOEXEC というのは close-on-exec の意味。 ファイルディスクリプタには FD_CLOEXEC フラグを設定できて、 このフラグが立っているファイルディスクリプタは、 exec を行なうと自動的にクローズされる。 fcntl(2) でこのフラグの設定/解除ができるが、 SOCK_CLOEXEC を付けて socket(2) を呼び出すと、 ソケットをオープンすると同時に FD_CLOEXEC フラグを設定できる。 open(2) でも同様に O_CLOEXEC を付けることができる (Linux 2.6.23 以降)。

open() システムコールと fcntl(FD_CLOEXEC) とが、 2つの手順に分離されていることが問題だといいます。 もし 「open」 と 「fcntl」 という 2つの処理のすき間で、 別スレッド、またはシグナルハンドラから fork が呼ばれると、 ファイルディスクリプタが閉じられずにリークしてしまいます。 もし、 exec したプログラムに脆弱性が含まれていた場合、 親プロセスがオープンしていたファイルまでもが危険にさらされることになります。
...(中略)...
Linux においては、 open 以外にも新規ファイルディスクリプタを作成して返却するシステムコールは大量にあります。 そのすべてにおいて、 フラグ引数に 「XX_CLOEXEC フラグ」 を追加する、 またはフラグ引数を追加した新システムコールを新設するという修正が行われました。

というわけでエラーの原因は分かったが、 どうやって対策したものか。 coLinux が 2.6.27 以降をサポートするのはまだ先のことになりそうだし、 かといって SOCK_CLOEXEC 付で socket(2) がエラーを返さないようにする小手先パッチを作るのは、 他の全てのプログラムに影響するのであまりよろしくない (実は SOCK_CLOEXEC が付いていてもエラーにしないパッチも作ってみたのだが、 APR は他にも Linux 2.6.31.6 ではサポートされているが Linux 2.6.26.8 ではサポートされていない機能を使ってるらしく、 httpd の子プロセスがゾンビになってしまった)。

というわけで、 Linux 2.6.26.8 (つまり coLinux 環境) 下で APR ライブラリをビルドし直し、 得られた libapr-1.so を /usr/lib/colinux ディレクトリに入れておいて、 coLinux 環境ではこのディレクトリを LD_LIBRARY_PATH を指定して apache httpd を実行することにした。

ikeda:/usr/local/src/apr-1.3.9 % ./configure
        ...
checking for epoll_create1 support... no
checking for dup3 support... no
checking for accept4 support... no
checking for SOCK_CLOEXEC support... no
        ...
ikeda:/usr/local/src/apr-1.3.9 % make
ikeda:/usr/local/src/apr-1.3.9 % fg
# cp -d /usr/local/src/apr-1.3.9/.libs/*.so* /usr/lib/colinux/.
# ls -l /usr/lib/colinux
total 572
lrwxrwxrwx 1 root root     17 Dec  3 08:46 libapr-1.so -> libapr-1.so.0.3.9
lrwxrwxrwx 1 root root     17 Dec  3 08:46 libapr-1.so.0 -> libapr-1.so.0.3.9
-rwxr-xr-x 1 root root 578999 Dec  3 08:46 libapr-1.so.0.3.9
# LD_LIBRARY_PATH=/usr/lib/colinux /usr/apache2/bin/httpd

無事 coLinux 環境においても httpd サーバを実行することができた。

なお、 epoll_create1(2), dup3(2), accept4(2) は、 それぞれ epoll_create(2), dup2(2), accept(2) に int flags 引数を追加し、 close-on-exec フラグを設定できるようにした Linux 独自の拡張。 いずれも Linux 2.6.26 以前ではサポートされない。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 07:24

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.