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

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

このコミットは、Goランタイムのamd64アーキテクチャにおけるmemmove関数の整数オーバーフローのバグを修正するものです。具体的には、memmoveが処理できるメモリブロックのサイズがuint32に制限されていたため、32ビットを超える大きなサイズを扱う際に問題が発生していました。この修正により、memmoveuintptr(ポインタサイズに合わせた符号なし整数型)を使用するようになり、64ビットシステムでより大きなメモリ範囲を正確に処理できるようになります。また、このバグを再現し、修正を検証するための新しいテストケースが追加されています。

コミット

  • コミットハッシュ: 1b8f51c91794e3fb90e582ba22ad06b6ad28e1d4
  • Author: Rémy Oudompheng oudomphe@phare.normalesup.org
  • Date: Sat Mar 9 00:41:03 2013 +0100

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

https://github.com/golang/go/commit/1b8f51c91794e3fb90e582ba22ad06b6ad28e1d4

元コミット内容

    runtime: fix integer overflow in amd64 memmove.
    
    Fixes #4981.
    
    R=bradfitz, fullung, rsc, minux.ma
    CC=golang-dev
    https://golang.org/cl/7474047

変更の背景

この変更は、GoのIssue #4981「runtime.memmove on amd64 has 32-bit length limit」を修正するために行われました。報告された問題は、amd64アーキテクチャ上のruntime.memmove関数が、コピーするバイト数(長さ)を32ビット符号付き整数として扱っていたため、2GBを超えるサイズのメモリブロックを移動しようとすると、整数オーバーフローが発生するというものでした。

amd64システムでは、ポインタやサイズは通常64ビットで表現されます。しかし、memmoveの実装において、長さを示す変数が32ビットとして扱われると、2^31 - 1バイト(約2GB)を超える値が正しく表現できなくなり、負の値として解釈されたり、予期せぬ動作を引き起こしたりする可能性がありました。これは、特に大きなデータ構造やファイル操作を行うアプリケーションにおいて、データの破損やクラッシュにつながる重大なバグでした。

このバグは、Goのランタイムが提供する低レベルのメモリ操作関数に影響を与えるため、Goプログラム全体の安定性と信頼性に関わる問題でした。そのため、memmoveamd64の能力を最大限に活用し、64ビットのサイズを正しく扱えるように修正する必要がありました。

前提知識の解説

memmove関数

memmoveは、C言語の標準ライブラリ関数の一つで、メモリブロックをコピーするために使用されます。memcpyと似ていますが、memmoveはコピー元とコピー先のメモリ領域が重なっている場合でも正しく動作するという重要な違いがあります。これは、コピー操作を順方向または逆方向に行うことで実現されます。Goランタイムのmemmoveも同様の機能を提供し、Goの内部的なメモリ管理やスライス操作などで利用されます。

amd64アーキテクチャ

amd64(またはx86-64)は、64ビットの汎用レジスタと64ビットのアドレス空間を持つCPUアーキテクチャです。これにより、従来の32ビットアーキテクチャよりもはるかに大きなメモリ(理論上は16エクサバイト)を直接アドレス指定できるようになります。Goは、この64ビットの能力を最大限に活用するように設計されています。

アセンブリ命令: MOVQMOVLQSX

  • MOVQ (Move Quadword): amd64アセンブリにおける命令で、64ビットの値をレジスタ間、またはレジスタとメモリ間で移動します。QはQuadword(8バイト、64ビット)を意味します。この命令は、ソースオペランドの64ビット値をそのままデスティネーションにコピーします。

  • MOVLQSX (Move Longword to Quadword with Sign-Extension): この命令は、32ビットのソースオペランドを64ビットのデスティネーションに移動する際に、符号拡張(Sign-Extension)を行います。つまり、32ビット値の最上位ビット(符号ビット)を64ビット値の残りの上位ビットにコピーして埋めます。これにより、32ビットの符号付き整数が64ビットの符号付き整数として正しく表現されます。 しかし、今回のケースのように、本来符号なしのサイズ情報を扱うべき場所でMOVLQSXを使用すると、32ビットの正の値が符号拡張によって負の値として解釈される可能性があり、これがオーバーフローの原因となります。例えば、0xFFFFFFFF(32ビット符号なしで4,294,967,295)は、MOVLQSXで64ビットに拡張されると0xFFFFFFFFFFFFFFFF(64ビット符号付きで-1)として扱われてしまいます。

