Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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に関連する操作と、ファイルディスクリプタの操作(特にdupClose-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()関数は以下の順序で処理を行っていました。

  1. syscall.Dup(fd.sysfd): 既存のソケットのファイルディスクリプタを複製し、新しいファイルディスクリプタnsを取得。
  2. syscall.CloseOnExec(ns): 新しいファイルディスクリプタnsClose-on-execフラグを設定。

この2つの操作の間に、別のゴルーチンがfork/execを実行する可能性がありました。もしfork/execsyscall.Dupの直後でsyscall.CloseOnExecの直前に発生した場合、複製されたファイルディスクリプタnsはまだClose-on-execフラグが設定されていないため、子プロセスに継承されてしまいます。これが「ファイルリーク」の原因でした。

このコミットでは、この競合状態を解消するためにsyscall.ForkLockが導入されました。

変更後のnetFD.dup()関数は以下のようになります。

  1. syscall.ForkLock.RLock(): ForkLockの読み込みロックを取得します。これにより、他のゴルーチンがfork/execの書き込みロックを取得するのを防ぎます。
  2. ns, err := syscall.Dup(fd.sysfd): ファイルディスクリプタを複製します。
  3. エラーが発生した場合: syscall.ForkLock.RUnlock()を呼び出してロックを解放し、エラーを返します。
  4. syscall.CloseOnExec(ns): 複製されたファイルディスクリプタにClose-on-execフラグを設定します。
  5. syscall.ForkLock.RUnlock(): ForkLockの読み込みロックを解放します。

この変更により、syscall.Dupsyscall.CloseOnExecの操作がForkLockの読み込みロックによって保護されるようになりました。つまり、これらの操作が実行されている間は、fork/exec操作はブロックされ、ファイルディスクリプタがClose-on-execフラグが設定されていない状態で子プロセスに継承される可能性がなくなりました。

また、src/pkg/os/exec/exec_test.goに追加されたTestExtraFilesRaceは、この競合状態を再現し、修正が正しく機能することを確認するためのテストです。このテストは、複数のゴルーチンで同時にTCPリスナーを作成し、そのファイルディスクリプタを子プロセスに渡す操作を繰り返し実行することで、競合状態が発生しないことを検証しています。

コアとなるコードの変更箇所

src/pkg/net/fd.gonetFD.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によって保護されることで、dupClose-on-execの間で競合状態が発生するのを防ぎます。

この変更により、net.TCPListener.File()が返す*os.Fileオブジェクトは、子プロセスがexecされた際に自動的に閉じられることが保証され、ファイルディスクリプタのリークが防止されます。

関連リンク

参考にした情報源リンク

  • 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言語のネットワークプログラミングに関する一般的な情報。