[インデックス 18355] ファイルの概要
このコミットは、Goランタイムにおけるstringtoslicerune
関数でのバッファオーバーフローの脆弱性を修正するものです。特に32ビットシステムにおいて、メモリ割り当てサイズの計算時に発生しうる整数オーバーフローが原因で、mallocgc
が誤って0バイトを割り当ててしまう問題に対処しています。
コミット
commit e1a91c5b8963e3e02c897f96218d4eae17bcb740
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jan 27 20:29:21 2014 +0400
runtime: fix buffer overflow in stringtoslicerune
On 32-bits n*sizeof(r[0]) can overflow.
Or it can become 1<<32-eps, and mallocgc will "successfully"
allocate 0 pages for it, there are no checks downstream
and MHeap_Grow just does:
npage = (npage+15)&~15;
ask = npage<<PageShift;
LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews
https://golang.org/cl/54760045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e1a91c5b8963e3e02c897f96218d4eae17bcb740
元コミット内容
Goランタイムのstringtoslicerune
関数において、32ビット環境でn*sizeof(r[0])
(ルーンのスライスに必要なメモリサイズ)の計算がオーバーフローする可能性がありました。このオーバーフローが発生すると、計算結果が非常に小さい値(例えば1<<32-eps
のような値)になり、mallocgc
が「成功裏に」0ページ分のメモリを割り当ててしまうことがありました。その後の処理では、この不適切な割り当てに対するチェックがなく、MHeap_Grow
のような関数が、割り当てられたページ数に基づいてメモリを要求する際に、実質的に0バイトのメモリに対して書き込みが行われ、結果としてバッファオーバーフローを引き起こす可能性がありました。
変更の背景
この変更は、Goランタイムにおける潜在的なセキュリティ脆弱性および安定性の問題に対処するために行われました。特に、ユーザーからの入力や、非常に長い文字列をルーンのスライスに変換する際に、メモリ割り当ての計算が整数オーバーフローを起こし、結果としてヒープの破損やサービス拒否(DoS)攻撃につながる可能性がありました。
Goのランタイムは、メモリ管理において非常に効率的かつ安全であることを目指していますが、低レベルのメモリ操作では、このようなエッジケースでの整数オーバーフローが予期せぬ動作を引き起こすことがあります。このコミットは、このような潜在的な問題を未然に防ぎ、ランタイムの堅牢性を向上させることを目的としています。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのコンポーネントです。ガベージコレクション、スケジューリング、メモリ割り当てなど、Go言語の並行処理モデルとメモリ安全性を実現するための重要な機能を提供します。
stringtoslicerune
関数: Goの内部ランタイム関数で、UTF-8エンコードされた文字列を、各ルーン(Unicodeコードポイント)を要素とする[]rune
スライスに変換する際に使用されます。この変換では、文字列内のルーンの数を数え、その数に基づいて必要なメモリを割り当てます。mallocgc
: Goランタイムのガベージコレクタと連携するメモリ割り当て関数です。プログラムがヒープメモリを要求する際に呼び出され、必要なサイズのメモリブロックを割り当てます。- 整数オーバーフロー (Integer Overflow): 整数型変数が表現できる最大値を超えた場合に発生する現象です。例えば、32ビット符号なし整数が
2^32 - 1
を超えると、値が0に戻ったり、予期せぬ小さな値になったりします。このコミットのケースでは、必要なメモリサイズを計算するn*sizeof(r[0])
がオーバーフローし、結果として計算されたサイズが実際の必要量よりもはるかに小さくなることが問題でした。 - バッファオーバーフロー (Buffer Overflow): プログラムが、割り当てられたバッファ(メモリ領域)の境界を超えてデータを書き込もうとしたときに発生する脆弱性です。これにより、隣接するメモリ領域が上書きされ、プログラムのクラッシュ、予期せぬ動作、または悪意のあるコードの実行につながる可能性があります。
PageSize
/PageShift
: メモリ管理におけるページングの概念に関連します。オペレーティングシステムはメモリを固定サイズの「ページ」に分割して管理します。PageSize
は1ページのバイトサイズ(例: 4KB)、PageShift
はそのサイズを2の累乗で表したものです(例: 4KB =2^12
なのでPageShift
は12)。MHeap_Grow
は、これらのページ単位でメモリを要求します。MaxMem
: Goランタイムが利用できる最大メモリ量を示す定数、またはそれに類する概念です。メモリ割り当ての健全性をチェックするために使用されます。
技術的詳細
このコミットが修正する問題は、32ビットシステムにおけるポインタサイズと整数型の限界に起因します。stringtoslicerune
関数が文字列をルーンのスライスに変換する際、必要なメモリサイズはルーンの数 * sizeof(rune)
で計算されます。ここでsizeof(rune)
は通常4バイトです。
32ビットシステムでは、uintptr
(ポインタを保持できる符号なし整数型)やuint32
の最大値は2^32 - 1
です。もしルーンの数が2^32 / 4
(約10億)に近づくと、n * sizeof(r[0])
の計算結果が32ビット整数の最大値を超えてオーバーフローする可能性があります。
オーバーフローが発生すると、例えば0xFFFFFFFF
のような大きな値が0x00000003
のような小さな値にラップアラウンドしてしまうことがあります。この小さな値がmallocgc
に渡されると、mallocgc
は「成功裏に」非常に少ない(あるいは0)ページ分のメモリを割り当ててしまいます。
コミットメッセージにあるMHeap_Grow
の記述は、Goのヒープ管理がページ単位で行われることを示唆しています。
npage = (npage+15)&~15;
ask = npage<<PageShift;
このコードは、要求されたページ数を16の倍数に切り上げ、それをバイト数に変換してメモリを要求する一般的なパターンです。しかし、もしnpage
がオーバーフローによって非常に小さな値になっていた場合、ask
も非常に小さな値になり、結果として割り当てられるメモリが不足します。
この問題は、割り当てられたメモリ領域が実際に必要なサイズよりもはるかに小さいため、後続のルーンのコピー処理で割り当てられたバッファの境界を越えて書き込みが行われ、バッファオーバーフローが発生するというものです。これは、プログラムのクラッシュ、データ破損、または悪意のあるコード実行につながる可能性があります。
コアとなるコードの変更箇所
このコミットでは、以下の2つのファイルにそれぞれ1行ずつ、合計2行のコードが追加されています。
src/pkg/runtime/malloc.goc
--- a/src/pkg/runtime/malloc.goc +++ b/src/pkg/runtime/malloc.goc @@ -224,6 +224,8 @@ largealloc(uint32 flag, uintptr *sizep) // Allocate directly from heap. size = *sizep; + if(size + PageSize < size) + runtime·throw("out of memory"); npages = size >> PageShift; if((size & PageMask) != 0) npages++;
src/pkg/runtime/string.goc
--- a/src/pkg/runtime/string.goc +++ b/src/pkg/runtime/string.goc @@ -334,6 +334,8 @@ func stringtoslicerune(s String) (b Slice) { tn++; } + if(n > MaxMem/sizeof(r[0])) + runtime·throw("out of memory"); mem = runtime·roundupsize(n*sizeof(r[0])); b.array = runtime·mallocgc(mem, 0, FlagNoScan|FlagNoZero);\n \tb.len = n;
コアとなるコードの解説
-
src/pkg/runtime/malloc.goc
の変更点:largealloc
関数は、大きなメモリブロックをヒープから直接割り当てる際に使用されます。 追加された行:if(size + PageSize < size) runtime·throw("out of memory");
このチェックは、size
が非常に大きな値(uintptr
の最大値に近い値)である場合に、size + PageSize
の計算がオーバーフローするかどうかを検出します。もしオーバーフローが発生すると、size + PageSize
の結果はsize
よりも小さくなります(ラップアラウンドするため)。この条件が真になった場合、それはメモリ割り当て要求がシステムで扱える範囲を超えているか、または計算がオーバーフローしていることを意味するため、runtime·throw("out of memory")
を呼び出してパニックを発生させ、プログラムを異常終了させます。これにより、不正なメモリ割り当てを防ぎます。 -
src/pkg/runtime/string.goc
の変更点:stringtoslicerune
関数は、文字列をルーンのスライスに変換する際に、必要なメモリサイズを計算します。 追加された行:if(n > MaxMem/sizeof(r[0])) runtime·throw("out of memory");
このチェックは、ルーンの数n
が、MaxMem / sizeof(r[0])
(システムが割り当て可能な最大メモリ量MaxMem
をルーン1個のサイズで割った値、つまり割り当て可能なルーンの最大数)を超えているかどうかを判断します。もしn
がこの限界を超えている場合、n*sizeof(r[0])
の計算が確実にオーバーフローするか、または割り当て可能なメモリ量を超過することになります。この場合も、runtime·throw("out of memory")
を呼び出してパニックを発生させ、不正なメモリ割り当てやそれに続くバッファオーバーフローを防ぎます。
これらの変更は、メモリ割り当ての計算がオーバーフローする前に、早期にエラーを検出してプログラムを安全に終了させることで、バッファオーバーフローの発生を防ぐ「フェイルファスト」の原則に基づいています。これにより、Goランタイムの堅牢性とセキュリティが向上します。
関連リンク
- Go言語のメモリ管理に関するドキュメント (当時のバージョン): Goの公式ドキュメントやブログ記事で、メモリ管理やガベージコレクションの仕組みについて詳細が説明されています。
- Goのランタイムソースコード:
src/pkg/runtime/
ディレクトリには、Goランタイムのコア部分のソースコードが含まれています。
参考にした情報源リンク
- Goのコミット履歴
- Goのコードレビューシステム (Gerrit) - コミットメッセージに記載されている
https://golang.org/cl/54760045
は、当時のGerritの変更リストへのリンクです。 - Go言語の公式ドキュメント
- Go言語のソースコード
- 一般的な整数オーバーフローとバッファオーバーフローに関するセキュリティ情報源