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

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

このコミットは、Go言語の実験的なSSHパッケージ(exp/ssh)における、パケット長に関連する3つのビットシフトバグを修正するものです。具体的には、channel.goclient.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氏による初期報告と調査が、この問題の特定と修正のきっかけとなりました。

前提知識の解説

このコミットを理解するためには、以下の概念が前提となります。

  1. SSH (Secure Shell) プロトコル: SSHは、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。クライアントとサーバー間で暗号化された通信チャネルを確立し、リモートコマンド実行、ファイル転送(SCP/SFTP)、ポートフォワーディングなどを行います。SSHプロトコルは、メッセージの送受信に特定のフォーマットを使用し、各メッセージにはタイプ、長さ、ペイロードなどの情報が含まれます。

  2. バイトオーダー (Endianness): マルチバイトのデータをメモリに格納する際のバイトの順序を指します。SSHプロトコルでは、通常、ネットワークバイトオーダーとしてビッグエンディアン(最上位バイトが最初に格納される)が使用されます。これは、4バイトの整数0x12345678をバイト配列に変換する際に、[0x12, 0x34, 0x56, 0x78]の順で格納されることを意味します。

  3. ビットシフト演算子: 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は通常不要です。
  4. Go言語の型変換と演算子の優先順位: Go言語では、型変換は他の多くの演算子よりも高い優先順位を持ちます。例えば、byte(c.theirId) >> 24という式では、まずc.theirIdbyte型に変換され(この時点で上位ビットが失われる)、その後に右シフト演算が適用されます。これがこのバグの根本原因でした。正しい動作のためには、ビットシフトを先に行い、その結果を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) >> 240x04 >> 24 となり、結果は 0 になります。
  • byte(c.theirId) >> 160x04 >> 16 となり、結果は 0 になります。
  • byte(c.theirId) >> 80x04 >> 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 >> 240x01 になります。byte(0x01)0x01
  • c.theirId >> 160x0102 になります。byte(0x0102)0x02
  • c.theirId >> 80x010203 になります。byte(0x010203)0x03
  • byte(c.theirId)0x04

これにより、packet配列には[0x01, 0x02, 0x03, 0x04]という正しいバイトシーケンスが格納されるようになり、SSHプロトコルの仕様に準拠したパケットが生成されるようになりました。この修正は、SSH通信の信頼性と互換性を確保するために不可欠でした。

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

このコミットでは、以下の2つのファイルが変更されています。

  1. src/pkg/exp/ssh/channel.go
  2. 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 channeldata 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通信の信頼性が向上しました。

関連リンク

参考にした情報源リンク

  • SSH File Transfer Protocol (SFTP) draft-ietf-secsh-filexfer-02.txt (SSHプロトコルのメッセージフォーマットに関する一般的な情報源)
  • Go言語の型変換と演算子の優先順位に関するドキュメント (Go言語の公式ドキュメントやチュートリアル)
  • ビットシフト演算子に関する一般的なプログラミングの知識