[インデックス 13581] ファイルの概要
このコミットでは、主にGo言語のネットワークパッケージ(net
)におけるファイルディスクリプタの扱いと、それに関連するテストコードが変更されています。具体的には、src/pkg/net/fd.go
が修正され、src/pkg/os/exec/exec_test.go
に新しいテストが追加されています。
コミット
- コミットハッシュ:
2d2866ee8454d096d0dc8b5906324a36cfb3cc6e
- 作者: Brad Fitzpatrick bradfitz@golang.org
- コミット日時: Mon Aug 6 14:12:23 2012 +1000
- 変更ファイル:
src/pkg/net/fd.go
: 4行追加src/pkg/os/exec/exec_test.go
: 60行追加
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2d2866ee8454d096d0dc8b5906324a36cfb3cc6e
元コミット内容
net: fix TCPListener file leak to child processes
Hold ForkLock during dup of fd + cloexec in the net pkg,
per the locking policy documented in syscall/exec_unix.go.
R=golang-dev, dsymonds, adg
CC=golang-dev
https://golang.org/cl/6457080
変更の背景
このコミットは、Goプログラムが子プロセスを生成する際に、親プロセスがオープンしているTCPリスナーのファイルディスクリプタが意図せず子プロセスに「リーク」してしまう問題を修正するために行われました。
Unix系のシステムでは、fork
システムコールによって子プロセスが生成されると、親プロセスのファイルディスクリプタが子プロセスに継承されます。通常、サーバーアプリケーションがTCPリスナーを開いている場合、そのリスナーのファイルディスクリプタは子プロセスには不要であり、むしろセキュリティやリソース管理の観点から継承されるべきではありません。
この問題は、特にnet.TCPListener.File()
メソッドを使用してリスナーのファイルディスクリプタを取得し、それを子プロセスに渡すようなシナリオで顕在化します。dup
システムコール(Goではsyscall.Dup
)でファイルディスクリプタを複製し、その複製に対してClose-on-exec
フラグ(FD_CLOEXEC
)を設定する際に、競合状態(race condition)が発生する可能性がありました。
具体的には、dup
でファイルディスクリプタを複製した後、Close-on-exec
フラグを設定するまでのごく短い期間に、別のゴルーチンがfork
/exec
を実行してしまうと、Close-on-exec
が設定されていない状態のファイルディスクリプタが子プロセスに継承されてしまう、という問題です。これにより、子プロセスが不要なファイルディスクリプタを保持し続ける「ファイルリーク」が発生していました。
このコミットは、この競合状態を解消し、ファイルディスクリプタが安全に子プロセスに渡されないようにするために、syscall.ForkLock
というミューテックスを使用するように変更されました。
前提知識の解説
1. ファイルディスクリプタ (File Descriptor, FD)
Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースは、整数値で表現される「ファイルディスクリプタ」を通じてアクセスされます。プログラムがファイルを開いたり、ネットワーク接続を確立したりすると、OSは対応するファイルディスクリプタを返します。
2. dup
システムコールとファイルディスクリプタの複製
dup
(duplicate file descriptor) システムコールは、既存のファイルディスクリプタを複製するために使用されます。複製されたファイルディスクリプタは、元のファイルディスクリプタと同じファイル記述エントリ(ファイルオフセット、ファイルステータスフラグなど)を参照します。Go言語では、syscall.Dup
関数がこれに相当します。
3. Close-on-exec
フラグ (FD_CLOEXEC
)
Close-on-exec
(Close on Execute) フラグは、ファイルディスクリプタに設定できるフラグの一つです。このフラグが設定されたファイルディスクリプタは、exec
システムコール(新しいプログラムを実行する際に使用される)が成功した際に自動的に閉じられます。これにより、子プロセスが親プロセスから不要なファイルディスクリプタを継承するのを防ぐことができます。Go言語では、syscall.CloseOnExec
関数がこのフラグを設定します。
4. fork
とファイルディスクリプタの継承
Unix系OSでは、fork
システムコールによって子プロセスが生成されると、子プロセスは親プロセスのメモリ空間のコピーと、オープンしているファイルディスクリプタのコピーを継承します。これは、親プロセスと子プロセスが同じファイルディスクリプタを共有することを意味します。exec
システムコールが呼び出されると、通常は新しいプログラムがロードされ、既存のファイルディスクリプタはClose-on-exec
フラグが設定されていない限り継承されます。
5. syscall.ForkLock
Go言語のsyscall
パッケージには、ForkLock
というグローバルなミューテックス(読み書きロック)が存在します。これは、fork
/exec
に関連する操作と、ファイルディスクリプタの操作(特にdup
やClose-on-exec
の設定)との間の競合状態を防ぐために設計されています。
syscall/exec_unix.go
に記述されているポリシーによれば、ForkLock
は以下の目的で使用されます。
fork
またはexec
を呼び出す前に、ForkLock
の書き込みロック(Lock()
)を取得する。- ファイルディスクリプタを複製し、
Close-on-exec
フラグを設定するような操作を行う際には、ForkLock
の読み込みロック(RLock()
)を取得する。
これにより、fork
/exec
が実行されている間は、ファイルディスクリプタの複製とClose-on-exec
の設定がブロックされ、またその逆も同様にブロックされることで、ファイルディスクリプタのリークを防ぐことができます。
技術的詳細
このコミットの核心は、net.TCPListener.File()
メソッドが内部で呼び出すnetFD.dup()
関数におけるsyscall.ForkLock
の使用です。
以前の実装では、netFD.dup()
関数は以下の順序で処理を行っていました。
syscall.Dup(fd.sysfd)
: 既存のソケットのファイルディスクリプタを複製し、新しいファイルディスクリプタns
を取得。syscall.CloseOnExec(ns)
: 新しいファイルディスクリプタns
にClose-on-exec
フラグを設定。
この2つの操作の間に、別のゴルーチンがfork
/exec
を実行する可能性がありました。もしfork
/exec
がsyscall.Dup
の直後でsyscall.CloseOnExec
の直前に発生した場合、複製されたファイルディスクリプタns
はまだClose-on-exec
フラグが設定されていないため、子プロセスに継承されてしまいます。これが「ファイルリーク」の原因でした。
このコミットでは、この競合状態を解消するためにsyscall.ForkLock
が導入されました。
変更後のnetFD.dup()
関数は以下のようになります。
syscall.ForkLock.RLock()
:ForkLock
の読み込みロックを取得します。これにより、他のゴルーチンがfork
/exec
の書き込みロックを取得するのを防ぎます。ns, err := syscall.Dup(fd.sysfd)
: ファイルディスクリプタを複製します。- エラーが発生した場合:
syscall.ForkLock.RUnlock()
を呼び出してロックを解放し、エラーを返します。 syscall.CloseOnExec(ns)
: 複製されたファイルディスクリプタにClose-on-exec
フラグを設定します。syscall.ForkLock.RUnlock()
:ForkLock
の読み込みロックを解放します。
この変更により、syscall.Dup
とsyscall.CloseOnExec
の操作がForkLock
の読み込みロックによって保護されるようになりました。つまり、これらの操作が実行されている間は、fork
/exec
操作はブロックされ、ファイルディスクリプタがClose-on-exec
フラグが設定されていない状態で子プロセスに継承される可能性がなくなりました。
また、src/pkg/os/exec/exec_test.go
に追加されたTestExtraFilesRace
は、この競合状態を再現し、修正が正しく機能することを確認するためのテストです。このテストは、複数のゴルーチンで同時にTCPリスナーを作成し、そのファイルディスクリプタを子プロセスに渡す操作を繰り返し実行することで、競合状態が発生しないことを検証しています。
コアとなるコードの変更箇所
src/pkg/net/fd.go
のnetFD.dup()
関数に以下の変更が加えられました。
--- a/src/pkg/net/fd.go
+++ b/src/pkg/net/fd.go
@@ -645,10 +645,14 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (netfd *netFD, err e
}
func (fd *netFD) dup() (f *os.File, err error) {
+\tsyscall.ForkLock.RLock()\
\tns, err := syscall.Dup(fd.sysfd)\
\tif err != nil {\
+\t\tsyscall.ForkLock.RUnlock()\
\t\treturn nil, &OpError{"dup", fd.net, fd.laddr, err}\
\t}\
+\tsyscall.CloseOnExec(ns)\
+\tsyscall.ForkLock.RUnlock()\
\t// We want blocking mode for the new fd, hence the double negative.\
\tif err = syscall.SetNonblock(ns, false); err != nil {
コアとなるコードの解説
syscall.ForkLock.RLock()
:netFD.dup()
関数の冒頭で、syscall.ForkLock
の読み込みロックを取得しています。これにより、この関数が実行されている間は、fork
/exec
操作(ForkLock
の書き込みロックを必要とする)がブロックされます。syscall.ForkLock.RUnlock()
:syscall.Dup
がエラーを返した場合、またはsyscall.CloseOnExec
が成功した後に、読み込みロックを解放しています。これにより、他のゴルーチンがForkLock
を取得できるようになります。syscall.CloseOnExec(ns)
:syscall.Dup
で複製された新しいファイルディスクリプタns
に対して、Close-on-exec
フラグを設定しています。この操作がForkLock
によって保護されることで、dup
とClose-on-exec
の間で競合状態が発生するのを防ぎます。
この変更により、net.TCPListener.File()
が返す*os.File
オブジェクトは、子プロセスがexec
された際に自動的に閉じられることが保証され、ファイルディスクリプタのリークが防止されます。
関連リンク
- Go CL 6457080: https://golang.org/cl/6457080
参考にした情報源リンク
- Go言語の
syscall
パッケージのドキュメント (特にForkLock
に関する記述):src/pkg/syscall/exec_unix.go
(Goソースコード)
- Unix系OSにおける
fork
,exec
,dup
,FD_CLOEXEC
に関する一般的なドキュメント:man 2 fork
man 2 execve
man 2 dup
man 2 fcntl
(特にFD_CLOEXEC
について)
- Go言語のネットワークプログラミングに関する一般的な情報。