[インデックス 10520] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージ(exp/ssh
)における、パケット長に関連する3つのビットシフトバグを修正するものです。具体的には、channel.go
とclient.go
内のコードで、byte()
型変換とビットシフト演算子の適用順序が原因で発生していたデータエンコーディングの誤りを修正しています。
コミット
commit ce7e11997b9706aa3e0c2aa284470b8e8c11b86c
Author: Dave Cheney <dave@cheney.net>
Date: Mon Nov 28 12:10:16 2011 -0500
exp/ssh: fix three shift bugs related to packet lengths
Thanks for Ke Lan for the initial report and investigation.
R=agl, gustav.paul, tg8866, rsc
CC=golang-dev
https://golang.org/cl/5443044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ce7e11997b9706aa3e0c2aa284470b8e8c11b86c
元コミット内容
exp/ssh: fix three shift bugs related to packet lengths
このコミットは、パケット長に関連する3つのビットシフトバグを修正します。 Ke Lan氏の初期報告と調査に感謝します。
変更の背景
このコミットは、Go言語の実験的なSSHパッケージ(exp/ssh
)において、SSHプロトコルメッセージのエンコード時に発生していた深刻なバグを修正するために行われました。具体的には、SSHパケットのヘッダー部分で、チャネルID(theirId
)やペイロード長(len(todo)
、n
)といった4バイトの整数値をバイト配列に変換する際に、ビットシフト演算子の適用順序に関する誤りがありました。
元のコードでは、byte(value) >> shift
のように記述されていました。Go言語では、byte()
への型変換は値を8ビットに切り詰めます。このため、value
が8ビットを超える値(例えば、0x12345678
のような32ビット整数)であった場合、まず下位8ビットのみがbyte
型に変換され、その後にビットシフトが行われていました。結果として、上位のバイト(例えば、>> 24
で取得しようとしていた最上位バイト)は、型変換によって既に失われており、常に0
になってしまうという問題が発生していました。
このバグは、SSHプロトコルにおける重要な情報(チャネルIDやデータ長)のエンコードを誤らせ、結果としてSSHセッションの確立失敗やデータ転送の破損を引き起こす可能性がありました。Ke Lan氏による初期報告と調査が、この問題の特定と修正のきっかけとなりました。
前提知識の解説
このコミットを理解するためには、以下の概念が前提となります。
-
SSH (Secure Shell) プロトコル: SSHは、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。クライアントとサーバー間で暗号化された通信チャネルを確立し、リモートコマンド実行、ファイル転送(SCP/SFTP)、ポートフォワーディングなどを行います。SSHプロトコルは、メッセージの送受信に特定のフォーマットを使用し、各メッセージにはタイプ、長さ、ペイロードなどの情報が含まれます。
-
バイトオーダー (Endianness): マルチバイトのデータをメモリに格納する際のバイトの順序を指します。SSHプロトコルでは、通常、ネットワークバイトオーダーとしてビッグエンディアン(最上位バイトが最初に格納される)が使用されます。これは、4バイトの整数
0x12345678
をバイト配列に変換する際に、[0x12, 0x34, 0x56, 0x78]
の順で格納されることを意味します。 -
ビットシフト演算子: Go言語を含む多くのプログラミング言語で提供されるビット単位の操作です。
>>
(右シフト): ビットを右に移動させます。x >> n
は、x
のビットをn
ビット右に移動させ、右端からあふれたビットは破棄されます。これは、整数を2の累乗で割ることに相当します。- 例えば、32ビット整数
value
から各バイトを抽出する場合、- 最上位バイト:
(value >> 24) & 0xFF
- 2番目のバイト:
(value >> 16) & 0xFF
- 3番目のバイト:
(value >> 8) & 0xFF
- 最下位バイト:
value & 0xFF
Go言語では、byte()
への型変換が自動的に下位8ビットを抽出するため、& 0xFF
は通常不要です。
- 最上位バイト:
-
Go言語の型変換と演算子の優先順位: Go言語では、型変換は他の多くの演算子よりも高い優先順位を持ちます。例えば、
byte(c.theirId) >> 24
という式では、まずc.theirId
がbyte
型に変換され(この時点で上位ビットが失われる)、その後に右シフト演算が適用されます。これがこのバグの根本原因でした。正しい動作のためには、ビットシフトを先に行い、その結果をbyte
型に変換する必要があります。
技術的詳細
このバグは、Go言語の型変換とビットシフト演算子の優先順位に関する誤解、または不注意によって引き起こされました。
SSHプロトコルでは、チャネルIDやデータ長などの数値は、通常4バイトの符号なし整数としてエンコードされ、ネットワークバイトオーダー(ビッグエンディアン)で送信されます。これは、32ビットの整数を4つの個別のバイトに分解し、それぞれをパケットの適切な位置に配置することを意味します。
元のコードでは、以下のようなパターンが見られました。
packet[1] = byte(c.theirId) >> 24
packet[2] = byte(c.theirId) >> 16
packet[3] = byte(c.theirId) >> 8
packet[4] = byte(c.theirId) // これは正しい
ここで問題となるのは、byte(c.theirId)
の部分です。c.theirId
が例えばuint32
型であった場合、byte(c.theirId)
はc.theirId
の値を8ビットのbyte
型に変換します。この変換は、c.theirId
の最下位8ビットのみを保持し、残りの上位ビットは切り捨てられます。
例: c.theirId = 0x01020304
(32ビット整数)
byte(c.theirId)
は0x04
になります。byte(c.theirId) >> 24
は0x04 >> 24
となり、結果は0
になります。byte(c.theirId) >> 16
は0x04 >> 16
となり、結果は0
になります。byte(c.theirId) >> 8
は0x04 >> 8
となり、結果は0
になります。
このように、上位バイトを抽出する意図であったにもかかわらず、実際には常に0
が書き込まれていました。これは、SSHパケットのヘッダー情報が正しくエンコードされないことを意味し、通信相手がパケットを正しく解釈できなくなる原因となります。
修正後のコードは、この問題を解決するために、ビットシフト演算をbyte()
型変換よりも先に行うように変更されました。
packet[1] = byte(c.theirId >> 24)
packet[2] = byte(c.theirId >> 16)
packet[3] = byte(c.theirId >> 8)
packet[4] = byte(c.theirId) // これは変更なし
例: c.theirId = 0x01020304
(32ビット整数)
c.theirId >> 24
は0x01
になります。byte(0x01)
は0x01
。c.theirId >> 16
は0x0102
になります。byte(0x0102)
は0x02
。c.theirId >> 8
は0x010203
になります。byte(0x010203)
は0x03
。byte(c.theirId)
は0x04
。
これにより、packet
配列には[0x01, 0x02, 0x03, 0x04]
という正しいバイトシーケンスが格納されるようになり、SSHプロトコルの仕様に準拠したパケットが生成されるようになりました。この修正は、SSH通信の信頼性と互換性を確保するために不可欠でした。
コアとなるコードの変更箇所
このコミットでは、以下の2つのファイルが変更されています。
src/pkg/exp/ssh/channel.go
src/pkg/exp/ssh/client.go
それぞれのファイルで、byte(value) >> shift
の形式で記述されていた部分が byte(value >> shift)
に変更されています。
src/pkg/exp/ssh/channel.go
の変更点:
--- a/src/pkg/exp/ssh/channel.go
+++ b/src/pkg/exp/ssh/channel.go
@@ -244,13 +244,13 @@ func (c *channel) Write(data []byte) (n int, err error) {
packet := make([]byte, 1+4+4+len(todo))
packet[0] = msgChannelData
- packet[1] = byte(c.theirId) >> 24
- packet[2] = byte(c.theirId) >> 16
- packet[3] = byte(c.theirId) >> 8
+ packet[1] = byte(c.theirId >> 24)
+ packet[2] = byte(c.theirId >> 16)
+ packet[3] = byte(c.theirId >> 8)
packet[4] = byte(c.theirId)
- packet[5] = byte(len(todo)) >> 24
- packet[6] = byte(len(todo)) >> 16
- packet[7] = byte(len(todo)) >> 8
+ packet[5] = byte(len(todo) >> 24)
+ packet[6] = byte(len(todo) >> 16)
+ packet[7] = byte(len(todo) >> 8)
packet[8] = byte(len(todo))
copy(packet[9:], todo)
src/pkg/exp/ssh/client.go
の変更点:
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -403,8 +403,8 @@ func (w *chanWriter) Write(data []byte) (n int, err error) {
n = len(data)
packet := make([]byte, 0, 9+n)
packet = append(packet, msgChannelData,
- byte(w.peersId)>>24, byte(w.peersId)>>16, byte(w.peersId)>>8, byte(w.peersId),
- byte(n)>>24, byte(n)>>16, byte(n)>>8, byte(n))\
+ byte(w.peersId>>24), byte(w.peersId>>16), byte(w.peersId>>8), byte(w.peersId),
+ byte(n>>24), byte(n>>16), byte(n>>8), byte(n))\
err = w.writePacket(append(packet, data...))\
w.rwin -= n
return
コアとなるコードの解説
変更されたコードは、SSHプロトコルにおけるSSH_MSG_CHANNEL_DATA
メッセージの構築部分です。このメッセージは、SSHチャネルを介してデータを送信するために使用されます。メッセージのフォーマットは通常、以下のようになります。
byte SSH_MSG_CHANNEL_DATA
uint32 recipient channel
uint32 data length
byte[data length] data
ここで、recipient channel
とdata length
は4バイトの符号なし整数としてエンコードされます。
src/pkg/exp/ssh/channel.go
の (*channel).Write
メソッド:
このメソッドは、チャネルにデータを書き込む際に呼び出されます。packet
というバイトスライスを構築し、SSHメッセージのヘッダーとペイロードを格納します。
packet[0] = msgChannelData
: メッセージタイプを設定します。packet[1]
からpacket[4]
までがc.theirId
(受信者チャネルID)の4バイトを格納する部分です。- 修正前:
byte(c.theirId) >> 24
など。これは、c.theirId
をまずbyte
に切り詰めてからシフトするため、上位バイトが失われ、常に0
が書き込まれていました。 - 修正後:
byte(c.theirId >> 24)
など。これは、c.theirId
を先にシフトして目的のバイトを最下位に移動させてからbyte
に変換するため、正しいバイト値が抽出されます。これにより、c.theirId
の32ビット値がビッグエンディアン形式で正しくバイト配列に変換されます。
- 修正前:
packet[5]
からpacket[8]
までがlen(todo)
(データ長)の4バイトを格納する部分です。- 同様に、修正前は
byte(len(todo)) >> 24
のように誤った順序で演算が行われていましたが、修正後はbyte(len(todo) >> 24)
のように正しい順序に修正されています。
- 同様に、修正前は
copy(packet[9:], todo)
: 実際のデータペイロードをパケットにコピーします。
src/pkg/exp/ssh/client.go
の (*chanWriter).Write
メソッド:
このメソッドも、クライアント側でチャネルにデータを書き込む際に使用されます。packet
スライスを構築し、msgChannelData
メッセージを生成します。
byte(w.peersId)>>24, byte(w.peersId)>>16, byte(w.peersId)>>8, byte(w.peersId)
: ここでw.peersId
(ピアのチャネルID)の4バイトをエンコードしています。- 修正前は
byte(w.peersId)>>24
のように誤った順序でしたが、修正後はbyte(w.peersId>>24)
のように正しい順序に修正されています。
- 修正前は
byte(n)>>24, byte(n)>>16, byte(n)>>8, byte(n)
: ここでn
(データ長)の4バイトをエンコードしています。- 同様に、修正前は
byte(n)>>24
のように誤った順序でしたが、修正後はbyte(n>>24)
のように正しい順序に修正されています。
- 同様に、修正前は
これらの修正により、SSHプロトコルで要求される4バイトの整数値が、正しくビッグエンディアン形式のバイト配列に変換されるようになり、SSH通信の信頼性が向上しました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- このコミットのGerritチェンジリスト: https://golang.org/cl/5443044
参考にした情報源リンク
- SSH File Transfer Protocol (SFTP) draft-ietf-secsh-filexfer-02.txt (SSHプロトコルのメッセージフォーマットに関する一般的な情報源)
- Go言語の型変換と演算子の優先順位に関するドキュメント (Go言語の公式ドキュメントやチュートリアル)
- ビットシフト演算子に関する一般的なプログラミングの知識