[インデックス 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セッションの安定性と信頼性に直接影響を与える可能性がありました。
具体的には、以下の問題が指摘されていました。
Write
メソッドに渡されるバッファサイズが現在のウィンドウサイズ(w.rwin
)よりも大きい場合に、w.rwin
が負の値になる可能性があった。これは、SSHプロトコルにおけるウィンドウベースのフロー制御の原則に反し、データの送信が正しく行われない原因となり得ました。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つの整数a
とb
のうち、小さい方の値を返すシンプルなヘルパー関数です。このコミットでは、送信するデータ量をリモートウィンドウサイズに制限するために導入されました。
msgChannelData
msgChannelData
は、SSHプロプロトコルで定義されているメッセージタイプの一つで、チャネルを通じて実際のアプリケーションデータを送信するために使用されます。このメッセージには、チャネル識別子、データ長、そして実際のデータが含まれます。
技術的詳細
このコミットで修正された2つのフロー制御バグは、SSHプロトコルの信頼性と効率性に直接関わるものでした。
1. w.rwin
が負になる問題:
SSHプロトコルでは、送信側は受信側が提供するウィンドウサイズを超えてデータを送信してはなりません。chanWriter.Write
メソッドの元の実装では、Write
に渡されたdata
スライスのサイズが、現在のリモートウィンドウサイズw.rwin
よりも大きい場合、w.rwin
からlen(data)
を単純に減算していました。これにより、w.rwin
が負の値になる可能性がありました。
w.rwin -= n
(ここでn
はlen(data)
)
この問題は、送信側が自身の内部状態(w.rwin
)において、リモートピアが受け入れ可能なデータ量を超過していると誤って認識する原因となります。結果として、送信側はリモートピアからのウィンドウ更新を待たずに、負のウィンドウサイズに基づいてさらにデータを送信しようとするか、あるいは不正確なフロー制御ロジックによりデッドロック状態に陥る可能性がありました。SSHプロトコルでは、ウィンドウサイズは常に非負であるべきです。
2. writePacket
に渡されるデータ量がw.rwin
によって制限されていなかった問題:
これはより深刻な問題でした。chanWriter.Write
は、w.rwin
が0の場合にのみリモートピアからのウィンドウ更新を待つロジックを持っていましたが、実際にwritePacket
を呼び出す際に、送信するデータ量n
をw.rwin
に制限していませんでした。
元のコードでは、n = len(data)
として、Write
に渡されたdata
スライス全体を一度にwritePacket
に渡していました。
n = len(data)
err = w.clientChan.writePacket(append(packet, data...))
これは、たとえw.rwin
がlen(data)
よりもはるかに小さくても、data
全体が送信されることを意味します。SSHプロトコルの仕様では、送信されるデータパケットのサイズは、現在のウィンドウサイズを超えてはなりません。この違反が発生した場合、リモートピアは以下のいずれかの挙動を示す可能性があります。
- サイレントなデータ破棄: プロトコル違反のデータを警告なしに破棄し、送信側はデータが正常に送信されたと誤認する。これにより、アプリケーションレベルでのデータ損失が発生します。
- 接続の切断: プロトコル違反を検出し、セキュリティ上の理由またはプロトコルの一貫性を保つために接続を強制的に切断する。これにより、SSHセッションが予期せず終了します。
どちらの挙動も、SSH接続の信頼性と安定性を著しく損なうものであり、アプリケーションの動作に深刻な影響を与える可能性がありました。
このコミットでは、これらの問題を解決するために、Write
メソッドのループ構造とデータ送信ロジックが根本的に見直されました。特に、min
関数を導入し、送信するデータ量をw.rwin
とlen(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フロー制御の原則をより厳密に遵守するように変更されました。
変更点と修正のメカニズム:
-
for len(data) > 0
ループの導入:- 元のコードは
for {}
の無限ループで、return
で抜ける構造でした。新しいコードでは、Write
に渡されたdata
スライスがすべて送信されるまでループを継続するfor len(data) > 0
という条件付きループに変更されました。これにより、大きなデータブロックが渡された場合でも、ウィンドウサイズに合わせて分割して送信できるようになりました。
- 元のコードは
-
for w.rwin < 1
ループの変更:- 元のコードでは
if w.rwin == 0
でしたが、w.rwin < 1
(つまりw.rwin <= 0
)に変更されました。これにより、ウィンドウサイズが負になる可能性を完全に排除し、ウィンドウが利用可能になるまで確実に待機するようになりました。 continue
文が削除され、ウィンドウが更新されたらすぐに次の処理(データ送信)に進むようになりました。
- 元のコードでは
-
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)も同時に解決されます。
- これが最も重要な変更点です。送信するデータ量
-
data = data[n:]
による送信済みデータのスライス:min
関数によって決定されたn
バイトのデータが送信された後、data = data[n:]
によってdata
スライスが更新され、送信済みの部分が切り捨てられます。これにより、次のループイテレーションでは未送信のデータのみが処理されるようになります。
-
written += n
による合計書き込みバイト数の追跡:Write
メソッドの戻り値n int
がwritten int
に変更され、実際に書き込まれたバイト数の合計を正確に追跡するようになりました。
-
panic("unreachable")
の削除:- 元のコードにあった到達不能な
panic
文が削除されました。新しいロジックでは、すべてのデータが送信されるか、エラーが発生するまでループが継続するため、このpanic
は不要になりました。
- 元のコードにあった到達不能な
-
min
ヘルパー関数の追加:min(a, b int) int
というシンプルなヘルパー関数がファイルの下部に追加されました。
これらの変更により、chanWriter
はSSHプロトコルのフロー制御を正しく実装し、データの信頼性の高い送信を保証するようになりました。
関連リンク
- Go CL 5511043: https://golang.org/cl/5511043
参考にした情報源リンク
- N/A (今回はWeb検索を使用しませんでした。コミットメッセージとコード差分から十分な情報が得られました。)