[インデックス 19160] ファイルの概要
このコミットは、Go言語の標準ライブラリであるcrypto/tls
パッケージ内のconn.go
ファイルに対する変更です。conn.go
は、TLS (Transport Layer Security) プロトコルを用いたネットワーク接続の確立とデータ送受信を司るConn
型の実装を含んでいます。具体的には、TLSハンドシェイクの管理、レコード層の処理、そしてアプリケーションデータとのインターフェース(Read
およびWrite
メソッド)を提供します。このファイルは、Goアプリケーションが安全な通信を行う上で中心的な役割を担っています。
コミット
commit 853c99ddb8dc25ca361f1efdd65a9b371cc39fcb
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Apr 15 19:40:00 2014 -0700
crypto/tls: don't block on Read of zero bytes
Fixes #7775
LGTM=rsc
R=agl, rsc
CC=golang-codereviews
https://golang.org/cl/88340043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/853c99ddb8dc25ca361f1efdd65a9b371cc39fcb
元コミット内容
crypto/tls: don't block on Read of zero bytes
Fixes #7775
LGTM=rsc
R=agl, rsc
CC=golang-codereviews
https://golang.org/cl/88340043
変更の背景
このコミットの背景には、Go言語のcrypto/tls
パッケージにおけるConn.Read
メソッドの特定の挙動に関する問題がありました。具体的には、Conn.Read
メソッドがゼロバイトのスライス(例: Read(nil)
やRead([]byte{})
)を引数として呼び出された際に、予期せずブロックしてしまうというバグが存在していました。
通常、Go言語のio.Reader
インターフェースを実装するRead
メソッドは、引数として渡されたバイトスライスの長さがゼロの場合、ブロックせずにn=0, err=nil
を返すことが期待されます。これは、読み込むべきデータがない、あるいは読み込みバッファがゼロサイズであるため、何も読み込まずに即座に制御を返すという一般的な慣習です。
しかし、crypto/tls.Conn.Read
の実装では、TLSハンドシェイクがまだ完了していない場合に、Read
の呼び出しがハンドシェイクをトリガーする副作用を持っていました。この副作用を利用して、一部のコードではRead(nil)
のような呼び出しをハンドシェイクの強制実行のために使用することがありました。問題は、ハンドシェイクが完了した後も、ゼロバイトの読み込み要求に対してRead
がブロックし続けてしまい、呼び出し元のゴルーチンがデッドロック状態に陥る可能性があった点です。
この問題は、GoのIssue #7775として報告されており、このコミットはその問題を解決するために導入されました。開発者は、ハンドシェイクの副作用は維持しつつ、ゼロバイト読み込み時の不要なブロックを回避する必要がありました。
前提知識の解説
Go言語のio.Reader
インターフェース
Go言語において、データの読み込み操作は通常、標準ライブラリのio
パッケージで定義されているReader
インターフェースを通じて行われます。
type Reader interface {
Read(p []byte) (n int, err error)
}
このインターフェースのRead
メソッドは、p
に最大len(p)
バイトのデータを読み込み、読み込んだバイト数n
とエラーerr
を返します。慣習として、len(p)
がゼロの場合、Read
メソッドはブロックせずにn=0, err=nil
を返すことが期待されます。これは、読み込むべきバッファがないため、何も処理せずに即座にリターンするという意味合いを持ちます。
TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、クライアントとサーバー間でデータの機密性、完全性、認証を提供します。
Goのcrypto/tls
パッケージ
crypto/tls
は、Go言語の標準ライブラリの一部であり、TLSプロトコルを実装しています。このパッケージを使用することで、開発者は安全なクライアントおよびサーバーアプリケーションを簡単に構築できます。net.Conn
インターフェースをラップし、その上にTLSの暗号化・復号化レイヤーを追加することで、透過的に安全な通信を提供します。
TLSハンドシェイク (Handshake)
TLSハンドシェイクは、TLS接続が確立される際にクライアントとサーバー間で行われる一連の初期ネゴシエーションプロセスです。このプロセス中に、以下のことが行われます。
- プロトコルバージョンのネゴシエーション: クライアントとサーバーがサポートするTLSプロトコルバージョン(例: TLS 1.2, TLS 1.3)を決定します。
- 暗号スイートの選択: 使用する暗号アルゴリズムの組み合わせ(鍵交換、認証、暗号化、ハッシュ関数)を決定します。
- サーバー認証: サーバーが自身の身元を証明するためにデジタル証明書を提示します。クライアントはこの証明書を検証します。
- 鍵交換: クライアントとサーバーが、セッション中にデータを暗号化・復号化するための共有秘密鍵を安全に生成します。
- セッション鍵の生成: 共有秘密鍵から、実際のデータ暗号化に使用されるセッション鍵が導出されます。
ハンドシェイクが成功すると、以降のアプリケーションデータは確立されたセッション鍵と選択された暗号スイートに基づいて暗号化・復号化されます。crypto/tls.Conn
は、通常、最初のRead
またはWrite
呼び出し時に自動的にハンドシェイクを実行します。
技術的詳細
このコミットが解決しようとした技術的な問題は、crypto/tls.Conn.Read
メソッドが、len(b) == 0
(つまり、読み込むべきバッファがゼロサイズ)の場合でもブロックしてしまうという点にありました。
Conn.Read
メソッドの既存のロジックでは、まずTLSハンドシェイクが完了しているかを確認し、未完了であればc.Handshake()
を呼び出してハンドシェイクを強制的に実行します。これは、Read
やWrite
が呼び出された時点でTLS接続が完全に確立されていることを保証するための重要なステップです。
問題は、ハンドシェイクが完了した後、または既に完了している場合でも、len(b) == 0
の条件が適切に処理されていなかったことです。io.Reader
の慣習に従えば、この場合、Read
は即座にn=0, err=nil
を返すべきですが、実際には内部の読み込みループに入り、データが到着するまでブロックし続けていました。これにより、特にRead(nil)
をハンドシェイクのトリガーとして利用していたコードが、ハンドシェイク完了後にデッドロックに陥る可能性がありました。
このコミットによる変更は、この問題を直接的に解決します。c.Handshake()
の呼び出しが完了した直後に、len(b) == 0
であるかをチェックする条件分岐を追加しました。もしlen(b)
がゼロであれば、その場でreturn
し、n=0, err=nil
(Goの関数のゼロ値によるデフォルトの戻り値)を返します。これにより、ハンドシェイクの副作用(Read(nil)
がハンドシェイクをトリガーする)は維持しつつ、ゼロバイト読み込み時の不要なブロックを回避できるようになりました。
この変更は、Read
メソッドのセマンティクスをio.Reader
インターフェースの一般的な期待値に合わせるものであり、TLS接続の堅牢性と予測可能性を向上させます。
コアとなるコードの変更箇所
変更はsrc/pkg/crypto/tls/conn.go
ファイル内のConn.Read
メソッドにあります。
--- a/src/pkg/crypto/tls/conn.go
+++ b/src/pkg/crypto/tls/conn.go
@@ -884,6 +884,11 @@ func (c *Conn) Read(b []byte) (n int, err error) {
if err = c.Handshake(); err != nil {
return
}
+ if len(b) == 0 {
+ // Put this after Handshake, in case people were calling
+ // Read(nil) for the side effect of the Handshake.
+ return
+ }
c.in.Lock()
defer c.in.Unlock()
コアとなるコードの解説
追加されたコードは以下の5行です。
if len(b) == 0 {
// Put this after Handshake, in case people were calling
// Read(nil) for the side effect of the Handshake.
return
}
このコードブロックは、Conn.Read
メソッドの冒頭、TLSハンドシェイクが実行された直後に挿入されています。
if len(b) == 0 { ... }
: これは、Read
メソッドに渡されたバイトスライスb
の長さがゼロであるかどうかをチェックする条件分岐です。// Put this after Handshake, in case people were calling // Read(nil) for the side effect of the Handshake.
: このコメントは、この変更の意図を明確に示しています。Read(nil)
のような呼び出しがTLSハンドシェイクをトリガーするという既存の「副作用」を維持するために、このゼロバイトチェックはc.Handshake()
の呼び出しの後に配置されています。もしこのチェックがハンドシェイクの前に置かれていた場合、Read(nil)
はハンドシェイクをトリガーすることなく即座にリターンしてしまい、既存のコードの挙動を変えてしまう可能性がありました。return
:len(b)
がゼロの場合、このreturn
ステートメントが実行され、メソッドは即座に終了します。Goの関数では、名前付き戻り値(この場合はn int, err error
)が宣言されている場合、return
のみのステートメントはそれらの変数の現在の値(初期値はゼロ値、つまりn=0, err=nil
)を返します。これにより、io.Reader
の慣習に従い、ゼロバイトの読み込み要求に対してブロックせずに0, nil
が返されるようになります。
このシンプルな変更により、crypto/tls.Conn.Read
は、ゼロバイトの読み込み要求に対して適切に振る舞い、不要なブロックを回避できるようになりました。
関連リンク
- Go Issue 7775:
Conn#Read blocks on zero length slice argument
(直接のリンクはGoのIssueトラッカーの変更により見つけにくいですが、このコミットが解決した問題です) - Go Gerrit Change-List: https://golang.org/cl/88340043
参考にした情報源リンク
- goissues.org: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGp4O2vVuI6dKF3OYdBKfJmpRVODH4qCnYNOY0RN62usr7cIvBEaftTZVHLSzj8ruhNoaC_hb8VoDiamDt_ylXvYa5pxbWAHZ0FSsgapvdzp__WBFob5MVxAkrjNMv3X3No2JA5lA-R
- このリンクは、Go Issue #7775に関する情報を提供しており、問題の概要とそれが非常に古い問題であることが示されています。