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

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

このコミットは、Go言語の crypto/tls パッケージにおけるTLSコネクションのエラーハンドリングメカニズムを改善するものです。具体的には、読み取り/書き込み操作における競合状態(read/write races)を回避し、エラー発生時に読み取り方向からの追加情報が失われることを防ぐために、単一のコネクションエラー (connErr) を、読み取り、書き込み、およびハンドシェイクそれぞれに特化したエラー (in.err, out.err, handshakeErr) に分割しています。

コミット

commit 3656c2db9639c524fe492ce74f485a35e0794cf4
Author: Adam Langley <agl@golang.org>
Date:   Mon Mar 3 09:01:44 2014 -0500

    crypto/tls: split connErr to avoid read/write races.
    
    Currently a write error will cause future reads to return that same error.
    However, there may have been extra information from a peer pending on
    the read direction that is now unavailable.
    
    This change splits the single connErr into errors for the read, write and
    handshake. (Splitting off the handshake error is needed because both read
    and write paths check the handshake error.)
    
    Fixes #7414.
    
    LGTM=bradfitz, r
    R=golang-codereviews, r, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/69090044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3656c2db9639c524fe492ce74f485a35e0794cf4

元コミット内容

crypto/tls: 読み書きの競合を避けるために connErr を分割。

現在、書き込みエラーが発生すると、それ以降の読み込みも同じエラーを返すようになっている。しかし、ピアから読み込み方向に保留されている追加情報があった場合、それが利用できなくなる可能性がある。

この変更は、単一の connErr を、読み込み、書き込み、およびハンドシェイクのエラーに分割する。(ハンドシェイクエラーを分離する必要があるのは、読み込みパスと書き込みパスの両方がハンドシェイクエラーをチェックするためである。)

Fixes #7414.

変更の背景

このコミットが行われた背景には、Go言語の crypto/tls パッケージにおけるTLSコネクションのエラーハンドリングの設計上の問題がありました。以前のバージョンでは、Conn 構造体内に connErr という単一のエラーフィールドが存在し、これはコネクション全体で発生した最初のエラーを保持していました。

この設計の主な問題点は、読み書きの競合状態(read/write races)と、それに伴う情報損失でした。具体的には、以下のようなシナリオが考えられます。

  1. 書き込みエラーが読み取りに影響: TLSコネクションでデータ送信中にエラー(例: ネットワーク切断)が発生し、connErr にそのエラーが設定されたとします。この後、アプリケーションがコネクションからのデータ読み取りを試みると、たとえ読み取りバッファにまだピアからの有効なデータが残っていたとしても、connErr に設定された書き込みエラーがすぐに返されてしまい、その有効なデータが処理される機会が失われていました。
  2. ピアからの追加情報の損失: TLSプロトコルでは、エラー発生時にもピアから alert メッセージなどの追加情報が送信されることがあります。例えば、ピアがコネクションを正常に終了するために close_notify アラートを送信した場合、このアラートは読み取り方向で受信されます。しかし、もし書き込みエラーによって connErr が設定されてしまうと、この close_notify アラートが処理される前に読み取り操作がエラーを返してしまい、結果としてコネクションの正常なシャットダウンが妨げられたり、デバッグに必要な情報が失われたりする可能性がありました。

このような問題は、特に長期にわたるコネクションや、エラー発生時の堅牢な処理が求められるTLS通信において、予期せぬ動作やデッドロック、リソースリークの原因となる可能性がありました。このコミットは、これらの問題を解決し、TLSコネクションのエラーハンドリングをより正確で堅牢なものにすることを目的としています。

前提知識の解説

このコミットの理解を深めるために、以下の前提知識を解説します。

1. TLS (Transport Layer Security)

TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。WebブラウザとWebサーバー間のHTTPS通信などで広く利用されています。TLSコネクションは、ハンドシェイクフェーズ、レコードプロトコルフェーズ、アラートプロトコルフェーズなど、複数の層とフェーズで構成されます。

  • ハンドシェイクフェーズ: クライアントとサーバーが互いを認証し、暗号化アルゴリズムや鍵をネゴシエートする初期段階です。
  • レコードプロトコルフェーズ: ハンドシェイクが完了した後、アプリケーションデータを暗号化して送受信する段階です。
  • アラートプロトコルフェーズ: TLSコネクション内で発生したエラーや警告を通知するためのプロトコルです。例えば、close_notify はコネクションの正常な終了を通知します。

2. Go言語の crypto/tls パッケージ

crypto/tls パッケージは、Go言語でTLSクライアントおよびサーバーを実装するための標準ライブラリです。このパッケージは、TLSプロトコルの詳細を抽象化し、開発者が安全なネットワーク通信を容易に構築できるようにします。

  • tls.Conn 構造体: TLSコネクションを表す主要な構造体です。基盤となる net.Conn をラップし、TLSプロトコル層の処理(ハンドシェイク、暗号化/復号化、レコード処理など)を担当します。
  • halfConn 構造体: tls.Conn の内部で使用される構造体で、TLSコネクションの読み取り(in)または書き込み(out)の片方向の状態を管理します。これには、暗号化/MACの状態、シーケンス番号、バッファなどが含まれます。

3. 競合状態 (Race Condition)

競合状態とは、複数の並行プロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。ネットワークプログラミング、特に読み書きが同時に行われるようなシナリオでは、競合状態が発生しやすく、予期せぬバグやデータ破損の原因となることがあります。

このコミットの文脈では、単一の connErr フィールドが読み取りと書き込みの両方の操作によって共有されていたため、一方の操作でエラーが発生すると、もう一方の操作がそのエラーに影響を受け、本来処理されるべきデータが失われるという競合状態が発生していました。

4. エラーハンドリングの重要性

堅牢なシステムを構築する上で、適切なエラーハンドリングは不可欠です。特にネットワーク通信においては、一時的なネットワークの問題、ピアの切断、プロトコル違反など、様々なエラーが発生する可能性があります。エラーを適切に処理しないと、リソースリーク、デッドロック、アプリケーションのクラッシュ、またはセキュリティ上の脆弱性につながる可能性があります。

このコミットは、TLSコネクションのエラーハンドリングをより粒度高く、正確にすることで、これらの潜在的な問題を軽減しようとしています。読み取り、書き込み、ハンドシェイクのエラーを分離することで、各操作が独立してエラー状態を管理できるようになり、一方のエラーが他方の操作に不必要に影響を与えることを防ぎます。

技術的詳細

このコミットの核心は、tls.Conn 構造体におけるエラー管理の粒度を向上させることにあります。以前は、コネクション全体で単一の connErr フィールドが使用されていましたが、これは読み取り、書き込み、ハンドシェイクの各操作で発生するエラーを区別せず、最初のエラーが設定されるとそれ以降の操作に影響を与えていました。

この変更により、エラー管理は以下のように分割されました。

  1. handshakeErr error: tls.Conn 構造体に新しく追加されたフィールドで、TLSハンドシェイク中に発生したエラーを保持します。ハンドシェイクはコネクションの初期設定フェーズであり、読み取りと書き込みの両方のパスがこのエラーをチェックする必要があるため、独立したエラーフィールドとして分離されました。
  2. halfConn.err error: halfConn 構造体(tls.Conninout フィールドの型)に新しく追加されたフィールドです。これにより、読み取り方向 (c.in.err) と書き込み方向 (c.out.err) のエラーがそれぞれ独立して管理されるようになります。

この分割によって、以下のような技術的な改善が実現されました。

  • エラーの独立性: 読み取り操作中に発生したエラーが書き込み操作に影響を与えたり、その逆が発生したりすることがなくなりました。これにより、例えば書き込みエラーが発生しても、読み取りバッファに残っているデータを最後まで処理できるようになります。
  • 情報損失の防止: ピアから送信される alert メッセージ(特に close_notify)のような重要な情報が、別の方向で発生したエラーによって失われることを防ぎます。これにより、コネクションの正常なシャットダウンや、エラー発生時のより正確な診断が可能になります。
  • ハンドシェイクエラーの明確化: ハンドシェイクエラーが独立したフィールドになったことで、ハンドシェイクが完了する前に発生したエラーが、その後の読み書き操作にどのように影響するかをより明確に制御できるようになりました。

