[インデックス 12927] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージにおける、Close
操作とRead
操作間の競合状態(race condition)を修正するものです。具体的には、TCP、UDP、IP Raw、Unixドメインソケット接続において、Close
がRead
と同時に実行された際に発生しうる問題を解決します。
コミット
commit 1f14d45e7dc17d397e437e3bd9b507e5316e6ed6
Author: Dave Cheney <dave@cheney.net>
Date: Sat Apr 21 10:01:32 2012 +1000
net: fix race between Close and Read
Fixes #3507.
Applied the suggested fix from rsc. If the connection
is in closing state then errClosing will bubble up to
the caller.
The fix has been applied to udp, ip and unix as well as
their code path include nil'ing c.fd on close. Func
tests are available in the linked issue that verified
the bug existed there as well.
R=rsc, fullung, alex.brainman, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/6002053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1f14d45e7dc17d397e437e3bd9b507e5316e6ed6
元コミット内容
このコミットは、net
パッケージ内のIPConn
, TCPConn
, UDPConn
, UnixConn
のClose()
メソッドから、ファイルディスクリプタc.fd
をnil
に設定する行を削除しています。
変更前:
func (c *IPConn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
err := c.fd.Close()
c.fd = nil // この行が削除される
return err
}
変更後:
func (c *IPConn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
return c.fd.Close() // c.fd = nil が削除され、直接 Close() の結果を返す
}
同様の変更がtcpsock_posix.go
, udpsock_posix.go
, unixsock_posix.go
にも適用されています。
変更の背景
このコミットは、Go言語のnet
パッケージにおいて、ネットワーク接続のClose
操作とRead
操作が同時に実行された際に発生する競合状態(race condition)を修正するために行われました。具体的には、Issue #3507として報告された問題に対応しています。
従来のClose
実装では、ファイルディスクリプタc.fd
をクローズした直後にnil
に設定していました。このnil
化の処理が、まだc.fd
を使用しようとしているRead
操作と競合する可能性がありました。Read
がc.fd
にアクセスしようとした際に、それが既にnil
になっていると、不正なメモリアクセスやパニックを引き起こす可能性がありました。
この競合状態は、特にRead
がブロックされている間に別のゴルーチンがClose
を呼び出すようなシナリオで顕著になります。Close
がc.fd
をnil
に設定してしまうと、Read
がc.fd
を参照しようとしたときに、無効なポインタをデリファレンスしようとしてクラッシュする、あるいは予期せぬエラーを返す可能性がありました。
この修正の目的は、Close
が実行された際に、Read
が安全に終了できるようにすることです。c.fd
をnil
に設定する処理を削除することで、Close
が完了した後もc.fd
オブジェクト自体は有効な状態を保ち、Read
がそのオブジェクトに対して操作を試みても、適切にクローズされたことを示すエラー(例: errClosing
)が返されるようになります。これにより、システムがクラッシュすることなく、より堅牢なエラーハンドリングが可能になります。
前提知識の解説
競合状態 (Race Condition)
競合状態とは、複数のプロセスやスレッド(Goにおいてはゴルーチン)が共有リソースにアクセスする際に、そのアクセス順序によって結果が変わってしまう状態を指します。意図しない順序でアクセスが行われると、プログラムの動作が不安定になったり、クラッシュしたりする原因となります。
このコミットの文脈では、Close
ゴルーチンとRead
ゴルーチンがc.fd
という共有リソースにアクセスする際に競合状態が発生していました。
ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系OSにおいてファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。プログラムがファイルやネットワーク接続を操作する際には、このファイルディスクリプタを通じて行われます。
net
パッケージ
Go言語の標準ライブラリnet
パッケージは、ネットワークI/Oのプリミティブを提供します。TCP/IP、UDP、Unixドメインソケットなどのネットワークプロトコルを扱うための型や関数が含まれています。net.Conn
インターフェースは、汎用的なネットワーク接続を表し、Read
やWrite
, Close
などのメソッドを定義しています。
syscall
パッケージ
syscall
パッケージは、GoプログラムからOSのシステムコールを直接呼び出すための機能を提供します。このコミットでは、syscall.EINVAL
(無効な引数)などのシステムコールエラーが使用されています。
errClosing
errClosing
は、Goのnet
パッケージ内部で使用されるエラーで、接続がクローズ中であることを示します。このエラーが呼び出し元に伝播することで、接続が安全にシャットダウンされていることを通知します。
技術的詳細
この修正の核心は、Close()
メソッドからc.fd = nil
という行を削除した点にあります。
従来のClose()
の動作は以下のようでした:
c.fd.Close()
を呼び出して、基盤となるOSのファイルディスクリプタをクローズする。c.fd = nil
として、GoのConn
オブジェクト内のfd
フィールドをnil
に設定する。
この2番目のステップが問題を引き起こしていました。c.fd.Close()
が呼び出された後、OSレベルではファイルディスクリプタはクローズされますが、GoのConn
オブジェクト内のc.fd
フィールド自体はまだ有効なポインタを保持しています。しかし、c.fd = nil
とすることで、このポインタがnil
に上書きされてしまいます。
ここで競合状態が発生します。もし別のゴルーチンがRead()
を呼び出し、そのRead()
がc.fd
にアクセスしようとしたタイミングで、Close()
がc.fd = nil
を実行してしまった場合、Read()
はnil
ポインタをデリファレンスしようとします。これはGoのランタイムパニック(panic: runtime error: invalid memory address or nil pointer dereference
)を引き起こす可能性があり、プログラムがクラッシュする原因となります。
修正後のClose()
の動作は以下のようになります:
c.fd.Close()
を呼び出して、基盤となるOSのファイルディスクリプタをクローズする。c.fd
フィールドはnil
に設定されず、以前のfd
オブジェクトへのポインタを保持し続ける。
この変更により、Close()
が実行された後もc.fd
はnil
になりません。Read()
がc.fd
にアクセスしようとした場合、c.fd
は有効なオブジェクトを指していますが、そのオブジェクトがラップしているOSのファイルディスクリプタは既にクローズされています。この状態では、Read()
は通常、errClosing
のような適切なエラーを返すか、あるいはOSからの「ファイルディスクリプタが無効」といったエラーを受け取ります。これにより、プログラムはパニックを起こすことなく、エラーを適切に処理できるようになります。
この修正は、net
パッケージの内部実装におけるfd
(ファイルディスクリプタをラップする構造体)のライフサイクル管理を改善し、並行処理における堅牢性を高めるものです。c.fd
をnil
に設定しないことで、fd
オブジェクトが適切にガベージコレクションされるまで、その状態を維持し、他の操作が安全にエラーを検出できるようにします。
コアとなるコードの変更箇所
以下の4つのファイルで同様の変更が行われています。
src/pkg/net/iprawsock_posix.go
src/pkg/net/tcpsock_posix.go
src/pkg/net/udpsock_posix.go
src/pkg/net/unixsock_posix.go
各ファイルのClose()
メソッドにおいて、c.fd = nil
の行が削除され、c.fd.Close()
の戻り値が直接返されるようになっています。
例: src/pkg/net/iprawsock_posix.go
--- a/src/pkg/net/iprawsock_posix.go
+++ b/src/pkg/net/iprawsock_posix.go
@@ -83,9 +83,7 @@ func (c *IPConn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
- err := c.fd.Close()
- c.fd = nil
- return err
+ return c.fd.Close()
}
// LocalAddr returns the local network address.
コアとなるコードの解説
変更されたClose()
メソッドは、ネットワーク接続を閉じる役割を担います。
変更前は、c.fd.Close()
を呼び出して基盤となるOSのファイルディスクリプタを閉じ、その後にc.fd = nil
としてGoのConn
オブジェクト内のfd
フィールドをnil
に設定していました。このnil
化が、Read
操作との競合を引き起こす原因でした。
変更後は、c.fd = nil
の行が削除されています。これにより、c.fd.Close()
が呼び出された後も、c.fd
フィールドはnil
にならず、fd
オブジェクトへのポインタを保持し続けます。fd
オブジェクト自体は、OSのファイルディスクリプタが閉じられた状態を適切に反映するようになります。
この修正により、Read
がClose
と同時に実行された場合でも、Read
はnil
ポインタをデリファレンスする代わりに、クローズされたfd
オブジェクトに対して操作を試みます。この際、net
パッケージの内部ロジックやOSの挙動により、errClosing
などの適切なエラーが返され、プログラムのクラッシュを防ぎ、より予測可能なエラーハンドリングが可能になります。
この変更は、Goの並行処理モデルにおいて、共有リソースのライフサイクル管理をより安全に行うための典型的なアプローチを示しています。リソースがクローズされたことを示す状態を適切に伝達し、不適切なアクセスをパニックではなくエラーとして処理することで、システムの堅牢性を高めています。
関連リンク
- Go言語の
net
パッケージに関する公式ドキュメント: https://pkg.go.dev/net - Go言語の並行処理に関する公式ドキュメント(Go Concurrency Patternsなど): https://go.dev/doc/effective_go#concurrency
参考にした情報源リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/1f14d45e7dc17d397e437e3bd9b507e5316e6ed6
- Go Change-ID 6002053: https://golang.org/cl/6002053 (このリンクは古いGoのコードレビューシステムへのリンクであり、現在はGoのGerritシステムにリダイレクトされる可能性があります。当時の議論の詳細が確認できる場合があります。)
- Go言語における
net.Conn
のRead
とClose
間の競合状態に関する一般的な情報源(Stack Overflow, Goブログなど)