[インデックス 13758] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/tls
パッケージにおけるデータ競合(data race)の修正に関するものです。具体的には、Conn
構造体の err
フィールドへのアクセスが複数のゴルーチンから同時に行われることによって発生する競合状態を解消し、TLS接続の安定性と信頼性を向上させることを目的としています。
コミット
commit 67ee9a7db103b5ae5c8d077fef9e21cf6f137f3a
Author: Dave Cheney <dave@cheney.net>
Date: Thu Sep 6 17:50:26 2012 +1000
crypto/tls: fix data race on conn.err
Fixes #3862.
There were many areas where conn.err was being accessed
outside the mutex. This proposal moves the err value to
an embedded struct to make it more obvious when the error
value is being accessed.
As there are no Benchmark tests in this package I cannot
feel confident of the impact of this additional locking,
although most will be uncontended.
R=dvyukov, agl
CC=golang-dev
https://golang.org/cl/6497070
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/67ee9a7db103b5ae5c8d077fef9e21cf6f137f3a
元コミット内容
このコミットは、crypto/tls
パッケージ内の Conn
構造体における conn.err
フィールドへのデータ競合を修正します。以前は、conn.err
がミューテックス(mutex)の保護なしにアクセスされる箇所が多数存在していました。この修正では、err
値を埋め込み構造体 connErr
に移動させることで、エラー値へのアクセスがより明確に同期されるように変更されています。
作者は、このパッケージにベンチマークテストが存在しないため、追加されたロックによるパフォーマンスへの影響について確信が持てないとしていますが、ほとんどのロックは競合しないだろうと述べています。
変更の背景
Go言語のような並行処理をサポートする言語では、複数のゴルーチン(goroutine)が共有データに同時にアクセスする際に、データ競合が発生する可能性があります。データ競合は、プログラムの予測不能な動作、クラッシュ、または誤った結果を引き起こす深刻なバグの原因となります。
crypto/tls
パッケージは、TLS(Transport Layer Security)プロトコルを実装しており、ネットワーク通信において暗号化と認証を提供します。TLS接続は、複数の内部処理(ハンドシェイク、データの読み書き、エラー処理など)が並行して行われることが多く、これらの処理が共有する状態(特にエラー状態)は適切に同期される必要があります。
このコミット以前の crypto/tls
パッケージでは、Conn
構造体の err
フィールドが、その状態を保護するためのミューテックス(errMutex
)とは独立してアクセスされるケースが存在していました。これにより、異なるゴルーチンが同時に err
フィールドを読み書きしようとした際にデータ競合が発生し、TLS接続が不安定になったり、予期せぬエラーが発生したりする可能性がありました。
この問題は、GoのIssue #3862として報告されており、このコミットはその問題を解決するために作成されました。
前提知識の解説
データ競合 (Data Race)
データ競合は、以下の3つの条件がすべて満たされたときに発生するプログラミング上のバグです。
- 少なくとも2つのゴルーチン(またはスレッド)が同じメモリ位置にアクセスする。
- それらのアクセスのうち少なくとも1つが書き込みである。
- それらのアクセスが同期メカニズムによって保護されていない。
データ競合が発生すると、プログラムの実行結果がアクセス順序に依存するようになり、非決定的な動作を引き起こします。これはデバッグを非常に困難にします。
ミューテックス (Mutex)
ミューテックス(Mutual Exclusionの略)は、共有リソースへのアクセスを制御するための同期プリミティブです。Go言語では sync.Mutex
型として提供されます。ミューテックスは、一度に1つのゴルーチンだけが特定のコードセクション(クリティカルセクション)を実行できるようにすることで、データ競合を防ぎます。
Lock()
: ミューテックスをロックします。既にロックされている場合、現在のゴルーチンはロックが解放されるまでブロックされます。Unlock()
: ミューテックスをアンロックします。
一般的なパターンとして、共有データにアクセスする前に Lock()
を呼び出し、アクセスが完了した後に Unlock()
を呼び出します。Goでは defer
キーワードと組み合わせて defer mu.Unlock()
とすることで、関数の終了時に確実にアンロックされるようにするのが一般的です。
埋め込み構造体 (Embedded Structs)
Go言語では、ある構造体の中に別の構造体をフィールドとして宣言する際に、フィールド名を省略することができます。これを「埋め込み(embedding)」と呼びます。埋め込まれた構造体のフィールドやメソッドは、外側の構造体のフィールドやメソッドであるかのように直接アクセスできます。これは、Goにおける「継承」に似た機能を提供しますが、実際にはコンポジション(合成)の一種です。
このコミットでは、sync.Mutex
と error
フィールドをまとめた connErr
構造体を定義し、それを Conn
構造体に埋め込むことで、エラー状態とその保護メカニズムを論理的にカプセル化しています。
技術的詳細
このコミットの主要な技術的アプローチは、Conn
構造体の err
フィールドとその関連するミューテックス errMutex
を、connErr
という新しい埋め込み構造体にカプセル化することです。
変更前は、Conn
構造体は errMutex sync.Mutex
と err error
を直接持っていました。setError
メソッドと error
メソッドは errMutex
を使用して err
フィールドへのアクセスを保護していましたが、コードベースの他の場所で c.err
が直接参照される箇所があり、これらの直接参照は errMutex
によって保護されていませんでした。これがデータ競合の原因でした。
変更後は、以下のように定義された connErr
構造体が導入されました。
type connErr struct {
mu sync.Mutex
value error
}
そして、Conn
構造体は connErr
を埋め込みます。
type Conn struct {
// ...
connErr // first permanent error
// ...
}
connErr
構造体には、setError
と error
という2つのメソッドが定義されています。
func (e *connErr) setError(err error) error {
e.mu.Lock()
defer e.mu.Unlock()
if e.value == nil {
e.value = err
}
return err
}
func (e *connErr) error() error {
e.mu.Lock()
defer e.mu.Unlock()
return e.value
}
これらのメソッドは、それぞれ connErr
構造体自身のミューテックス e.mu
を使用して、内部の value
フィールド(以前の err
フィールドに相当)へのアクセスを保護します。
Conn
構造体に connErr
が埋め込まれたことで、Conn
のインスタンス c
から c.setError()
や c.error()
のように直接これらのメソッドを呼び出すことができるようになります。これにより、err
フィールドへのすべてのアクセスが、connErr
構造体によって提供される同期メカニズムを介して行われることが強制され、データ競合が解消されます。
この変更により、conn.err
への直接アクセスはすべて c.error()
または c.setError()
の呼び出しに置き換えられました。これにより、エラー状態の読み書きが常にミューテックスによって保護されることが保証されます。
コアとなるコードの変更箇所
src/pkg/crypto/tls/conn.go
Conn
構造体からerrMutex sync.Mutex
とerr error
フィールドが削除され、代わりにconnErr
埋め込み構造体が追加されました。--- a/src/pkg/crypto/tls/conn.go +++ b/src/pkg/crypto/tls/conn.go @@ -44,8 +44,7 @@ type Conn struct { clientProtocolFallback bool // first permanent error - errMutex sync.Mutex - err error + connErr // input/output in, out halfConn // in.Mutex < out.Mutex
connErr
構造体とそのメソッドsetError
およびerror
が新しく定義されました。--- a/src/pkg/crypto/tls/conn.go +++ b/src/pkg/crypto/tls/conn.go @@ -56,21 +55,25 @@ type Conn struct { tmp [16]byte }\n -func (c *Conn) setError(err error) error { - c.errMutex.Lock() - defer c.errMutex.Unlock() +type connErr struct { + mu sync.Mutex + value error +}\n +func (e *connErr) setError(err error) error { + e.mu.Lock() + defer e.mu.Unlock() - if c.err == nil { - c.err = err + if e.value == nil { + e.value = err } return err }\n -func (c *Conn) error() error { - c.errMutex.Lock() - defer c.errMutex.Unlock() - - return c.err +func (e *connErr) error() error { + e.mu.Lock() + defer e.mu.Unlock() + return e.value }
writeRecord
メソッド内で、エラー設定時にc.err = ...
の直接代入がc.setError(...)
の呼び出しに置き換えられました。--- a/src/pkg/crypto/tls/conn.go +++ b/src/pkg/crypto/tls/conn.go @@ -660,8 +663,7 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (n int, err) { c.tmp[0] = alertLevelError c.tmp[1] = byte(err.(alert))\n c.writeRecord(recordTypeAlert, c.tmp[0:2]) - c.err = &net.OpError{Op: "local error", Err: err} - return n, c.err + return n, c.setError(&net.OpError{Op: "local error", Err: err}) } } return
readHandshake
メソッド内で、c.err != nil
のチェックがif err := c.error(); err != nil
に置き換えられ、エラーの取得が同期的に行われるようになりました。--- a/src/pkg/crypto/tls/conn.go +++ b/src/pkg/crypto/tls/conn.go @@ -672,8 +674,8 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (n int, err) { // c.in.Mutex < L; c.out.Mutex < L. func (c *Conn) readHandshake() (interface{}, error) { for c.hand.Len() < 4 { - if c.err != nil { - return nil, c.err + if err := c.error(); err != nil { + return nil, err } if err := c.readRecord(recordTypeHandshake); err != nil { return nil, err @@ -684,11 +686,11 @@ func (c *Conn) readHandshake() (interface{}, error) { n := int(data[1])<<16 | int(data[2])<<8 | int(data[3]) if n > maxHandshake { c.sendAlert(alertInternalError) - return nil, c.err + return nil, c.error() } for c.hand.Len() < 4+n { - if c.err != nil { - return nil, c.err + if err := c.error(); err != nil { + return nil, err } if err := c.readRecord(recordTypeHandshake); err != nil { return nil, err
Write
メソッド内で、c.err
のチェックと設定がc.error()
とc.setError()
の呼び出しに置き換えられました。--- a/src/pkg/crypto/tls/conn.go +++ b/src/pkg/crypto/tls/conn.go @@ -738,12 +740,12 @@ func (c *Conn) readHandshake() (interface{}, error) { // Write writes data to the connection. func (c *Conn) Write(b []byte) (int, error) { - if c.err != nil { - return 0, c.err + if err := c.error(); err != nil { + return 0, err } - if c.err = c.Handshake(); c.err != nil { - return 0, c.err + if err := c.Handshake(); err != nil { + return 0, c.setError(err) } c.out.Lock() @@ -753,9 +755,8 @@ func (c *Conn) Write(b []byte) (int, error) { return 0, alertInternalError } - var n int - n, c.err = c.writeRecord(recordTypeApplicationData, b) - return n, c.err + n, err := c.writeRecord(recordTypeApplicationData, b) + return n, c.setError(err) }
Read
メソッド内で、c.err == nil
のチェックがc.error() == nil
に、c.err != nil
のチェックがif err := c.error(); err != nil
に置き換えられました。--- a/src/pkg/crypto/tls/conn.go +++ b/src/pkg/crypto/tls/conn.go @@ -768,14 +769,14 @@ func (c *Conn) Read(b []byte) (n int, err error) { c.in.Lock() defer c.in.Unlock() - for c.input == nil && c.err == nil { + for c.input == nil && c.error() == nil { if err := c.readRecord(recordTypeApplicationData); err != nil { // Soft error, like EAGAIN return 0, err } } - if c.err != nil { - return 0, c.err + if err := c.error(); err != nil { + return 0, err } n, err = c.input.Read(b) if c.input.off >= len(c.input.data) {
src/pkg/crypto/tls/handshake_client.go
clientHandshake
メソッド内で、c.err != nil
のチェックがif err := c.error(); err != nil
に置き換えられました。--- a/src/pkg/crypto/tls/handshake_client.go +++ b/src/pkg/crypto/tls/handshake_client.go @@ -306,8 +306,8 @@ func (c *Conn) clientHandshake() error { serverHash := suite.mac(c.vers, serverMAC) c.in.prepareCipherSpec(c.vers, serverCipher, serverHash) c.readRecord(recordTypeChangeCipherSpec) - if c.err != nil { - return c.err + if err := c.error(); err != nil { + return err } msg, err = c.readHandshake()
コアとなるコードの解説
このコミットの核心は、Conn
構造体のエラー状態管理を、データ競合が発生しないように再設計した点にあります。
-
connErr
構造体の導入:connErr
構造体は、エラー値 (value error
) と、そのエラー値へのアクセスを保護するためのミューテックス (mu sync.Mutex
) をカプセル化します。これにより、エラー状態とその同期メカニズムが一体となり、論理的な単位として扱えるようになります。 -
Conn
への埋め込み:Conn
構造体にconnErr
を埋め込むことで、Conn
のインスタンスがconnErr
のメソッド(setError
とerror
)を直接呼び出せるようになります。これは、Goの埋め込み機能の典型的な使用例であり、コンポジションを通じてコードの再利用性と構造化を促進します。 -
setError
メソッド: このメソッドは、connErr
のvalue
フィールドにエラーを設定する際に、必ずe.mu.Lock()
とdefer e.mu.Unlock()
を使用してミューテックスによる保護を保証します。これにより、複数のゴルーチンが同時にエラーを設定しようとしても、競合することなく安全に処理されます。また、if e.value == nil
のチェックにより、最初に設定されたエラーが保持され、後続のエラーで上書きされないようになっています。 -
error
メソッド: このメソッドは、connErr
のvalue
フィールドからエラー値を取得する際に、同様にe.mu.Lock()
とdefer e.mu.Unlock()
を使用してミューテックスによる保護を保証します。これにより、エラー値の読み取りも安全に行われ、不完全な状態や競合状態の値を読み取ることを防ぎます。 -
既存コードの修正:
src/pkg/crypto/tls/conn.go
およびsrc/pkg/crypto/tls/handshake_client.go
内のc.err
への直接アクセスはすべて、c.error()
またはc.setError()
の呼び出しに置き換えられました。これにより、エラー状態の読み書きが常に同期されたメソッドを介して行われることが保証され、データ競合が根本的に解消されます。
この変更は、共有状態へのアクセスをカプセル化し、そのアクセスを同期メカニズムによって強制するという、並行プログラミングにおけるベストプラクティスを示しています。これにより、crypto/tls
パッケージの堅牢性と信頼性が向上しました。
関連リンク
- Go Issue #3862: https://github.com/golang/go/issues/3862
- Go CL 6497070: https://golang.org/cl/6497070
参考にした情報源リンク
- Go言語の公式ドキュメント(
sync
パッケージ、構造体、埋め込みに関する情報) - Go言語におけるデータ競合と同期プリミティブに関する一般的な情報源