具体的なコード変更としては、以下の点が挙げられます。

  • tls.Conn から connErr 構造体が削除され、代わりに handshakeErr フィールドが追加されました。
  • halfConn 構造体に err フィールドが追加され、setErrorLockederror メソッドが定義されました。これらのメソッドは、halfConn のミューテックス (sync.Mutex) を使用して、エラーフィールドへの安全なアクセスを保証します。
  • tls.ConnreadRecord, Write, Read, Handshake メソッドなど、エラーをセットまたはチェックする箇所が、新しい c.in.setErrorLocked, c.out.setErrorLocked, c.in.err, c.out.err, c.handshakeErr を使用するように変更されました。

これらの変更により、crypto/tls パッケージは、TLSコネクションのエラーハンドリングにおいて、より堅牢で予測可能な動作を提供するようになりました。

コアとなるコードの変更箇所

このコミットでは、主に src/pkg/crypto/tls/conn.go ファイルが大幅に変更され、src/pkg/crypto/tls/handshake_client.gosrc/pkg/crypto/tls/handshake_server.go にも小さな変更が加えられています。

変更ファイル一覧:

  • src/pkg/crypto/tls/conn.go: 127行変更 (61挿入, 70削除)
  • src/pkg/crypto/tls/handshake_client.go: 2行変更 (1挿入, 1削除)
  • src/pkg/crypto/tls/handshake_server.go: 2行変更 (1挿入, 1削除)

コアとなるコードの解説

