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

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

このコミットは、Go言語のネットワークパッケージ(net)におけるファイルディスクリプタの複製(dup)処理に関するバグ修正です。具体的には、macOS (旧称 OS X) のバージョン 10.6 において F_DUPFD_CLOEXEC フラグを使用した fcntl システムコールが期待通りに動作しない問題に対処しています。

コミット

commit b2fcdfa5fd7d0bba1933c1c6f6c478549bb4cd82
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Aug 6 07:18:06 2013 -0700

    net: detect bad F_DUPFD_CLOEXEC on OS X 10.6
    
    On 10.6, OS X's fcntl returns EBADF instead of EINVAL.
    
    R=golang-dev, iant, dave
    CC=golang-dev
    https://golang.org/cl/12493043

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

https://github.com/golang/go/commit/b2fcdfa5fd7d0bba1933c1c6f6c478549bb4cd82

元コミット内容

net: detect bad F_DUPFD_CLOEXEC on OS X 10.6

On 10.6, OS X's fcntl returns EBADF instead of EINVAL.

変更の背景

Go言語のネットワークパッケージは、ソケットなどのファイルディスクリプタを扱う際に、子プロセスに継承させないように close-on-exec フラグを設定することが一般的です。これは、ファイルディスクリプタのリークを防ぎ、セキュリティを向上させるための重要なプラクティスです。

F_DUPFD_CLOEXEC は、fcntl システムコールで使用されるフラグの一つで、ファイルディスクリプタを複製(dup)すると同時に、その複製されたディスクリプタに close-on-exec フラグを設定する機能を提供します。これにより、dupF_SETFD (または FD_CLOEXEC) を個別に呼び出すよりもアトミックかつ効率的に処理を行うことができます。

しかし、macOS 10.6 (Snow Leopard) の特定の環境において、F_DUPFD_CLOEXEC を使用した fcntl システムコールが、期待される EINVAL (無効な引数) ではなく、EBADF (不正なファイルディスクリプタ) を返してしまうという問題がありました。EINVAL が返される場合は、Goランタイムは F_DUPFD_CLOEXEC がサポートされていないと判断し、従来の dupF_SETFD を組み合わせた方法にフォールバックします。しかし、EBADF が返されると、Goランタイムはファイルディスクリプタ自体が不正であると誤解し、エラーとして処理してしまい、結果としてネットワーク接続の確立などに失敗する可能性がありました。

このコミットは、このmacOS 10.6特有の挙動を検出し、EBADF が返された場合でも EINVAL として扱うことで、Goランタイムが正しくフォールバック処理を行えるようにするための修正です。

前提知識の解説

  • ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルが割り当てる非負の整数値です。プログラムはファイルディスクリプタを通じてこれらのリソースにアクセスします。
  • fcntl システムコール: ファイルディスクリプタのプロパティ(属性)を操作するためのシステムコールです。ファイルディスクリプタの複製、ステータスフラグの取得・設定、ロックの管理など、多岐にわたる機能を提供します。
  • F_DUPFD_CLOEXEC: fcntl システムコールで使用されるコマンドの一つです。既存のファイルディスクリプタを複製し、かつその複製されたファイルディスクリプタに FD_CLOEXEC フラグ(close-on-exec フラグ)を設定します。
  • FD_CLOEXEC (Close-on-exec): ファイルディスクリプタのフラグの一つです。このフラグが設定されているファイルディスクリプタは、exec ファミリーのシステムコール(例: execve, execl など)によって新しいプログラムが実行される際に、自動的に閉じられます。これにより、子プロセスに不要なファイルディスクリプタが継承されるのを防ぎ、リソースリークやセキュリティ上の問題を回避できます。
  • dup システムコール: 既存のファイルディスクリプタを複製するシステムコールです。複製されたファイルディスクリプタは、元のファイルディスクリプタと同じファイル記述エントリ(ファイルオフセット、ファイルステータスフラグなど)を参照します。
  • syscall.EBADF: Unix系OSのシステムコールが返すエラーコードの一つで、「Bad file descriptor」(不正なファイルディスクリプタ)を意味します。通常、存在しない、または無効なファイルディスクリプタに対して操作を行おうとした場合に返されます。
  • syscall.EINVAL: Unix系OSのシステムコールが返すエラーコードの一つで、「Invalid argument」(無効な引数)を意味します。システムコールに渡された引数が不正である場合に返されます。
  • macOS 10.6 (Snow Leopard): 2009年にリリースされたAppleのmacOSのバージョンです。このコミットが作成された2013年時点では、まだ一定のユーザーベースを持っていました。

技術的詳細

Goランタイムは、ファイルディスクリプタを複製する際に、まず F_DUPFD_CLOEXEC を使用してアトミックに close-on-exec フラグを設定しようと試みます。これは、syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_DUPFD_CLOEXEC, 0) の呼び出しによって行われます。

このシステムコールがエラーを返した場合、Goランタイムはそのエラーコードを評価します。通常、F_DUPFD_CLOEXEC がOSによってサポートされていない場合、EINVAL エラーが返されることが期待されます。EINVAL が返された場合、Goランタイムは F_DUPFD_CLOEXEC が利用できないと判断し、代わりに dupF_SETFD を個別に呼び出す従来のフォールバックパスを使用します。

