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

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

exp/ssh: chanWriterにおける2つのフロー制御バグの修正

コミット

コミットハッシュ: 424f53fa0c60fd62cb77186ffb9643dae5429a5c 作者: Dave Cheney dave@cheney.net 日付: 2012年1月4日(水) 10:36:21 -0500

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

https://github.com/golang/go/commit/424f53fa0c60fd62cb77186ffb9643dae5429a5c

元コミット内容

exp/ssh: fix two flow control bugs in chanWriter

This CL fixes two issues sending data to the remote peer.
The first bug occurs when the size of the buffer passed to
Write is larger than the current window, in this case, w.rwin
can become negative.

The second issue is more problematic than the first as the
amount of data passed to writePacket was not limited to w.rwin.
In this case the remote peer could silently drop the additional
data, or drop the connection.

Credit to Jacek Masiulaniec for the bug report.

R=agl, jacek.masiulaniec
CC=golang-dev
https://golang.org/cl/5511043

変更の背景

このコミットは、Go言語の実験的なSSHパッケージ(exp/ssh)におけるchanWriterの実装に存在していた、リモートピアへのデータ送信に関する2つのフロー制御バグを修正するために行われました。これらのバグは、SSHセッションの安定性と信頼性に直接影響を与える可能性がありました。

具体的には、以下の問題が指摘されていました。

  1. Writeメソッドに渡されるバッファサイズが現在のウィンドウサイズ(w.rwin)よりも大きい場合に、w.rwinが負の値になる可能性があった。これは、SSHプロトコルにおけるウィンドウベースのフロー制御の原則に反し、データの送信が正しく行われない原因となり得ました。
  2. writePacket関数に渡されるデータ量がw.rwinによって制限されていなかった。この問題はより深刻で、リモートピアが余分なデータをサイレントに破棄したり、接続自体を切断したりする可能性がありました。これは、SSHセッションの予期せぬ終了やデータ損失につながる重大な欠陥でした。

これらのバグはJacek Masiulaniecによって報告され、その修正がこのコミットの目的です。

前提知識の解説

SSH (Secure Shell)

SSHは、ネットワークを介してコンピュータを安全に操作するためのプロトコルです。クライアントとサーバー間で暗号化された通信チャネルを提供し、リモートコマンドの実行、ファイル転送(SCP/SFTP)、ポートフォワーディングなど、様々な機能を実現します。SSHプロトコルは、複数の論理的な「チャネル」を単一のTCP接続上で多重化して使用します。

SSHのフロー制御 (Flow Control)

SSHプロトコルでは、各チャネルごとにフロー制御メカニズムが実装されています。これは、送信側が受信側の処理能力を超えてデータを送りつけないようにするための仕組みです。受信側は、自身が受け入れ可能なデータ量を示す「ウィンドウサイズ」を送信側に通知します。送信側はこのウィンドウサイズ内でデータを送信し、受信側がデータを処理してウィンドウサイズを更新するまで、それ以上のデータ送信を控えます。これにより、バッファオーバーフローを防ぎ、ネットワークの輻輳を緩和します。

chanWriter

Go言語のexp/sshパッケージにおけるchanWriterは、SSHチャネルを通じてデータを書き込むためのio.Writerインターフェースを実装した構造体です。これは、通常、リモートプロセスの標準入力(stdin)にデータを送信するために使用されます。chanWriterは、SSHチャネルのウィンドウサイズを管理し、フロー制御を適切に行う責任を負います。

w.rwin (Remote Window)

chanWriter構造体内のw.rwinフィールドは、リモートピアが現在受け入れ可能な残りのデータ量(バイト単位)を示す「リモートウィンドウサイズ」を表します。この値は、リモートピアからウィンドウ更新メッセージを受信するたびに増加し、ローカルからデータを送信するたびに減少します。SSHのフロー制御において非常に重要な役割を果たします。

writePacket

writePacketは、SSHプロトコルメッセージをネットワーク経由で送信するための内部関数です。SSHチャネルを通じてデータを送信する際には、この関数が呼び出され、データはSSHプロトコルで定義されたパケット形式にカプセル化されて送信されます。

