[インデックス 15650] ファイルの概要
このコミットは、Goランタイムのamd64
アーキテクチャにおけるmemmove
関数の整数オーバーフローのバグを修正するものです。具体的には、memmove
が処理できるメモリブロックのサイズがuint32
に制限されていたため、32ビットを超える大きなサイズを扱う際に問題が発生していました。この修正により、memmove
はuintptr
(ポインタサイズに合わせた符号なし整数型)を使用するようになり、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プログラム全体の安定性と信頼性に関わる問題でした。そのため、memmove
がamd64
の能力を最大限に活用し、64ビットのサイズを正しく扱えるように修正する必要がありました。
前提知識の解説
memmove
関数
memmove
は、C言語の標準ライブラリ関数の一つで、メモリブロックをコピーするために使用されます。memcpy
と似ていますが、memmove
はコピー元とコピー先のメモリ領域が重なっている場合でも正しく動作するという重要な違いがあります。これは、コピー操作を順方向または逆方向に行うことで実現されます。Goランタイムのmemmove
も同様の機能を提供し、Goの内部的なメモリ管理やスライス操作などで利用されます。
amd64
アーキテクチャ
amd64
(またはx86-64)は、64ビットの汎用レジスタと64ビットのアドレス空間を持つCPUアーキテクチャです。これにより、従来の32ビットアーキテクチャよりもはるかに大きなメモリ(理論上は16エクサバイト)を直接アドレス指定できるようになります。Goは、この64ビットの能力を最大限に活用するように設計されています。
アセンブリ命令: MOVQ
と MOVLQSX
-
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)として扱われてしまいます。
uintptr
と uint32
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)
であり、n
はuintptr
型として定義されています。しかし、アセンブリコードでMOVLQSX
を使用すると、n
がたとえuintptr
であっても、メモリから読み出される32ビットの値が符号拡張されてBX
レジスタ(64ビット)に格納されます。
例えば、n
が0x1_0000_0000
(4GB)のような64ビットの値であった場合、メモリ上では下位32ビットの0x0000_0000
と上位32ビットの0x0000_0001
に分かれて格納されます。MOVLQSX
は下位32ビットのみを読み込み、それを符号拡張して64ビットレジスタに格納します。もしn
が0x8000_0000
(2GB)のような値であった場合、下位32ビットは0x8000_0000
となり、これは32ビット符号付き整数としては負の値(-2GB)に相当します。MOVLQSX
はこの0x8000_0000
を符号拡張し、BX
には0xFFFFFFFF_80000000
という64ビットの負の値が格納されてしまいます。
memmove
の内部ロジックでは、このBX
レジスタの値(コピーするバイト数)に基づいてループ回数やオフセットを計算します。負の値が渡されると、ループが無限に実行されたり、不正なメモリアドレスにアクセスしようとしたりする可能性があり、これがクラッシュやデータ破損の原因となります。
修正は、MOVLQSX
をMOVQ
に置き換えることで行われました。
修正後のコード:
MOVQ n+16(FP), BX
MOVQ
命令は、メモリから64ビットの値を直接読み出し、それをBX
レジスタ(64ビット)に格納します。これにより、n
がuintptr
として正しく64ビットの値として扱われ、符号拡張による誤った値の解釈がなくなります。結果として、memmove
はamd64
システム上で最大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
レジスタに格納します。これにより、n
がuintptr
として正しく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: https://github.com/golang/go/issues/4981
- Go CL 7474047: https://golang.org/cl/7474047
参考にした情報源リンク
- Go Issue #4981の議論内容
amd64
アセンブリ命令MOVQ
およびMOVLQSX
に関するドキュメント- Go言語の
uintptr
型に関するドキュメント - C言語の
memmove
関数に関するドキュメント - 整数オーバーフローに関する一般的な情報