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

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

このコミットは、Go言語の標準ライブラリ os パッケージにおけるファイルディスクリプタのクローズオンエグゼック (Close-on-exec) 動作に関する修正です。特にmacOS (旧称 OS X) 環境において、O_CLOEXEC フラグの挙動がOSのバージョンによって異なる問題に対応しています。

コミット

commit 1dfe3d1f6e510e7c62cf74240a53d26131042049
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Dec 20 15:41:37 2011 -0800

    os: don't trust O_CLOEXEC on OS X
    
    OS X 10.6 doesn't do O_CLOEXEC.
    OS X 10.7 does.
    
    For now, always fall back to using syscall.CloseOnExec on darwin.
    
    This can removed when 10.6 is old news, or if we find a
    way to cheaply & reliably detect 10.6 vs 10.7 at runtime.
    
    Fixes #2587
    
    R=golang-dev, rsc, iant
    CC=golang-dev
    https://golang.org/cl/5500053

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1dfe3d1f6e510e7c62cf74240a53d26131042049

元コミット内容

このコミットの元のメッセージは以下の通りです。

os: don't trust O_CLOEXEC on OS X

OS X 10.6 doesn't do O_CLOEXEC. OS X 10.7 does.

For now, always fall back to using syscall.CloseOnExec on darwin.

This can removed when 10.6 is old news, or if we find a way to cheaply & reliably detect 10.6 vs 10.7 at runtime.

Fixes #2587

変更の背景

この変更の背景には、macOSの異なるバージョン間での O_CLOEXEC フラグの挙動の不一致があります。O_CLOEXEC は、ファイルディスクリプタをオープンする際に、そのディスクリプタが exec システムコール(新しいプログラムを実行する際に使用される)によって子プロセスに継承されないように設定するためのフラグです。

具体的には、OS X 10.6 (Snow Leopard) では O_CLOEXEC フラグが正しく機能せず、ファイルディスクリプタが意図せず子プロセスに継承されてしまう問題がありました。一方、OS X 10.7 (Lion) ではこの問題が修正され、O_CLOEXEC が期待通りに動作するようになりました。

Go言語の os パッケージは、ファイル操作を行う際にこの O_CLOEXEC フラグを利用して、セキュリティとリソース管理を最適化しようとします。しかし、OS X 10.6のような古いバージョンで O_CLOEXEC が信頼できない場合、子プロセスが予期せぬファイルディスクリプタを継承し、潜在的なセキュリティ脆弱性やリソースリークを引き起こす可能性があります。

このコミットは、このようなOSのバージョン間の差異を吸収し、macOS環境全体でファイルディスクリプタが確実にクローズオンエグゼックされるようにするための暫定的な対策として導入されました。

前提知識の解説

ファイルディスクリプタ (File Descriptor, FD)

ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プロセスはファイルディスクリプタを通じてこれらのリソースにアクセスします。

fork()exec() システムコール

Unix系システムでは、新しいプロセスを作成する際に主に二つのシステムコールが使われます。

  • fork(): 現在のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタ、その他のリソースのコピーを受け取ります。
  • exec() (例: execve()): 現在のプロセスを、指定された新しいプログラムで置き換えます。exec が成功すると、現在のプロセスのメモリ空間は新しいプログラムのコードとデータで上書きされ、実行が開始されます。この際、デフォルトでは、exec を呼び出す前のプロセスが持っていた開いているファイルディスクリプタは、新しいプログラムに継承されます。

クローズオンエグゼック (Close-on-exec, FD_CLOEXEC)

クローズオンエグゼックは、ファイルディスクリプタに設定できるフラグの一つです。このフラグが設定されたファイルディスクリプタは、exec システムコールが成功して新しいプログラムが実行される際に、自動的に閉じられます。これにより、子プロセスが親プロセスから不要なファイルディスクリプタを継承するのを防ぎ、以下のような利点があります。

  • セキュリティの向上: 機密性の高いファイルディスクリプタ(例: 認証情報を含むソケット)が意図せず子プロセスに漏洩するのを防ぎます。
  • リソース管理の簡素化: 子プロセスが不要なファイルディスクリプタを保持しないため、リソースリークを防ぎ、ファイルディスクリプタの枯渇を回避します。
  • プログラミングの容易さ: 子プロセス側で不要なファイルディスクリプタを明示的に閉じる手間が省けます。