uintptruint32

  • uint32: 32ビットの符号なし整数型です。最大値は2^32 - 1(約42億)です。
  • uintptr: Go言語における型で、ポインタを保持するのに十分な大きさの符号なし整数型です。そのサイズは、実行されているシステムのポインタサイズ(32ビットシステムでは32ビット、64ビットシステムでは64ビット)に依存します。uintptrは、ポインタと整数の間で変換を行う必要がある場合や、システムコールなどでポインタを整数として渡す必要がある場合に使用されます。メモリサイズやオフセットを表現する際には、システムのアドレス空間の大きさに合わせてuintptrを使用するのが適切です。

整数オーバーフロー

整数オーバーフローは、計算結果がそのデータ型で表現できる最大値を超えた場合に発生します。符号付き整数では、最大値を超えると最小値に戻る(ラップアラウンドする)ことがあり、正の値が負の値として解釈されることがあります。符号なし整数では、最大値を超えると0に戻ります。今回のケースでは、32ビットの符号付き整数として扱われたサイズが、実際には32ビット符号なしの範囲を超えることで、予期せぬ負の値として解釈され、memmoveの内部ロジックが誤動作する原因となりました。

技術的詳細

このバグは、amd64アーキテクチャのmemmoveアセンブリ実装において、コピーするバイト数nをレジスタBXにロードする際に、誤ってMOVLQSX命令を使用していたことに起因します。

元のコード: MOVLQSX n+16(FP), BX

ここでnは、memmove関数の第3引数であり、コピーするバイト数を示します。GoのmemmoveのC言語風のプロトタイプはvoid runtime·memmove(void*, void*, uintptr)であり、nuintptr型として定義されています。しかし、アセンブリコードでMOVLQSXを使用すると、nがたとえuintptrであっても、メモリから読み出される32ビットの値が符号拡張されてBXレジスタ(64ビット)に格納されます。

例えば、n0x1_0000_0000(4GB)のような64ビットの値であった場合、メモリ上では下位32ビットの0x0000_0000と上位32ビットの0x0000_0001に分かれて格納されます。MOVLQSXは下位32ビットのみを読み込み、それを符号拡張して64ビットレジスタに格納します。もしn0x8000_0000(2GB)のような値であった場合、下位32ビットは0x8000_0000となり、これは32ビット符号付き整数としては負の値(-2GB)に相当します。MOVLQSXはこの0x8000_0000を符号拡張し、BXには0xFFFFFFFF_80000000という64ビットの負の値が格納されてしまいます。

memmoveの内部ロジックでは、このBXレジスタの値(コピーするバイト数)に基づいてループ回数やオフセットを計算します。負の値が渡されると、ループが無限に実行されたり、不正なメモリアドレスにアクセスしようとしたりする可能性があり、これがクラッシュやデータ破損の原因となります。

修正は、MOVLQSXMOVQに置き換えることで行われました。

修正後のコード: MOVQ n+16(FP), BX

MOVQ命令は、メモリから64ビットの値を直接読み出し、それをBXレジスタ(64ビット)に格納します。これにより、nuintptrとして正しく64ビットの値として扱われ、符号拡張による誤った値の解釈がなくなります。結果として、memmoveamd64システム上で最大2^64 - 1バイトのメモリブロックを正しくコピーできるようになります。

