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

[インデックス 13748] ファイルの概要

このコミットは、Go言語の標準ライブラリnetパッケージにおいて、FileConnFileListenerFilePacketConnといったファイルディスクリプタ(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パッケージのFileConnFileListenerFilePacketConnといった関数は、既存のファイルディスクリプタからネットワーク接続やリスナーを再構築するために使用されます。これらの関数内部でnewFileFDが呼び出され、そこでsyscall.Dupを使ってFDが複製されます。

問題は、syscall.DupがFDを複製する際に、元のFDに設定されていたclose-on-execフラグ(FD_CLOEXEC)が新しいFDに引き継がれないことにありました。close-on-execフラグは、execシステムコール(新しいプログラムを実行する際に呼び出される)が実行されるときに、そのFDを自動的に閉じるように指示するものです。このフラグが設定されていないFDは、子プロセスが新しいプログラムを実行しても閉じられず、子プロセスに継承されてしまいます。

これにより、以下のような問題が発生する可能性がありました。

  1. リソースリーク: 不要なFDが子プロセスに継承され続けることで、システム全体で利用可能なFDの数が枯渇する可能性があります。
  2. セキュリティリスク: 親プロセスが持つ機密性の高いFD(例えば、認証情報を含むファイルやソケット)が、意図しない子プロセスに公開されてしまうリスクがあります。
  3. 予期せぬ動作: 子プロセスが継承した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. forkexecシステムコール

  • 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が複製されていました。

  1. syscall.Dup(int(f.Fd()))を呼び出して、元のos.FileのFDを複製し、新しいFD(fd)を取得します。
  2. この新しいfdを使用して、ソケットタイプなどの情報を取得し、netFD構造体を初期化します。

この問題は、syscall.DupがFDを複製する際に、元のFDに設定されていたclose-on-execフラグを新しいFDに引き継がないという、Unix系OSの一般的な挙動に起因します。つまり、元のFDがclose-on-execに設定されていても、複製されたFDはデフォルトでclose-on-execが設定されていない状態になります。

修正後のコードでは、この問題に対処するために以下の変更が加えられました。

  1. syscall.ForkLock.RLock()の追加: syscall.Dupを呼び出す前に、syscall.ForkLock.RLock()が呼び出されます。これは、Goランタイムがforkシステムコールを安全に実行するための読み取りロックを取得します。これにより、dup操作中にGoランタイムの内部状態が不整合になることを防ぎ、特にfork/execのシーケンスにおいて競合状態が発生するのを抑制します。dupforkと密接に関連する操作であり、forkの安全性を確保するためのロックと連携させることで、より堅牢な動作が期待されます。

  2. syscall.CloseOnExec(fd)の追加: syscall.Dupによって新しいFD(fd)が正常に作成された直後に、syscall.CloseOnExec(fd)が呼び出されます。この関数は、複製されたFDに対して明示的にclose-on-execフラグを設定します。これにより、このFDが子プロセスに継承されたとしても、子プロセスがexecシステムコールを実行する際には自動的に閉じられるようになります。

  3. syscall.ForkLock.RUnlock()の追加: syscall.Dupの呼び出し後、およびエラーハンドリングのパスの両方で、syscall.ForkLock.RUnlock()が呼び出され、取得した読み取りロックが解放されます。

この修正により、net.FileConnnet.FileListenernet.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)を作成する際に呼び出されます。

  1. syscall.ForkLock.RLock(): syscall.Dupを呼び出す前に、GoランタイムのForkLockを読み取りモードでロックします。これは、forkシステムコールが実行される際にGoランタイムの内部状態が安全であることを保証するためのものです。dup操作はforkと関連するコンテキストでよく使用されるため、このロックは競合状態を防ぎ、堅牢性を高めます。

  2. syscall.Dup(int(f.Fd())): 元のos.Fileが持つファイルディスクリプタ(f.Fd())を複製します。これにより、新しいファイルディスクリプタfdが作成されます。

  3. エラーハンドリング内のsyscall.ForkLock.RUnlock(): syscall.Dupがエラーを返した場合、取得したロックを解放してからエラーを返します。

  4. syscall.CloseOnExec(fd): これがこのコミットの核心的な修正です。syscall.Dupによって複製された新しいファイルディスクリプタfdに対して、明示的にclose-on-execフラグを設定します。これにより、このfdが子プロセスに継承されたとしても、子プロセスがexecシステムコールを実行する際には自動的に閉じられるようになります。これにより、FDリークが防止されます。

  5. syscall.ForkLock.RUnlock(): syscall.CloseOnExecの呼び出し後、取得した読み取りロックを解放します。

src/pkg/os/exec/exec_test.goの変更

このファイルには、TestExtraFilesという既存のテスト関数に新しいテストケースが追加されました。

  1. f, err := ln.(*net.TCPListener).File(): 既存のTCPリスナーlnから、その基盤となるos.Fileオブジェクトを取得します。このos.Fileは、ネットワークソケットに対応するファイルディスクリプタをカプセル化しています。

  2. ln2, err := net.FileListener(f): 取得したos.Fileオブジェクトfから、net.FileListener関数を使って新しいネットワークリスナーln2を再構築します。このnet.FileListenerの内部で、修正されたnewFileFD関数が呼び出され、ファイルディスクリプタが複製されます。

  3. 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)