[インデックス 13748] ファイルの概要
このコミットは、Go言語の標準ライブラリnet
パッケージにおいて、FileConn
、FileListener
、FilePacketConn
といったファイルディスクリプタ(FD)を扱う関数が、子プロセスにFDをリークする問題を修正するものです。具体的には、newFileFD
関数がファイルディスクリプタを複製(duplicate)する際に、close-on-exec
フラグが適切に設定されないために発生するリークを防ぎます。
コミット
commit 2836c63459ac9619cc3c2cf894ece83f85ce5190
Author: Sébastien Paolacci <sebastien.paolacci@gmail.com>
Date: Tue Sep 4 12:37:23 2012 -0700
net: fix {FileConn, FileListener, FilePacketConn} fd leak to child process.
All of them call `newFileFD' which must properly restore close-on-exec on
duplicated fds.
R=golang-dev, bradfitz, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/6445081
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2836c63459ac9619cc3c2cf894ece83f85ce5190
元コミット内容
diff --git a/src/pkg/net/file.go b/src/pkg/net/file.go
index 11c8f77a82..60911b17d3 100644
--- a/src/pkg/net/file.go
+++ b/src/pkg/net/file.go
@@ -12,10 +12,14 @@ import (
)
func newFileFD(f *os.File) (*netFD, error) {
+\tsyscall.ForkLock.RLock()
\tfd, err := syscall.Dup(int(f.Fd()))
\tif err != nil {
+\t\tsyscall.ForkLock.RUnlock()
\t\treturn nil, os.NewSyscallError("dup", err)
\t}\n+\tsyscall.CloseOnExec(fd)\n+\tsyscall.ForkLock.RUnlock()\n \n \tsotype, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)\n \tif err != nil {
diff --git a/src/pkg/os/exec/exec_test.go b/src/pkg/os/exec/exec_test.go
index 2cc053e5bc..af07452b46 100644
--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -167,6 +167,18 @@ func TestExtraFiles(t *testing.T) {
}\n \tdefer ln.Close()\n \n+\t// Make sure duplicated fds don't leak to the child.\n+\tf, err := ln.(*net.TCPListener).File()\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tdefer f.Close()\n+\tln2, err := net.FileListener(f)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tdefer ln2.Close()\n+\n \t// Force TLS root certs to be loaded (which might involve\n \t// cgo), to make sure none of that potential C code leaks fds.\n \tts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
変更の背景
この変更は、Goプログラムが子プロセスを起動する際に、親プロセスが持つファイルディスクリプタ(FD)が意図せず子プロセスに継承されてしまう「FDリーク」の問題を解決するために行われました。特に、net
パッケージのFileConn
、FileListener
、FilePacketConn
といった関数は、既存のファイルディスクリプタからネットワーク接続やリスナーを再構築するために使用されます。これらの関数内部でnewFileFD
が呼び出され、そこでsyscall.Dup
を使ってFDが複製されます。
問題は、syscall.Dup
がFDを複製する際に、元のFDに設定されていたclose-on-exec
フラグ(FD_CLOEXEC)が新しいFDに引き継がれないことにありました。close-on-exec
フラグは、exec
システムコール(新しいプログラムを実行する際に呼び出される)が実行されるときに、そのFDを自動的に閉じるように指示するものです。このフラグが設定されていないFDは、子プロセスが新しいプログラムを実行しても閉じられず、子プロセスに継承されてしまいます。
これにより、以下のような問題が発生する可能性がありました。
- リソースリーク: 不要なFDが子プロセスに継承され続けることで、システム全体で利用可能なFDの数が枯渇する可能性があります。
- セキュリティリスク: 親プロセスが持つ機密性の高いFD(例えば、認証情報を含むファイルやソケット)が、意図しない子プロセスに公開されてしまうリスクがあります。
- 予期せぬ動作: 子プロセスが継承したFDを誤って操作したり、閉じたりすることで、親プロセスや他の関連プロセスに悪影響を与える可能性があります。
このコミットは、newFileFD
関数で複製されたFDに対して明示的にclose-on-exec
フラグを設定することで、このリークを防ぎ、より堅牢で安全なプログラムの実行を保証します。
前提知識の解説
1. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プロセスはFDを通じてこれらのリソースにアクセスします。
2. dup
システムコールとファイルディスクリプタの複製
dup
(duplicate file descriptor)システムコールは、既存のファイルディスクリプタを複製し、新しいファイルディスクリプタを返します。新しいFDは元のFDと同じファイル記述エントリ(ファイルオフセット、ファイルステータスフラグなど)を参照します。これにより、同じファイルやソケットに対して複数のFDを持つことができます。
3. close-on-exec
フラグ (FD_CLOEXEC)
close-on-exec
フラグは、ファイルディスクリプタに設定できるフラグの一つです。このフラグが設定されたFDは、プロセスがexec
システムコール(execve
, execl
, execvp
など、新しいプログラムを実行する際に呼び出される)を実行する際に、カーネルによって自動的に閉じられます。
なぜ重要か?
Unix系OSでは、fork
システムコールによって子プロセスが作成されると、子プロセスは親プロセスのすべてのファイルディスクリプタを継承します。通常、子プロセスが親プロセスとは異なる新しいプログラムを実行する場合(exec
)、親プロセスのFDは不要であり、むしろリークやセキュリティリスクの原因となることがあります。close-on-exec
フラグは、このような不要なFDの継承を防ぐための重要なメカニズムです。
4. fork
とexec
システムコール
fork
: 現在のプロセス(親プロセス)のコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタ、その他のリソースのコピーを受け取ります。exec
: 現在のプロセスイメージを、指定された新しいプログラムイメージで置き換えます。プロセスIDは変わりませんが、実行されるコード、データ、スタックはすべて新しいプログラムのものになります。exec
が成功すると、元のプログラムは実行を終了し、新しいプログラムが開始されます。
多くのプログラムでは、fork
で子プロセスを作成し、その子プロセスでexec
を呼び出して別のプログラムを実行するというパターンが使われます。
5. Go言語のsyscall
パッケージとForkLock
Go言語のsyscall
パッケージは、低レベルのシステムコールへのアクセスを提供します。これには、ファイルディスクリプタの操作(Dup
, CloseOnExec
など)や、プロセス管理(ForkLock
)に関する機能が含まれます。
syscall.ForkLock
は、Goランタイムがfork
システムコールを安全に実行するためのミューテックス(排他ロック)です。Goのランタイムは、ガベージコレクションやスケジューラなど、多くの内部状態を持っています。fork
はプロセスの状態をそのままコピーするため、これらの内部状態が不整合な状態でコピーされると、子プロセスで問題が発生する可能性があります。ForkLock
は、fork
が実行される際にGoランタイムの特定の操作を一時停止させ、安全な状態を確保するために使用されます。RLock()
は読み取りロック、RUnlock()
は読み取りロックの解除です。
技術的詳細
このコミットの技術的詳細な修正点は、src/pkg/net/file.go
内のnewFileFD
関数に集約されています。
newFileFD
関数は、os.File
オブジェクトから新しいネットワークファイルディスクリプタ(netFD
)を作成する役割を担っています。このプロセスには、元のos.File
が持つファイルディスクリプタを複製することが含まれます。
修正前のコードでは、以下の手順でFDが複製されていました。
syscall.Dup(int(f.Fd()))
を呼び出して、元のos.File
のFDを複製し、新しいFD(fd
)を取得します。- この新しい
fd
を使用して、ソケットタイプなどの情報を取得し、netFD
構造体を初期化します。
この問題は、syscall.Dup
がFDを複製する際に、元のFDに設定されていたclose-on-exec
フラグを新しいFDに引き継がないという、Unix系OSの一般的な挙動に起因します。つまり、元のFDがclose-on-exec
に設定されていても、複製されたFDはデフォルトでclose-on-exec
が設定されていない状態になります。
修正後のコードでは、この問題に対処するために以下の変更が加えられました。
-
syscall.ForkLock.RLock()
の追加:syscall.Dup
を呼び出す前に、syscall.ForkLock.RLock()
が呼び出されます。これは、Goランタイムがfork
システムコールを安全に実行するための読み取りロックを取得します。これにより、dup
操作中にGoランタイムの内部状態が不整合になることを防ぎ、特にfork
/exec
のシーケンスにおいて競合状態が発生するのを抑制します。dup
はfork
と密接に関連する操作であり、fork
の安全性を確保するためのロックと連携させることで、より堅牢な動作が期待されます。 -
syscall.CloseOnExec(fd)
の追加:syscall.Dup
によって新しいFD(fd
)が正常に作成された直後に、syscall.CloseOnExec(fd)
が呼び出されます。この関数は、複製されたFDに対して明示的にclose-on-exec
フラグを設定します。これにより、このFDが子プロセスに継承されたとしても、子プロセスがexec
システムコールを実行する際には自動的に閉じられるようになります。 -
syscall.ForkLock.RUnlock()
の追加:syscall.Dup
の呼び出し後、およびエラーハンドリングのパスの両方で、syscall.ForkLock.RUnlock()
が呼び出され、取得した読み取りロックが解放されます。
この修正により、net.FileConn
、net.FileListener
、net.FilePacketConn
が内部でnewFileFD
を使用する際に、複製されたファイルディスクリプタが子プロセスに意図せずリークすることがなくなります。これは、Goプログラムが外部コマンドを実行する際の安定性とセキュリティを向上させる上で非常に重要です。
また、src/pkg/os/exec/exec_test.go
には、この修正が正しく機能することを確認するための新しいテストケースが追加されました。このテストは、net.TCPListener
からファイルディスクリプタを取得し、それをnet.FileListener
で再度開くという、まさにリークが発生しうるシナリオを再現しています。このテストが成功することで、複製されたFDが子プロセスにリークしないことが保証されます。
コアとなるコードの変更箇所
src/pkg/net/file.go
func newFileFD(f *os.File) (*netFD, error) {
+ syscall.ForkLock.RLock()
fd, err := syscall.Dup(int(f.Fd()))
if err != nil {
+ syscall.ForkLock.RUnlock()
return nil, os.NewSyscallError("dup", err)
}
+ syscall.CloseOnExec(fd)
+ syscall.ForkLock.RUnlock()
sotype, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)
if err != nil {
src/pkg/os/exec/exec_test.go
func TestExtraFiles(t *testing.T) {
// ... 既存のコード ...
// Make sure duplicated fds don't leak to the child.
f, err := ln.(*net.TCPListener).File()
if err != nil {
t.Fatal(err)
}
defer f.Close()
ln2, err := net.FileListener(f)
if err != nil {
t.Fatal(err)
}
defer ln2.Close()
// ... 既存のコード ...
}
コアとなるコードの解説
src/pkg/net/file.go
の変更
newFileFD
関数は、os.File
オブジェクトから新しいネットワークファイルディスクリプタ(netFD
)を作成する際に呼び出されます。
-
syscall.ForkLock.RLock()
:syscall.Dup
を呼び出す前に、GoランタイムのForkLock
を読み取りモードでロックします。これは、fork
システムコールが実行される際にGoランタイムの内部状態が安全であることを保証するためのものです。dup
操作はfork
と関連するコンテキストでよく使用されるため、このロックは競合状態を防ぎ、堅牢性を高めます。 -
syscall.Dup(int(f.Fd()))
: 元のos.File
が持つファイルディスクリプタ(f.Fd()
)を複製します。これにより、新しいファイルディスクリプタfd
が作成されます。 -
エラーハンドリング内の
syscall.ForkLock.RUnlock()
:syscall.Dup
がエラーを返した場合、取得したロックを解放してからエラーを返します。 -
syscall.CloseOnExec(fd)
: これがこのコミットの核心的な修正です。syscall.Dup
によって複製された新しいファイルディスクリプタfd
に対して、明示的にclose-on-exec
フラグを設定します。これにより、このfd
が子プロセスに継承されたとしても、子プロセスがexec
システムコールを実行する際には自動的に閉じられるようになります。これにより、FDリークが防止されます。 -
syscall.ForkLock.RUnlock()
:syscall.CloseOnExec
の呼び出し後、取得した読み取りロックを解放します。
src/pkg/os/exec/exec_test.go
の変更
このファイルには、TestExtraFiles
という既存のテスト関数に新しいテストケースが追加されました。
-
f, err := ln.(*net.TCPListener).File()
: 既存のTCPリスナーln
から、その基盤となるos.File
オブジェクトを取得します。このos.File
は、ネットワークソケットに対応するファイルディスクリプタをカプセル化しています。 -
ln2, err := net.FileListener(f)
: 取得したos.File
オブジェクトf
から、net.FileListener
関数を使って新しいネットワークリスナーln2
を再構築します。このnet.FileListener
の内部で、修正されたnewFileFD
関数が呼び出され、ファイルディスクリプタが複製されます。 -
defer f.Close()
とdefer ln2.Close()
: テストの終了時に、取得したos.File
と新しいリスナーln2
が確実に閉じられるようにdefer
文が使用されています。
このテストケースの目的は、「複製されたFDが子プロセスにリークしないことを確認する」ことです。net.FileListener
が内部でnewFileFD
を呼び出し、その際にclose-on-exec
フラグが正しく設定されることを検証しています。もし修正がなければ、このテストが実行される環境で子プロセスが起動された際に、意図しないFDリークが発生する可能性がありましたが、修正後はそれが防止されます。
関連リンク
- Go言語の
syscall
パッケージドキュメント: https://pkg.go.dev/syscall - Go言語の
net
パッケージドキュメント: https://pkg.go.dev/net dup
システムコール (man page):man 2 dup
fcntl
システムコール (man page, FD_CLOEXECについて):man 2 fcntl
参考にした情報源リンク
- Go CL 6445081 (このコミットの元の変更リスト): https://golang.org/cl/6445081
- Unix/Linuxにおけるファイルディスクリプタと
close-on-exec
に関する一般的な情報源 (例: Stack Overflow, Linux man pages) - Go言語の
syscall.ForkLock
に関する議論やドキュメント (例: Goのソースコードコメント、関連するGoのIssue)