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

[インデックス 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つの条件がすべて満たされたときに発生するプログラミング上のバグです。

  1. 少なくとも2つのゴルーチン(またはスレッド)が同じメモリ位置にアクセスする。
  2. それらのアクセスのうち少なくとも1つが書き込みである。
  3. それらのアクセスが同期メカニズムによって保護されていない。

データ競合が発生すると、プログラムの実行結果がアクセス順序に依存するようになり、非決定的な動作を引き起こします。これはデバッグを非常に困難にします。

ミューテックス (Mutex)

ミューテックス(Mutual Exclusionの略)は、共有リソースへのアクセスを制御するための同期プリミティブです。Go言語では sync.Mutex 型として提供されます。ミューテックスは、一度に1つのゴルーチンだけが特定のコードセクション(クリティカルセクション)を実行できるようにすることで、データ競合を防ぎます。

  • Lock(): ミューテックスをロックします。既にロックされている場合、現在のゴルーチンはロックが解放されるまでブロックされます。
  • Unlock(): ミューテックスをアンロックします。

一般的なパターンとして、共有データにアクセスする前に Lock() を呼び出し、アクセスが完了した後に Unlock() を呼び出します。Goでは defer キーワードと組み合わせて defer mu.Unlock() とすることで、関数の終了時に確実にアンロックされるようにするのが一般的です。

埋め込み構造体 (Embedded Structs)

Go言語では、ある構造体の中に別の構造体をフィールドとして宣言する際に、フィールド名を省略することができます。これを「埋め込み(embedding)」と呼びます。埋め込まれた構造体のフィールドやメソッドは、外側の構造体のフィールドやメソッドであるかのように直接アクセスできます。これは、Goにおける「継承」に似た機能を提供しますが、実際にはコンポジション(合成)の一種です。

このコミットでは、sync.Mutexerror フィールドをまとめた connErr 構造体を定義し、それを Conn 構造体に埋め込むことで、エラー状態とその保護メカニズムを論理的にカプセル化しています。

技術的詳細

このコミットの主要な技術的アプローチは、Conn 構造体の err フィールドとその関連するミューテックス errMutex を、connErr という新しい埋め込み構造体にカプセル化することです。

変更前は、Conn 構造体は errMutex sync.Mutexerr 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 構造体には、setErrorerror という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.Mutexerr 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 構造体のエラー状態管理を、データ競合が発生しないように再設計した点にあります。

  1. connErr 構造体の導入: connErr 構造体は、エラー値 (value error) と、そのエラー値へのアクセスを保護するためのミューテックス (mu sync.Mutex) をカプセル化します。これにより、エラー状態とその同期メカニズムが一体となり、論理的な単位として扱えるようになります。

  2. Conn への埋め込み: Conn 構造体に connErr を埋め込むことで、Conn のインスタンスが connErr のメソッド(setErrorerror)を直接呼び出せるようになります。これは、Goの埋め込み機能の典型的な使用例であり、コンポジションを通じてコードの再利用性と構造化を促進します。

  3. setError メソッド: このメソッドは、connErrvalue フィールドにエラーを設定する際に、必ず e.mu.Lock()defer e.mu.Unlock() を使用してミューテックスによる保護を保証します。これにより、複数のゴルーチンが同時にエラーを設定しようとしても、競合することなく安全に処理されます。また、if e.value == nil のチェックにより、最初に設定されたエラーが保持され、後続のエラーで上書きされないようになっています。

  4. error メソッド: このメソッドは、connErrvalue フィールドからエラー値を取得する際に、同様に e.mu.Lock()defer e.mu.Unlock() を使用してミューテックスによる保護を保証します。これにより、エラー値の読み取りも安全に行われ、不完全な状態や競合状態の値を読み取ることを防ぎます。

  5. 既存コードの修正: src/pkg/crypto/tls/conn.go および src/pkg/crypto/tls/handshake_client.go 内の c.err への直接アクセスはすべて、c.error() または c.setError() の呼び出しに置き換えられました。これにより、エラー状態の読み書きが常に同期されたメソッドを介して行われることが保証され、データ競合が根本的に解消されます。

この変更は、共有状態へのアクセスをカプセル化し、そのアクセスを同期メカニズムによって強制するという、並行プログラミングにおけるベストプラクティスを示しています。これにより、crypto/tls パッケージの堅牢性と信頼性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(sync パッケージ、構造体、埋め込みに関する情報)
  • Go言語におけるデータ競合と同期プリミティブに関する一般的な情報源