io.EOF

io.EOFは、Go言語のioパッケージで定義されているエラー変数で、入力の終わりに達したことを示します。例えば、Readメソッドがこれ以上読み取るデータがない場合に返されます。

min 関数

min(a, b int) intは、2つの整数abのうち、小さい方の値を返すシンプルなヘルパー関数です。このコミットでは、送信するデータ量をリモートウィンドウサイズに制限するために導入されました。

msgChannelData

msgChannelDataは、SSHプロプロトコルで定義されているメッセージタイプの一つで、チャネルを通じて実際のアプリケーションデータを送信するために使用されます。このメッセージには、チャネル識別子、データ長、そして実際のデータが含まれます。

技術的詳細

このコミットで修正された2つのフロー制御バグは、SSHプロトコルの信頼性と効率性に直接関わるものでした。

1. w.rwinが負になる問題: SSHプロトコルでは、送信側は受信側が提供するウィンドウサイズを超えてデータを送信してはなりません。chanWriter.Writeメソッドの元の実装では、Writeに渡されたdataスライスのサイズが、現在のリモートウィンドウサイズw.rwinよりも大きい場合、w.rwinからlen(data)を単純に減算していました。これにより、w.rwinが負の値になる可能性がありました。

w.rwin -= n (ここでnlen(data))

この問題は、送信側が自身の内部状態(w.rwin)において、リモートピアが受け入れ可能なデータ量を超過していると誤って認識する原因となります。結果として、送信側はリモートピアからのウィンドウ更新を待たずに、負のウィンドウサイズに基づいてさらにデータを送信しようとするか、あるいは不正確なフロー制御ロジックによりデッドロック状態に陥る可能性がありました。SSHプロトコルでは、ウィンドウサイズは常に非負であるべきです。

2. writePacketに渡されるデータ量がw.rwinによって制限されていなかった問題: これはより深刻な問題でした。chanWriter.Writeは、w.rwinが0の場合にのみリモートピアからのウィンドウ更新を待つロジックを持っていましたが、実際にwritePacketを呼び出す際に、送信するデータ量nw.rwinに制限していませんでした。

元のコードでは、n = len(data)として、Writeに渡されたdataスライス全体を一度にwritePacketに渡していました。

n = len(data) err = w.clientChan.writePacket(append(packet, data...))

これは、たとえw.rwinlen(data)よりもはるかに小さくても、data全体が送信されることを意味します。SSHプロトコルの仕様では、送信されるデータパケットのサイズは、現在のウィンドウサイズを超えてはなりません。この違反が発生した場合、リモートピアは以下のいずれかの挙動を示す可能性があります。

  • サイレントなデータ破棄: プロトコル違反のデータを警告なしに破棄し、送信側はデータが正常に送信されたと誤認する。これにより、アプリケーションレベルでのデータ損失が発生します。
  • 接続の切断: プロトコル違反を検出し、セキュリティ上の理由またはプロトコルの一貫性を保つために接続を強制的に切断する。これにより、SSHセッションが予期せず終了します。

どちらの挙動も、SSH接続の信頼性と安定性を著しく損なうものであり、アプリケーションの動作に深刻な影響を与える可能性がありました。

このコミットでは、これらの問題を解決するために、Writeメソッドのループ構造とデータ送信ロジックが根本的に見直されました。特に、min関数を導入し、送信するデータ量をw.rwinlen(data)の小さい方に制限することで、SSHプロトコルのフロー制御規則を厳密に遵守するように修正されました。

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

変更はsrc/pkg/exp/ssh/client.goファイル内のchanWriter構造体のWriteメソッドに集中しています。

--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -420,27 +420,37 @@ type chanWriter struct {
 }
 
 // Write writes data to the remote process's standard input.
