[インデックス 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
フラグを設定する機能を提供します。これにより、dup
と F_SETFD
(または FD_CLOEXEC
) を個別に呼び出すよりもアトミックかつ効率的に処理を行うことができます。
しかし、macOS 10.6 (Snow Leopard) の特定の環境において、F_DUPFD_CLOEXEC
を使用した fcntl
システムコールが、期待される EINVAL
(無効な引数) ではなく、EBADF
(不正なファイルディスクリプタ) を返してしまうという問題がありました。EINVAL
が返される場合は、Goランタイムは F_DUPFD_CLOEXEC
がサポートされていないと判断し、従来の dup
と F_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
が利用できないと判断し、代わりに dup
と F_SETFD
を個別に呼び出す従来のフォールバックパスを使用します。
しかし、macOS 10.6 の特定のバージョンでは、F_DUPFD_CLOEXEC
がサポートされていないにもかかわらず、fcntl
が EINVAL
ではなく 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
がサポートされていないと正しく判断し、安全なフォールバックパス(dup
と F_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
フラグを設定しようとします。
atomic.LoadInt32(&tryDupCloexec) == 1
の条件は、F_DUPFD_CLOEXEC
を使用する試みが有効になっているかを確認します。これは、以前の試行でF_DUPFD_CLOEXEC
が確実に失敗することが判明した場合に、このパスをスキップするための最適化です。syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_DUPFD_CLOEXEC, 0)
を呼び出し、F_DUPFD_CLOEXEC
を使用してファイルディスクリプタの複製とclose-on-exec
フラグの設定を試みます。結果はr0
(新しいFD) とe1
(エラーコード) に格納されます。- 追加されたコードブロック:
この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
)に進むことができます。
switch e1
ブロックでは、fcntl
の結果に基づいて処理を分岐します。case 0
: エラーがなく成功した場合、新しいファイルディスクリプタr0
を返します。case syscall.EINVAL
:F_DUPFD_CLOEXEC
がサポートされていない場合、従来のdup
とF_SETFD
を使用するフォールバックパスに進みます。今回の修正により、macOS 10.6でのEBADF
もこのパスで処理されるようになります。default
: その他のエラーの場合、エラーを返します。
この修正により、macOS 10.6環境下でもGoアプリケーションがファイルディスクリプタの複製と close-on-exec
フラグの設定を正しく行えるようになり、ネットワーク関連の安定性が向上しました。
関連リンク
fcntl(2)
man page: https://man7.org/linux/man-pages/man2/fcntl.2.html (Linux版ですが、概念は共通です)dup(2)
man page: https://man7.org/linux/man-pages/man2/dup.2.html- Go言語の
syscall
パッケージ: https://pkg.go.dev/syscall
参考にした情報源リンク
- 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, 技術ブログなど)EBADF
とEINVAL
エラーコードに関する情報