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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージにおける、Close操作とRead操作間の競合状態(race condition)を修正するものです。具体的には、TCP、UDP、IP Raw、Unixドメインソケット接続において、CloseReadと同時に実行された際に発生しうる問題を解決します。

コミット

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, UnixConnClose()メソッドから、ファイルディスクリプタc.fdnilに設定する行を削除しています。

変更前:

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操作と競合する可能性がありました。Readc.fdにアクセスしようとした際に、それが既にnilになっていると、不正なメモリアクセスやパニックを引き起こす可能性がありました。

この競合状態は、特にReadがブロックされている間に別のゴルーチンがCloseを呼び出すようなシナリオで顕著になります。Closec.fdnilに設定してしまうと、Readc.fdを参照しようとしたときに、無効なポインタをデリファレンスしようとしてクラッシュする、あるいは予期せぬエラーを返す可能性がありました。

この修正の目的は、Closeが実行された際に、Readが安全に終了できるようにすることです。c.fdnilに設定する処理を削除することで、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インターフェースは、汎用的なネットワーク接続を表し、ReadWrite, Closeなどのメソッドを定義しています。

syscallパッケージ

syscallパッケージは、GoプログラムからOSのシステムコールを直接呼び出すための機能を提供します。このコミットでは、syscall.EINVAL(無効な引数)などのシステムコールエラーが使用されています。

errClosing

errClosingは、Goのnetパッケージ内部で使用されるエラーで、接続がクローズ中であることを示します。このエラーが呼び出し元に伝播することで、接続が安全にシャットダウンされていることを通知します。

技術的詳細

この修正の核心は、Close()メソッドからc.fd = nilという行を削除した点にあります。

従来のClose()の動作は以下のようでした:

  1. c.fd.Close()を呼び出して、基盤となるOSのファイルディスクリプタをクローズする。
  2. 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()の動作は以下のようになります:

  1. c.fd.Close()を呼び出して、基盤となるOSのファイルディスクリプタをクローズする。
  2. c.fdフィールドはnilに設定されず、以前のfdオブジェクトへのポインタを保持し続ける。

この変更により、Close()が実行された後もc.fdnilになりません。Read()c.fdにアクセスしようとした場合、c.fdは有効なオブジェクトを指していますが、そのオブジェクトがラップしているOSのファイルディスクリプタは既にクローズされています。この状態では、Read()は通常、errClosingのような適切なエラーを返すか、あるいはOSからの「ファイルディスクリプタが無効」といったエラーを受け取ります。これにより、プログラムはパニックを起こすことなく、エラーを適切に処理できるようになります。

この修正は、netパッケージの内部実装におけるfd(ファイルディスクリプタをラップする構造体)のライフサイクル管理を改善し、並行処理における堅牢性を高めるものです。c.fdnilに設定しないことで、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のファイルディスクリプタが閉じられた状態を適切に反映するようになります。

この修正により、ReadCloseと同時に実行された場合でも、Readnilポインタをデリファレンスする代わりに、クローズされたfdオブジェクトに対して操作を試みます。この際、netパッケージの内部ロジックやOSの挙動により、errClosingなどの適切なエラーが返され、プログラムのクラッシュを防ぎ、より予測可能なエラーハンドリングが可能になります。

この変更は、Goの並行処理モデルにおいて、共有リソースのライフサイクル管理をより安全に行うための典型的なアプローチを示しています。リソースがクローズされたことを示す状態を適切に伝達し、不適切なアクセスをパニックではなくエラーとして処理することで、システムの堅牢性を高めています。

関連リンク

参考にした情報源リンク