-func (w *chanWriter) Write(data []byte) (n int, err error) {
-	for {
-		if w.rwin == 0 {
+func (w *chanWriter) Write(data []byte) (written int, err error) {
+	for len(data) > 0 {
+		for w.rwin < 1 {
 			win, ok := <-w.win
 			if !ok {
 				return 0, io.EOF
 			}
 			w.rwin += win
-			continue
 		}
-		peersId := w.clientChan.peersId
-		n = len(data)
-		packet := make([]byte, 0, 9+n)
-		packet = append(packet, msgChannelData,
-			byte(peersId>>24), byte(peersId>>16), byte(peersId>>8), byte(peersId),
-			byte(n>>24), byte(n>>16), byte(n>>8), byte(n))\n-\t\terr = w.clientChan.writePacket(append(packet, data...))\n+\t\tn := min(len(data), w.rwin)\n+\t\tpeersId := w.clientChan.peersId\n+\t\tpacket := []byte{\n+\t\t\tmsgChannelData,\n+\t\t\tbyte(peersId >> 24), byte(peersId >> 16), byte(peersId >> 8), byte(peersId),\n+\t\t\tbyte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n),\n+\t\t}\n+\t\tif err = w.clientChan.writePacket(append(packet, data[:n]...)); err != nil {\n+\t\t\tbreak\n+\t\t}\n+\t\tdata = data[n:]\n 		w.rwin -= n
-		return
+		written += n
 	}
-	panic("unreachable")
+	return
+}
+
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
 }
 
 func (w *chanWriter) Close() error {

コアとなるコードの解説

修正されたchanWriter.Writeメソッドは、SSHフロー制御の原則をより厳密に遵守するように変更されました。

変更点と修正のメカニズム:

  1. for len(data) > 0 ループの導入:

    • 元のコードはfor {}の無限ループで、returnで抜ける構造でした。新しいコードでは、Writeに渡されたdataスライスがすべて送信されるまでループを継続するfor len(data) > 0という条件付きループに変更されました。これにより、大きなデータブロックが渡された場合でも、ウィンドウサイズに合わせて分割して送信できるようになりました。
  2. for w.rwin < 1 ループの変更:

    • 元のコードではif w.rwin == 0でしたが、w.rwin < 1(つまりw.rwin <= 0)に変更されました。これにより、ウィンドウサイズが負になる可能性を完全に排除し、ウィンドウが利用可能になるまで確実に待機するようになりました。
    • continue文が削除され、ウィンドウが更新されたらすぐに次の処理(データ送信)に進むようになりました。
  3. n := min(len(data), w.rwin) の導入 (バグ1と2の修正):

    • これが最も重要な変更点です。送信するデータ量nを、Writeに渡された残りのデータ量len(data)と、現在のリモートウィンドウサイズw.rwinの小さい方に制限するmin関数が導入されました。
    • これにより、writePacketに渡されるデータ量が常にw.rwin以下に保たれるため、リモートピアがデータを破棄したり接続を切断したりする問題(バグ2)が解決されます。
    • また、w.rwinから減算されるnが常にw.rwin以下になるため、w.rwinが負になる問題(バグ1)も同時に解決されます。
  4. data = data[n:] による送信済みデータのスライス:

    • min関数によって決定されたnバイトのデータが送信された後、data = data[n:]によってdataスライスが更新され、送信済みの部分が切り捨てられます。これにより、次のループイテレーションでは未送信のデータのみが処理されるようになります。
  5. written += n による合計書き込みバイト数の追跡:

    • Writeメソッドの戻り値n intwritten intに変更され、実際に書き込まれたバイト数の合計を正確に追跡するようになりました。
  6. panic("unreachable") の削除:

    • 元のコードにあった到達不能なpanic文が削除されました。新しいロジックでは、すべてのデータが送信されるか、エラーが発生するまでループが継続するため、このpanicは不要になりました。
  7. min ヘルパー関数の追加:

    • min(a, b int) intというシンプルなヘルパー関数がファイルの下部に追加されました。

これらの変更により、chanWriterはSSHプロトコルのフロー制御を正しく実装し、データの信頼性の高い送信を保証するようになりました。

関連リンク

参考にした情報源リンク

  • N/A (今回はWeb検索を使用しませんでした。コミットメッセージとコード差分から十分な情報が得られました。)