しかし、macOS 10.6 の特定のバージョンでは、F_DUPFD_CLOEXEC がサポートされていないにもかかわらず、fcntlEINVAL ではなく EBADF を返していました。コミットメッセージによると、これは「undocumented」(文書化されていない)挙動であり、fcntl が内部的に ioctl のような動作にフォールバックし、ファイルディスクリプタが期待されるデバイスFDタイプではないために EBADF を返していた可能性が示唆されています。

この EBADF エラーは、Goランタイムにとってはファイルディスクリプタ自体が不正であるという深刻な問題として解釈されてしまいます。そのため、Goランタイムはフォールバックパスに進むことなく、エラーとして処理を中断してしまいます。

このコミットの修正は、このmacOS 10.6特有の挙動を検出し、runtime.GOOS == "darwin" (OSがmacOSであること) かつ e1 == syscall.EBADF (エラーが EBADF であること) の場合に、エラーコードを syscall.EINVAL に上書きするというものです。これにより、Goランタイムは F_DUPFD_CLOEXEC がサポートされていないと正しく判断し、安全なフォールバックパス(dupF_SETFD を使用する)を実行できるようになります。

コメントには「TODO: only do this on 10.6 if we can detect 10.6 cheaply.」とあり、将来的にはOSのバージョンをより厳密にチェックしてこの修正を適用する可能性が示唆されていますが、このコミット時点ではmacOSであれば一律にこの変換を行うようになっています。これは、macOS 10.6以降のバージョンではこの問題が発生しないため、影響は限定的であると判断されたためと考えられます。

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

変更は src/pkg/net/fd_unix.go ファイルの dupCloseOnExec 関数内で行われています。

--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -413,6 +413,19 @@ var tryDupCloexec = int32(1)
 func dupCloseOnExec(fd int) (newfd int, err error) {
  	if atomic.LoadInt32(&tryDupCloexec) == 1 {
  		r0, _, e1 := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_DUPFD_CLOEXEC, 0)
+		if runtime.GOOS == "darwin" && e1 == syscall.EBADF {
+			// On OS X 10.6 and below (but we only support
+			// >= 10.6), F_DUPFD_CLOEXEC is unsupported
+			// and fcntl there falls back (undocumented)
+			// to doing an ioctl instead, returning EBADF
+			// in this case because fd is not of the
+			// expected device fd type.  Treat it as
+			// EINVAL instead, so we fall back to the
+			// normal dup path.
+			// TODO: only do this on 10.6 if we can detect 10.6
+			// cheaply.
+			e1 = syscall.EINVAL
+		}
  		switch e1 {
  		case 0:
  			return int(r0), nil

コアとなるコードの解説

dupCloseOnExec 関数は、与えられたファイルディスクリプタ fd を複製し、その複製に close-on-exec フラグを設定しようとします。

  1. atomic.LoadInt32(&tryDupCloexec) == 1 の条件は、F_DUPFD_CLOEXEC を使用する試みが有効になっているかを確認します。これは、以前の試行で F_DUPFD_CLOEXEC が確実に失敗することが判明した場合に、このパスをスキップするための最適化です。
  2. syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_DUPFD_CLOEXEC, 0) を呼び出し、F_DUPFD_CLOEXEC を使用してファイルディスクリプタの複製と close-on-exec フラグの設定を試みます。結果は r0 (新しいFD) と e1 (エラーコード) に格納されます。
  3. 追加されたコードブロック:
    		if runtime.GOOS == "darwin" && e1 == syscall.EBADF {
    			// ... コメント ...
    			e1 = syscall.EINVAL
    		}
    
    この if 文が今回のコミットの核心です。
    • runtime.GOOS == "darwin": 現在のOSがmacOSであるかをチェックします。
    • e1 == syscall.EBADF: fcntl システムコールが EBADF エラーを返したかをチェックします。
    • 両方の条件が真の場合、つまりmacOS上で F_DUPFD_CLOEXEC の呼び出しが EBADF を返した場合、e1 の値を syscall.EINVAL に上書きします。
    • この上書きにより、次の switch e1 ブロックでは、EBADF ではなく EINVAL として処理され、Goランタイムは F_DUPFD_CLOEXEC がサポートされていないと判断し、フォールバックロジック(case syscall.EINVAL)に進むことができます。
  4. switch e1 ブロックでは、fcntl の結果に基づいて処理を分岐します。
    • case 0: エラーがなく成功した場合、新しいファイルディスクリプタ r0 を返します。
    • case syscall.EINVAL: F_DUPFD_CLOEXEC がサポートされていない場合、従来の dupF_SETFD を使用するフォールバックパスに進みます。今回の修正により、macOS 10.6での EBADF もこのパスで処理されるようになります。
    • default: その他のエラーの場合、エラーを返します。

この修正により、macOS 10.6環境下でもGoアプリケーションがファイルディスクリプタの複製と close-on-exec フラグの設定を正しく行えるようになり、ネットワーク関連の安定性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴: https://github.com/golang/go/commits/master
  • Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている https://golang.org/cl/12493043 は、Gerritの変更リストへのリンクです)
  • Unix/Linuxプログラミング関連のドキュメント (例: man pages)
  • macOSのシステムコールに関する情報 (Apple Developer Documentationなど)
  • Go言語のソースコード (src/pkg/net/fd_unix.go)
  • F_DUPFD_CLOEXEC の挙動に関する一般的な情報 (Stack Overflow, 技術ブログなど)
  • EBADFEINVAL エラーコードに関する情報