[インデックス 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)と、それに伴う情報損失でした。具体的には、以下のようなシナリオが考えられます。
- 書き込みエラーが読み取りに影響: TLSコネクションでデータ送信中にエラー(例: ネットワーク切断)が発生し、
connErr
にそのエラーが設定されたとします。この後、アプリケーションがコネクションからのデータ読み取りを試みると、たとえ読み取りバッファにまだピアからの有効なデータが残っていたとしても、connErr
に設定された書き込みエラーがすぐに返されてしまい、その有効なデータが処理される機会が失われていました。 - ピアからの追加情報の損失: 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
フィールドが使用されていましたが、これは読み取り、書き込み、ハンドシェイクの各操作で発生するエラーを区別せず、最初のエラーが設定されるとそれ以降の操作に影響を与えていました。
この変更により、エラー管理は以下のように分割されました。
handshakeErr error
:tls.Conn
構造体に新しく追加されたフィールドで、TLSハンドシェイク中に発生したエラーを保持します。ハンドシェイクはコネクションの初期設定フェーズであり、読み取りと書き込みの両方のパスがこのエラーをチェックする必要があるため、独立したエラーフィールドとして分離されました。halfConn.err error
:halfConn
構造体(tls.Conn
のin
とout
フィールドの型)に新しく追加されたフィールドです。これにより、読み取り方向 (c.in.err
) と書き込み方向 (c.out.err
) のエラーがそれぞれ独立して管理されるようになります。
この分割によって、以下のような技術的な改善が実現されました。
- エラーの独立性: 読み取り操作中に発生したエラーが書き込み操作に影響を与えたり、その逆が発生したりすることがなくなりました。これにより、例えば書き込みエラーが発生しても、読み取りバッファに残っているデータを最後まで処理できるようになります。
- 情報損失の防止: ピアから送信される
alert
メッセージ(特にclose_notify
)のような重要な情報が、別の方向で発生したエラーによって失われることを防ぎます。これにより、コネクションの正常なシャットダウンや、エラー発生時のより正確な診断が可能になります。 - ハンドシェイクエラーの明確化: ハンドシェイクエラーが独立したフィールドになったことで、ハンドシェイクが完了する前に発生したエラーが、その後の読み書き操作にどのように影響するかをより明確に制御できるようになりました。
具体的なコード変更としては、以下の点が挙げられます。
tls.Conn
からconnErr
構造体が削除され、代わりにhandshakeErr
フィールドが追加されました。halfConn
構造体にerr
フィールドが追加され、setErrorLocked
とerror
メソッドが定義されました。これらのメソッドは、halfConn
のミューテックス (sync.Mutex
) を使用して、エラーフィールドへの安全なアクセスを保証します。tls.Conn
のreadRecord
,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.go
と src/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
の変更
-
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.
- 以前存在した
-
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) {
-
エラー設定とチェックロジックの変更:
readRecord
メソッド内で、エラーをc.setError(err)
で設定していた箇所が、c.in.setErrorLocked(err)
に変更されました。これにより、読み取り方向のエラーはc.in
のerr
フィールドにのみ影響するようになります。sendAlertLocked
メソッド内で、ローカルエラーをc.setError(...)
で設定していた箇所が、c.out.setErrorLocked(...)
に変更されました。これにより、アラート送信(書き込み方向の操作)によるエラーはc.out
のerr
フィールドにのみ影響するようになります。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コネクションのエラーハンドリングをより堅牢にし、読み書きの競合状態による情報損失を防ぐ上で非常に重要です。
関連リンク
- Go issue #7414: https://github.com/golang/go/issues/7414
- Go CL 69090044: https://golang.org/cl/69090044
- Go
crypto/tls
package documentation: https://pkg.go.dev/crypto/tls
参考にした情報源リンク
- 上記のGitHub issueとGo CLのページ
- Go言語の
crypto/tls
パッケージのソースコード - TLSプロトコルに関する一般的な知識
- 並行プログラミングにおける競合状態に関する一般的な知識