src/pkg/crypto/tls/conn.go の変更

  1. Conn 構造体からの connErr の削除と handshakeErr の追加:

    • 以前存在した connErr 構造体とその関連メソッド (setError, error) が完全に削除されました。
    • Conn 構造体に handshakeErr error フィールドが追加されました。これはハンドシェイク中に発生したエラーを保持します。
    --- a/src/pkg/crypto/tls/conn.go
    +++ b/src/pkg/crypto/tls/conn.go
    @@ -28,6 +28,7 @@ type Conn struct {
     
     	// constant after handshake; protected by handshakeMutex
     	handshakeMutex    sync.Mutex // handshakeMutex < in.Mutex, out.Mutex, errMutex
    +	handshakeErr      error      // error resulting from handshake
     	vers              uint16     // TLS version
     	haveVers          bool       // version has been negotiated
     	config            *Config    // configuration passed to constructor
    @@ -45,9 +46,6 @@ type Conn struct {
     	clientProtocol         string
     	clientProtocolFallback bool
     
    -	// first permanent error
    -	connErr
    -
     	// input/output
     	in, out  halfConn     // in.Mutex < out.Mutex
     	rawInput *block       // raw input, right off the wire
    @@ -57,27 +55,6 @@ type Conn struct {
     	tmp [16]byte
     }
     
    -type connErr struct {
    -	mu    sync.Mutex
    -	value error
    -}
    -
    -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
    -}
    -
     // Access to net.Conn methods.
     // Cannot just embed net.Conn because that would
     // export the struct field too.
    
  2. halfConn 構造体への err フィールドと関連メソッドの追加:

    • halfConn 構造体に err error フィールドが追加されました。これは、その halfConn (読み取りまたは書き込み方向) で発生した最初のエラーを保持します。
    • setErrorLocked(err error) error メソッドが追加され、halfConn のミューテックスをロックした状態でエラーを設定します。
    • error() error メソッドが追加され、halfConn のミューテックスをロックした状態でエラーを取得します。
    --- a/src/pkg/crypto/tls/conn.go
    +++ b/src/pkg/crypto/tls/conn.go
    @@ -116,6 +93,8 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
     // connection, either sending or receiving.\n type halfConn struct {\n     sync.Mutex\n    +\n    +	err     error       // first permanent error\n     	version uint16      // protocol version\n     	cipher  interface{} // cipher algorithm\n     	mac     macFunction\n    @@ -129,6 +108,18 @@ type halfConn struct {\n     	inDigestBuf, outDigestBuf []byte\n     }\n     \n    +func (hc *halfConn) setErrorLocked(err error) error {\n    +	hc.err = err\n    +	return err\n    +}\n    +\n    +func (hc *halfConn) error() error {\n    +	hc.Lock()\n    +	err := hc.err\n    +	hc.Unlock()\n    +	return err\n    +}\n    +\n     // prepareCipherSpec sets the encryption and MAC states\n     // that a subsequent changeCipherSpec will use.\n     func (hc *halfConn) prepareCipherSpec(version uint16, cipher interface{}, mac macFunction) {
    
  3. エラー設定とチェックロジックの変更:

    • readRecord メソッド内で、エラーを c.setError(err) で設定していた箇所が、c.in.setErrorLocked(err) に変更されました。これにより、読み取り方向のエラーは c.inerr フィールドにのみ影響するようになります。
    • sendAlertLocked メソッド内で、ローカルエラーを c.setError(...) で設定していた箇所が、c.out.setErrorLocked(...) に変更されました。これにより、アラート送信(書き込み方向の操作)によるエラーは c.outerr フィールドにのみ影響するようになります。
    • writeRecord メソッド内で、書き込みエラーを c.setError(...) で設定していた箇所が、c.out.setErrorLocked(...) に変更されました。
    • readHandshake メソッド内で、以前 c.error() をチェックしていた箇所が c.in.err をチェックするように変更されました。
    • Write メソッド内で、以前 c.error() をチェックしていた箇所が削除され、c.out.err をチェックするロジックが追加されました。また、c.setError(err) でエラーを返していた箇所が c.out.setErrorLocked(err) に変更されました。
    • Read メソッド内で、以前 c.error() をチェックしていた箇所が c.in.err をチェックするように変更されました。
    • Handshake メソッド内で、以前 c.error() をチェックしていた箇所が c.handshakeErr をチェックするように変更されました。また、ハンドシェイクの実行結果を c.handshakeErr に設定するように変更されました。

    これらの変更により、エラーの発生源(読み取り、書き込み、ハンドシェイク)に応じて、それぞれ独立したエラーフィールドにエラーが記録されるようになり、エラーの伝播がより正確に制御されるようになりました。

src/pkg/crypto/tls/handshake_client.go および src/pkg/crypto/tls/handshake_server.go の変更

これらのファイルでは、readFinished メソッド内で c.error() を呼び出していた箇所が、c.in.error() を呼び出すように変更されました。これは、ハンドシェイクの完了メッセージの読み取りが、読み取り方向の操作であることを明確にし、そのエラーが c.in.err に関連付けられるようにするためです。

--- a/src/pkg/crypto/tls/handshake_client.go
+++ b/src/pkg/crypto/tls/handshake_client.go
@@ -501,7 +501,7 @@ func (hs *clientHandshakeState) readFinished() error {
 	c := hs.c
 
 	c.readRecord(recordTypeChangeCipherSpec)
-	if err := c.error(); err != nil {
+	if err := c.in.error(); err != nil {
 		return err
 	}
--- a/src/pkg/crypto/tls/handshake_server.go
+++ b/src/pkg/crypto/tls/handshake_server.go
@@ -470,7 +470,7 @@ func (hs *serverHandshakeState) readFinished() error {
 	c := hs.c
 
 	c.readRecord(recordTypeChangeCipherSpec)
-	if err := c.error(); err != nil {
+	if err := c.in.error(); err != nil {
 		return err
 	}

これらの変更は、TLSコネクションのエラーハンドリングをより堅牢にし、読み書きの競合状態による情報損失を防ぐ上で非常に重要です。

関連リンク

参考にした情報源リンク

  • 上記のGitHub issueとGo CLのページ
  • Go言語の crypto/tls パッケージのソースコード
  • TLSプロトコルに関する一般的な知識
  • 並行プログラミングにおける競合状態に関する一般的な知識