O_CLOEXEC フラグ

O_CLOEXEC は、open()socket() などのシステムコールでファイルディスクリプタをオープンする際に指定できるフラグです。このフラグを指定すると、ファイルディスクリプタがオープンされると同時にクローズオンエグゼック属性が設定されます。これにより、ファイルディスクリプタをオープンした後に別途 fcntl(fd, F_SETFD, FD_CLOEXEC) を呼び出す必要がなくなり、アトミックな操作が可能になります。これは、ファイルディスクリプタをオープンしてから FD_CLOEXEC を設定するまでの間に fork()exec() が発生する「競合状態 (race condition)」を防ぐ上で非常に重要です。

syscall.CloseOnExec

Go言語の syscall パッケージには、特定のファイルディスクリプタに対してクローズオンエグゼック属性を設定するための CloseOnExec 関数が提供されています。これは、C言語の fcntl(fd, F_SETFD, FD_CLOEXEC) に相当する機能を提供します。

技術的詳細

このコミットは、Go言語の src/pkg/os/file_unix.go ファイル内の OpenFile 関数に修正を加えています。OpenFile 関数は、Unix系システムでファイルを開くための内部的なヘルパー関数です。

元のコードでは、syscall.O_CLOEXEC == 0 (つまり、システムが O_CLOEXEC フラグをサポートしていない場合) にのみ syscall.CloseOnExec(r) を呼び出して、明示的にクローズオンエグゼック属性を設定していました。これは、O_CLOEXEC がサポートされているシステムでは、open 時にこのフラグが使用されることを期待していたためです。

しかし、macOS環境において、OS X 10.6では O_CLOEXEC がサポートされていると報告されるにもかかわらず、実際には正しく機能しないという問題がありました。OS X 10.7ではこの問題が修正されましたが、Goランタイムが実行されているOSのバージョンを安価かつ確実に検出する方法がなかったため、GoプログラムはOS X 10.6上で O_CLOEXEC を信頼してしまい、ファイルディスクリプタが子プロセスに継承されてしまう可能性がありました。

このコミットでは、この問題を解決するために、OpenFile 関数内の条件式を以下のように変更しました。

if syscall.O_CLOEXEC == 0 || runtime.GOOS == "darwin" { // O_CLOEXEC not supported
	syscall.CloseOnExec(r)
}

この変更により、以下のいずれかの条件が満たされる場合に syscall.CloseOnExec(r) が呼び出されるようになります。

  1. syscall.O_CLOEXEC == 0: システムが O_CLOEXEC フラグをサポートしていない場合。
  2. runtime.GOOS == "darwin": 実行環境がmacOSである場合。

特に runtime.GOOS == "darwin" の条件が追加されたことで、macOS上ではOSのバージョンに関わらず、常に syscall.CloseOnExec を呼び出してファイルディスクリプタにクローズオンエグゼック属性を明示的に設定するようになりました。これにより、OS X 10.6のような O_CLOEXEC が信頼できない環境でも、ファイルディスクリプタが確実にクローズオンエグゼックされることが保証されます。

コミットメッセージにもあるように、これはOS X 10.6が「古いニュース」になるか、または10.6と10.7を安価かつ確実に実行時に検出する方法が見つかるまでの暫定的な対策です。将来的には、この追加の syscall.CloseOnExec 呼び出しは不要になる可能性があります。

また、src/pkg/os/exec/exec_test.go のテストファイルにもコメントが追加されており、この変更がビルド環境に反映された後にテストのTODOコメントを削除できることが示唆されています。これは、O_CLOEXEC の問題が解決されることで、関連するテストの失敗が解消されることを期待しているためです。

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

src/pkg/os/file_unix.go