また、src/pkg/runtime/runtime.hにおけるruntime·memmoveの関数宣言も、第3引数の型をuint32からuintptrに変更することで、C言語側のプロトタイプとアセンブリ実装の整合性が保たれました。これは、Goの内部的な型システムとC言語のリンケージにおける型の整合性を確保するために重要です。

追加されたテストケースTestMemmoveOverflowは、このバグを具体的に再現し、修正が正しく機能することを確認するために非常に重要です。このテストは、3GBという大きなメモリ領域を確保し、その中でcopy関数(内部でmemmoveを使用する可能性がある)を呼び出すことで、以前の32ビット制限を超えるサイズでのmemmoveの動作を検証します。具体的には、syscall.SYS_MMAPを使用して3GBのメモリをマップし、そのスライスに対してcopy操作を実行し、コピーされたバイト数が期待通りであることを確認します。これにより、修正が大規模なメモリ操作においても堅牢であることを保証します。

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

src/pkg/runtime/memmove_amd64.s

--- a/src/pkg/runtime/memmove_amd64.s
+++ b/src/pkg/runtime/memmove_amd64.s
@@ -23,11 +23,12 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
+// void runtime·memmove(void*, void*, uintptr)
 TEXT runtime·memmove(SB), 7, $0
 
 	MOVQ	to+0(FP), DI
 	MOVQ	fr+8(FP), SI
-	MOVLQSX	n+16(FP), BX
+	MOVQ	n+16(FP), BX
 
 /*
  * check and set for backwards
@@ -38,7 +39,7 @@ TEXT runtime·memmove(SB), 7, $0
 /*
  * forward copy loop
  */
-forward:	
+forward:
 	MOVQ	BX, CX
 	SHRQ	$3, CX
 	ANDQ	$7, BX

src/pkg/runtime/memmove_linux_amd64_test.go (新規追加)

--- /dev/null
+++ b/src/pkg/runtime/memmove_linux_amd64_test.go
@@ -0,0 +1,61 @@
+// Copyright 2013 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package runtime_test
+
+import (
+	"io/ioutil"
+	"os"
+	"reflect"
+	"syscall"
+	"testing"
+	"unsafe"
+)
+
+// TestMemmoveOverflow maps 3GB of memory and calls memmove on
+// the corresponding slice.
+func TestMemmoveOverflow(t *testing.T) {
+	// Create a temporary file.
+	tmp, err := ioutil.TempFile("", "go-memmovetest")
+	if err != nil {
+		t.Fatal(err)
+	}
+	_, err = tmp.Write(make([]byte, 65536))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(tmp.Name())
+	defer tmp.Close()
+
+	// Set up mappings.
+	base, _, errno := syscall.Syscall6(syscall.SYS_MMAP,
+		0xa0<<32, 3<<30, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS, ^uintptr(0), 0)
+	if errno != 0 {
+		t.Skipf("could not create memory mapping: %s", errno)
+	}
+	syscall.Syscall(syscall.SYS_MUNMAP, base, 3<<30, 0)
+
+	for off := uintptr(0); off < 3<<30; off += 65536 {
+		_, _, errno := syscall.Syscall6(syscall.SYS_MMAP,
+			base+off, 65536, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED|syscall.MAP_FIXED, tmp.Fd(), 0)
+		if errno != 0 {
+			t.Fatalf("could not map a page at requested 0x%x: %s", base+off, errno)
+		}
+		defer syscall.Syscall(syscall.SYS_MUNMAP, base+off, 65536, 0)
+	}
+
+	var s []byte
+	sp := (*reflect.SliceHeader)(unsafe.Pointer(&s))
+	sp.Data = base
+	sp.Len, sp.Cap = 3<<30, 3<<30
+
+	n := copy(s[1:], s)
+	if n != 3<<30-1 {
+		t.Fatalf("copied %d bytes, expected %d", n, 3<<30-1)
+	}
+	n = copy(s, s[1:])
+	if n != 3<<30-1 {
+		t.Fatalf("copied %d bytes, expected %d", n, 3<<30-1)
+	}
+}

