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

[インデックス 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接続が確立される際にクライアントとサーバー間で行われる一連の初期ネゴシエーションプロセスです。このプロセス中に、以下のことが行われます。

  1. プロトコルバージョンのネゴシエーション: クライアントとサーバーがサポートするTLSプロトコルバージョン(例: TLS 1.2, TLS 1.3)を決定します。
  2. 暗号スイートの選択: 使用する暗号アルゴリズムの組み合わせ(鍵交換、認証、暗号化、ハッシュ関数)を決定します。
  3. サーバー認証: サーバーが自身の身元を証明するためにデジタル証明書を提示します。クライアントはこの証明書を検証します。
  4. 鍵交換: クライアントとサーバーが、セッション中にデータを暗号化・復号化するための共有秘密鍵を安全に生成します。
  5. セッション鍵の生成: 共有秘密鍵から、実際のデータ暗号化に使用されるセッション鍵が導出されます。

ハンドシェイクが成功すると、以降のアプリケーションデータは確立されたセッション鍵と選択された暗号スイートに基づいて暗号化・復号化されます。crypto/tls.Connは、通常、最初のReadまたはWrite呼び出し時に自動的にハンドシェイクを実行します。

技術的詳細

このコミットが解決しようとした技術的な問題は、crypto/tls.Conn.Readメソッドが、len(b) == 0(つまり、読み込むべきバッファがゼロサイズ)の場合でもブロックしてしまうという点にありました。

Conn.Readメソッドの既存のロジックでは、まずTLSハンドシェイクが完了しているかを確認し、未完了であればc.Handshake()を呼び出してハンドシェイクを強制的に実行します。これは、ReadWriteが呼び出された時点で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ハンドシェイクが実行された直後に挿入されています。

  1. if len(b) == 0 { ... }: これは、Readメソッドに渡されたバイトスライスbの長さがゼロであるかどうかをチェックする条件分岐です。
  2. // 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)はハンドシェイクをトリガーすることなく即座にリターンしてしまい、既存のコードの挙動を変えてしまう可能性がありました。
  3. 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

参考にした情報源リンク