[インデックス 10872] ファイルの概要
このコミットは、Go言語の標準ライブラリであるos/exec
パッケージのテストスイートに、net
パッケージが使用するepoll
(またはそれに相当するI/O多重化メカニズム)のファイルディスクリプタ(FD)が、子プロセスに意図せず継承されないことを検証する新しいテストを追加するものです。これにより、子プロセスが不要なFDを保持することによるリソースリークやセキュリティリスクを防ぎます。
コミット
commit 178be83e0eb465156be32c69e59aba0f815fb746
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Dec 19 09:23:07 2011 -0800
exec: add test to verify net package's epoll fd doesn't go to child
R=rsc
CC=golang-dev
https://golang.org/cl/5490075
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/178be83e0eb465156be32c69e59aba0f815fb746
元コミット内容
exec: add test to verify net package's epoll fd doesn't go to child
このコミットは、net
パッケージが内部的に使用するepoll
(または他のOSにおける同等のI/O多重化メカニズム)に関連するファイルディスクリプタが、os/exec
パッケージによって起動される子プロセスに誤って継承されないことを確認するためのテストを追加します。
変更の背景
Unix系OSでは、fork()
システムコールによって子プロセスが生成されると、親プロセスの開いているファイルディスクリプタ(FD)がデフォルトで子プロセスに継承されます。これは多くの場合望ましい動作ですが、ネットワークソケットやepoll
インスタンスのような特定のFDは、子プロセスに継承されるべきではありません。
特に、net
パッケージが内部的に使用するepoll
FDが子プロセスに継承されると、以下のような問題が発生する可能性があります。
- リソースリーク: 子プロセスが終了しても、親プロセスが管理する
epoll
インスタンスへの参照が残る可能性があり、リソースが適切に解放されない。 - セキュリティリスク: 子プロセスが親プロセスのネットワーク活動を監視したり、意図しない操作を行ったりする可能性がある。
- 予期せぬ動作: 子プロセスが親プロセスの
epoll
FDを操作しようとすると、競合状態やデッドロックを引き起こす可能性がある。
Goのランタイムは、子プロセスを起動する際に、不要なFDが継承されないようにCLOEXEC
(Close-on-exec) フラグを設定する責任があります。このコミットは、net
パッケージが生成するFDが正しくCLOEXEC
フラグでマークされていることを検証するためのテストを追加することで、この重要な動作が保証されるようにします。
前提知識の解説
ファイルディスクリプタ (File Descriptor, FD)
Unix系OSにおいて、ファイルディスクリプタは、プロセスが開いているファイルやI/Oリソース(ソケット、パイプ、デバイスなど)を参照するための抽象的なハンドルです。各プロセスは、0から始まる整数値のFDを持ちます。
0
: 標準入力 (stdin)1
: 標準出力 (stdout)2
: 標準エラー出力 (stderr) これら以外のFDは、プログラムがファイルを開いたり、ソケットを作成したりする際にOSによって割り当てられます。
fork()
とFDの継承
fork()
システムコールは、現在のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)を作成します。この際、親プロセスが開いているすべてのファイルディスクリプタは、デフォルトで子プロセスに継承されます。つまり、親プロセスがFD 3でファイルを開いていた場合、子プロセスもFD 3で同じファイルを開いている状態になります。
exec()
とCLOEXEC
フラグ
exec()
システムコール群(例: execve()
, execlp()
など)は、現在のプロセスイメージを新しいプログラムで置き換えます。fork()
とexec()
は通常組み合わせて使用され、新しいプロセスを起動する一般的な方法です(fork()
で子プロセスを作成し、その子プロセスでexec()
を呼び出して新しいプログラムを実行する)。
exec()
が呼び出される際、デフォルトでは開いているFDは新しいプログラムに引き継がれます。しかし、特定のFDを新しいプログラムに引き継ぎたくない場合があります。このために、CLOEXEC
(Close-on-exec) フラグが存在します。
CLOEXEC
フラグは、FDがexec()
システムコールによって新しいプログラムが実行される際に自動的に閉じられるように設定するものです。これにより、子プロセスが不要なFDを継承するのを防ぎ、リソースリークやセキュリティ上の問題を回避できます。Goのos/exec
パッケージは、子プロセスを起動する際に、デフォルトでCLOEXEC
フラグを適切に設定するようになっています。
epoll
(Linux) / kqueue
(BSD/macOS) / I/O Completion Ports (Windows)
これらは、多数のI/O操作を効率的に多重化するためのOSレベルのメカニズムです。
epoll
: Linuxカーネルが提供する高性能なI/Oイベント通知メカニズム。多数のソケットやファイルディスクリプタからのイベント(読み取り可能、書き込み可能など)を効率的に監視できます。ノンブロッキングI/Oと組み合わせて、高並行なネットワークサーバーなどで広く利用されます。kqueue
: FreeBSD、macOS、NetBSD、OpenBSDなどのBSD系OSで利用される同様のメカニズム。- I/O Completion Ports (IOCP): Windowsで利用される高性能な非同期I/Oメカニズム。
Goのnet
パッケージは、これらのOS固有のI/O多重化メカニズムを内部的に利用して、ネットワーク接続の効率的な処理を実現しています。これらのメカニズム自体もファイルディスクリプタ(epoll_create
はFDを返す)を使用するため、それらのFDも子プロセスに継承されないようにCLOEXEC
フラグが設定されている必要があります。
技術的詳細
このコミットは、os/exec/exec_test.go
ファイルにTestExtraFiles
という既存のテスト関数を修正し、ネットワーク操作を強制的に実行することで、net
パッケージが内部的に使用するFDが子プロセスに漏洩しないことを検証します。
テストの基本的な流れは以下の通りです。
- ネットワークリスナーの作成:
net.Listen("tcp", "127.0.0.1:0")
を呼び出して、一時的なTCPリスナーを作成します。これにより、net
パッケージが内部的にソケットFDと、それを監視するためのepoll
(またはOS固有のI/O多重化)FDを生成します。 - 子プロセスの起動:
os/exec
パッケージを使用して子プロセスを起動します。この子プロセスは、親プロセスから特定のFD(このテストではFD 3)を継承し、その内容を読み取るように設定されています。 - FDの漏洩チェック: 子プロセス側で、継承されたFDのリストを走査し、予期しないFD(特に
net
パッケージが生成したFD)が存在しないことを確認します。
重要なのは、net.Listen
が呼び出されたときに作成されるFDが、子プロセスに継承されないようにCLOEXEC
フラグが設定されていることです。もし設定されていなければ、子プロセスは親プロセスのネットワークリスナーに関連するFDを継承してしまい、テストは失敗します。
コミットのコメントにあるTODO(bradfitz,iant): the rest of this test is disabled for now. remove this block once 5494061 is in.
という部分は、当初はより厳密なFD漏洩チェック(FD 3以外のすべてのFDが閉じられていることを確認する)を意図していたものの、当時のGoのランタイムの制約により、一時的にその部分を無効化していたことを示唆しています。しかし、このコミットの主要な目的である「net
パッケージのFDが子プロセスに漏洩しないこと」の検証は、ネットワークリスナーの作成と子プロセスの起動によって達成されています。
コアとなるコードの変更箇所
src/pkg/os/exec/exec_test.go
--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"io/ioutil"
+ "net"
"os"
"runtime"
"strconv"
@@ -146,6 +147,15 @@ func TestExtraFiles(t *testing.T) {
t.Logf("no operating system support; skipping")
return
}
+
+ // Force network usage, to verify the epoll (or whatever) fd
+ // doesn't leak to the child,
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ln.Close()
+
tf, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("TempFile: %v", err)
@@ -167,7 +177,7 @@ func TestExtraFiles(t *testing.T) {
c.ExtraFiles = []*os.File{tf}
bs, err := c.CombinedOutput()
if err != nil {
- t.Fatalf("CombinedOutput: %v", err)
+ t.Fatalf("CombinedOutput: %v; output %q", err, bs)
}
if string(bs) != text {
t.Errorf("got %q; want %q", string(bs), text)
@@ -246,6 +256,29 @@ func TestHelperProcess(*testing.T) {
fmt.Printf("ReadAll from fd 3: %v", err)
os.Exit(1)
}
+ // TODO(bradfitz,iant): the rest of this test is disabled
+ // for now. remove this block once 5494061 is in.
+ {
+ os.Stderr.Write(bs)
+ os.Exit(0)
+ }
+ // Now verify that there are no other open fds.
+ var files []*os.File
+ for wantfd := os.Stderr.Fd() + 2; wantfd <= 100; wantfd++ {
+ f, err := os.Open(os.Args[0])
+ if err != nil {
+ fmt.Printf("error opening file with expected fd %d: %v", wantfd, err)
+ os.Exit(1)
+ }
+ if got := f.Fd(); got != wantfd {
+ fmt.Printf("leaked parent file. fd = %d; want %d", got, wantfd)
+ os.Exit(1)
+ }
+ files = append(files, f)
+ }
+ for _, f := range files {
+ f.Close()
+ }
os.Stderr.Write(bs)
case "exit":
n, _ := strconv.Atoi(args[0])
コアとなるコードの解説
TestExtraFiles
関数の変更
-
"net"
パッケージのインポート追加:+ "net"
ネットワーク操作を行うために
net
パッケージをインポートします。 -
ネットワークリスナーの作成:
+ // Force network usage, to verify the epoll (or whatever) fd + // doesn't leak to the child, + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close()
net.Listen
を呼び出すことで、TCPリスナーが作成されます。この操作により、OSレベルでソケットFDと、それを監視するためのepoll
(Linuxの場合)または同等のI/O多重化メカニズムのFDが生成されます。127.0.0.1:0
は、ループバックアドレス上の利用可能な任意のポートでリッスンすることを意味します。defer ln.Close()
は、テスト関数が終了する際にリスナーを確実にクローズし、関連するFDを解放します。このステップが、net
パッケージがFDを生成するトリガーとなります。 -
エラーメッセージの改善:
- t.Fatalf("CombinedOutput: %v", err) + t.Fatalf("CombinedOutput: %v; output %q", err, bs)
子プロセスの実行結果(
bs
)もエラーメッセージに含めることで、デバッグ時の情報量を増やしています。
TestHelperProcess
関数の変更(コメントアウトされた部分)
+ // TODO(bradfitz,iant): the rest of this test is disabled
+ // for now. remove this block once 5494061 is in.
+ {
+ os.Stderr.Write(bs)
+ os.Exit(0)
+ }
+ // Now verify that there are no other open fds.
+ var files []*os.File
+ for wantfd := os.Stderr.Fd() + 2; wantfd <= 100; wantfd++ {
+ f, err := os.Open(os.Args[0])
+ if err != nil {
+ fmt.Printf("error opening file with expected fd %d: %v", wantfd, err)
+ os.Exit(1)
+ }
+ if got := f.Fd(); got != wantfd {
+ fmt.Printf("leaked parent file. fd = %d; want %d", got, wantfd)
+ os.Exit(1)
+ }
+ files = append(files, f)
+ }
+ for _, f := range files {
+ f.Close()
+ }
この部分は、子プロセス側でFDの漏洩をより厳密にチェックしようとしたコードですが、コミット時点ではTODO
コメントと共にコメントアウトされています。これは、os.Stderr.Fd() + 2
からFD 100までを走査し、それぞれos.Open(os.Args[0])
を試みることで、予期しないFDがオープンされていないかを確認するものです。もし予期しないFDがオープンされていれば、os.Open
は新しいFDを割り当てられず、エラーになるか、あるいは既存のFDを再利用してしまい、got != wantfd
のチェックで漏洩が検出されるはずです。
しかし、このコードブロックはos.Exit(0)
によってすぐに終了するため、実質的には実行されません。これは、当時のGoランタイムのFD管理の複雑さや、特定のプラットフォームでの挙動の違いにより、この汎用的なチェックが困難であったためと考えられます。コミットメッセージにある5494061
は、関連する別の変更リスト(CL)を参照しており、そのCLがマージされればこの部分が有効になる予定だったことを示唆しています。
このコミットの主な価値は、net.Listen
を呼び出すことでnet
パッケージがFDを生成する状況を作り出し、そのFDが子プロセスに継承されないことを、TestExtraFiles
の既存のフレームワーク(子プロセスが特定のFDのみを継承することを期待する)を通じて間接的に検証している点にあります。もしnet
パッケージが生成するFDにCLOEXEC
フラグが適切に設定されていなければ、子プロセスは予期しないFDを継承し、テストは失敗するでしょう。
関連リンク
- Go issue tracker: https://go.dev/issue/2674 (このコミットに関連する可能性のある一般的なFD継承の問題)
- Go CL 5490075: https://go.dev/cl/5490075 (このコミットのChange Listページ)
参考にした情報源リンク
epoll
man page: https://man7.org/linux/man-pages/man7/epoll.7.htmlfork()
man page: https://man7.org/linux/man-pages/man2/fork.2.htmlexecve()
man page: https://man7.org/linux/man-pages/man2/execve.2.htmlfcntl()
man page (forCLOEXEC
): https://man7.org/linux/man-pages/man2/fcntl.2.html- Go
os/exec
documentation: https://pkg.go.dev/os/exec - Go
net
documentation: https://pkg.go.dev/net - "The Go Programming Language" by Alan A. A. Donovan and Brian W. Kernighan (Goの内部動作に関する一般的な情報源)
- "Unix Network Programming, Volume 1: The Sockets Networking API" by W. Richard Stevens (ソケットとI/O多重化に関する詳細な情報源)
- Stack Overflowや技術ブログ記事 (GoのFD管理、
CLOEXEC
に関する一般的な議論)