src/pkg/runtime/runtime.h

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -668,7 +668,7 @@ void	runtime·prints(int8*);
 void	runtime·printf(int8*, ...);
 byte*	runtime·mchr(byte*, byte, byte*);
 int32	runtime·mcmp(byte*, byte*, uint32);
-void	runtime·memmove(void*, void*, uint32);
+void	runtime·memmove(void*, void*, uintptr);
 void*	runtime·mal(uintptr);
 String	runtime·catstring(String, String);
 String	runtime·gostring(byte*);

コアとなるコードの解説

src/pkg/runtime/memmove_amd64.s

  • MOVLQSX n+16(FP), BX から MOVQ n+16(FP), BX への変更:
    • この行は、memmove関数の第3引数であるコピーサイズnを、スタックフレームポインタFPからのオフセット+16の位置からレジスタBXにロードする部分です。
    • 元のMOVLQSX命令は、メモリから32ビットの値を読み込み、それを符号拡張して64ビットのBXレジスタに格納していました。これにより、nが32ビット符号付き整数の最大値(約2GB)を超えると、負の値として解釈される問題が発生していました。
    • 新しいMOVQ命令は、メモリから直接64ビットの値を読み込み、それをBXレジスタに格納します。これにより、nuintptrとして正しく64ビットのサイズ情報として扱われ、整数オーバーフローが解消されます。

src/pkg/runtime/memmove_linux_amd64_test.go

  • TestMemmoveOverflow関数の追加:
    • このテストは、memmoveの整数オーバーフローバグを再現し、修正が正しく機能することを確認するために特別に設計されました。
    • メモリマッピング: syscall.Syscall6(syscall.SYS_MMAP, ...)を使用して、3GBという非常に大きなメモリ領域を確保しています。これは、従来の32ビット制限(約2GB)を超えるサイズを意図的に扱うためです。
    • copy関数の使用: Goの組み込み関数であるcopyは、内部的にruntime.memmoveを使用する可能性があります。このテストでは、確保した3GBのスライスsに対してcopy(s[1:], s)copy(s, s[1:])を実行しています。これにより、memmoveが重なり合うメモリ領域で大きなサイズをコピーする際の動作を検証します。
    • 検証: copyが返したコピーされたバイト数nが、期待される3<<30-1(3GB - 1バイト)と一致するかどうかを確認しています。これにより、memmoveが正確に指定されたバイト数をコピーできたかどうかが検証されます。
    • このテストの存在は、修正が単に理論的なものではなく、実際の大きなメモリ操作シナリオで機能することを保証するものです。

src/pkg/runtime/runtime.h

  • void runtime·memmove(void*, void*, uint32); から void runtime·memmove(void*, void*, uintptr); への変更:
    • この変更は、runtime.hにおけるruntime·memmove関数のC言語風のプロトタイプ宣言を更新するものです。
    • 元の宣言では、第3引数(サイズ)がuint32として定義されていました。これは、amd64システムで64ビットのサイズを扱うには不十分でした。
    • 新しい宣言では、第3引数をuintptrに変更しています。uintptrはシステム依存のポインタサイズを持つ符号なし整数型であるため、64ビットシステムでは64ビットのサイズを正しく表現できます。
    • この変更により、C言語のヘッダとアセンブリ実装の間で、memmoveの引数型に関する整合性が保たれ、コンパイラやリンカが正しい型情報に基づいてコードを生成できるようになります。

これらの変更は、Goランタイムのmemmove関数がamd64アーキテクチャの全能力を最大限に活用し、大規模なメモリ操作においても堅牢で正確な動作を保証するために不可欠でした。

関連リンク

参考にした情報源リンク

  • Go Issue #4981の議論内容
  • amd64アセンブリ命令 MOVQ および MOVLQSX に関するドキュメント
  • Go言語のuintptr型に関するドキュメント
  • C言語のmemmove関数に関するドキュメント
  • 整数オーバーフローに関する一般的な情報