--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -68,8 +68,13 @@ func OpenFile(name string, flag int, perm uint32) (file *File, err error) {
 	}
 
 	// There's a race here with fork/exec, which we are
-	// content to live with.  See ../syscall/exec.go
-	// if syscall.O_CLOEXEC == 0 { // O_CLOEXEC not supported
+	// content to live with.  See ../syscall/exec_unix.go.
+	// On OS X 10.6, the O_CLOEXEC flag is not respected.
+	// On OS X 10.7, the O_CLOEXEC flag works.
+	// Without a cheap & reliable way to detect 10.6 vs 10.7 at
+	// runtime, we just always call syscall.CloseOnExec on Darwin.
+	// Once >=10.7 is prevalent, this extra call can removed.
+	if syscall.O_CLOEXEC == 0 || runtime.GOOS == "darwin" { // O_CLOEXEC not supported
 	syscall.CloseOnExec(r)
 	}
 

src/pkg/os/exec/exec_test.go

--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -256,8 +256,9 @@ 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 we figure out why it fails.
+		// TODO(bradfitz): remove this block once the builders are restarted
+		// with a new binary including be47ea17bea0 (set CLOEXEC on epoll/kqueue fds)
+		// and 5500053 (don't trust O_CLOEXEC on OS X).
 		{
 			os.Stderr.Write(bs)
 			os.Exit(0)

コアとなるコードの解説

src/pkg/os/file_unix.go の変更

このファイルの OpenFile 関数は、Goプログラムがファイルを開く際に内部的に呼び出される関数です。変更の核心は、ファイルディスクリプタ r に対して syscall.CloseOnExec(r) を呼び出す条件の変更です。

  • 変更前:

    if syscall.O_CLOEXEC == 0 { // O_CLOEXEC not supported
        syscall.CloseOnExec(r)
    }
    

    この条件は、「システムが O_CLOEXEC フラグをサポートしていない場合のみ、明示的に CloseOnExec を呼び出す」ことを意味していました。これは、O_CLOEXEC がサポートされているシステムでは、open 時にこのフラグが使用され、自動的にクローズオンエグゼック属性が設定されることを前提としていました。

  • 変更後:

    if syscall.O_CLOEXEC == 0 || runtime.GOOS == "darwin" { // O_CLOEXEC not supported
        syscall.CloseOnExec(r)
    }
    

    この変更により、条件に || runtime.GOOS == "darwin" が追加されました。runtime.GOOS はGoプログラムが実行されているオペレーティングシステムを示す定数で、"darwin" はmacOSを指します。 この新しい条件は、「システムが O_CLOEXEC をサポートしていない場合」または「実行環境がmacOSである場合」に syscall.CloseOnExec(r) を呼び出すことを意味します。 これにより、macOS上ではOSのバージョン(特にOS X 10.6のように O_CLOEXEC が信頼できないバージョン)に関わらず、常に syscall.CloseOnExec が実行され、ファイルディスクリプタが確実にクローズオンエグゼックされるようになります。これは、O_CLOEXEC の挙動が不安定なmacOS環境での堅牢性を高めるための重要な変更です。

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

このファイルは os/exec パッケージのテストコードです。変更はコードの機能には影響せず、コメントの追加のみです。

  • 追加されたコメント:
    // TODO(bradfitz): remove this block once the builders are restarted
    // with a new binary including be47ea17bea0 (set CLOEXEC on epoll/kqueue fds)
    // and 5500053 (don't trust O_CLOEXEC on OS X).
    
    このコメントは、このテストブロックが一時的に無効化されている理由と、将来的に削除できる条件を示しています。具体的には、be47ea17bea0 (epoll/kqueue FDにCLOEXECを設定するコミット) と 5500053 (このコミット、OS XでO_CLOEXECを信頼しない) の両方の変更を含む新しいGoバイナリでビルド環境が再起動された後、このテストブロックを削除できることを示しています。これは、これらの変更によって関連するバグが修正され、テストが正しくパスするようになることを期待しているためです。

関連リンク

参考にした情報源リンク