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

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

このコミットは、Goランタイムにおけるmake(chan)関数に存在していたバッファオーバーフローの脆弱性を修正するものです。特に32ビットシステムにおいて、細工されたmake(chan)の引数によって、チャネルのバッファが予期せぬメモリ領域にアクセスできるようになる問題に対処しています。これにより、システム全体のメモリへの不正なアクセスが可能になる潜在的なセキュリティリスクが排除されました。

コミット

runtime: fix buffer overflow in make(chan)
On 32-bits one can arrange make(chan) params so that
the chan buffer gives you access to whole memory.

LGTM=r
R=golang-codereviews, r
CC=bradfitz, golang-codereviews, iant, khr
https://golang.org/cl/50250045

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

https://github.com/golang/go/commit/d409e44cfb7b2a323658f4b6fd6d5bb3a9104889

元コミット内容

runtime: fix buffer overflow in make(chan)
On 32-bits one can arrange make(chan) params so that
the chan buffer gives you access to whole memory.

変更の背景

Go言語のmake(chan)関数は、チャネルを作成するために使用されます。チャネルは、Goルーチン間で値を送受信するためのパイプのようなものです。make(chan Type, capacity)のように、要素の型とオプションでバッファ容量を指定して作成できます。

このコミットが行われた背景には、32ビットシステムにおけるメモリ管理の特性と、make(chan)が内部でメモリを割り当てる際の計算ミスがありました。具体的には、32ビット環境ではアドレス空間が4GBに制限されており、ポインタやサイズを表す整数型が32ビット幅になります。この制限されたアドレス空間において、チャネルのバッファサイズを計算する際にオーバーフローが発生する可能性がありました。

攻撃者がmake(chan)capacity引数を巧妙に操作することで、チャネルのバッファが本来割り当てられるべきサイズよりもはるかに小さいサイズで割り当てられ、その結果、チャネルのバッファがヒープ上の他の重要なデータ構造や、さらにはシステム全体のメモリ領域にまでアクセスできてしまう「バッファオーバーフロー」の状態を引き起こすことができました。これは、任意のメモリ読み書きを可能にする深刻な脆弱性につながる可能性がありました。

この脆弱性は、特に32ビットシステムで顕著であり、MaxMem(システムが利用可能な最大メモリ量)とチャネル構造体自体のサイズを考慮せずにバッファ容量のチェックを行っていたことが原因でした。

前提知識の解説

  • Goチャネル (chan): Goルーチン間で安全にデータをやり取りするための同期プリミティブです。make(chan Type, capacity)で作成され、capacityはチャネルが保持できる要素の数を指定します。capacityが0の場合は非バッファチャネル、正の値の場合はバッファチャネルとなります。
  • バッファオーバーフロー: プログラムが、割り当てられたバッファの境界を超えてデータを書き込もうとしたときに発生する脆弱性です。これにより、隣接するメモリ領域が上書きされ、プログラムのクラッシュ、予期せぬ動作、または悪意のあるコードの実行につながる可能性があります。
  • 32ビットシステムと64ビットシステム:
    • 32ビットシステム: メモリアドレスを32ビットの数値で表現します。これにより、最大で2^32バイト(4GB)のメモリ空間しか直接アドレス指定できません。
    • 64ビットシステム: メモリアドレスを64ビットの数値で表現します。これにより、はるかに広大なメモリ空間(2^64バイト)をアドレス指定できます。
  • MaxMem: Goランタイムが利用できる最大メモリ量を示す内部定数または変数です。これはシステムアーキテクチャやOSによって異なる場合があります。
  • sizeof(*c): C言語の構文で、GoランタイムのCコード内でチャネル構造体自体のサイズをバイト単位で取得するために使用されます。チャネルのバッファとは別に、チャネルのメタデータや内部状態を保持するためのメモリ領域です。
  • elem->size: チャネルが保持する要素の型(Type)のサイズをバイト単位で示します。例えば、make(chan int)の場合、elem->sizeintのサイズ(通常4バイトまたは8バイト)になります。
  • hint: make(chan)に渡されるcapacity引数に対応する内部変数です。これは、チャネルバッファの要素数を表します。

技術的詳細

このコミットの核心は、src/pkg/runtime/chan.cファイル内のruntime·makechan_c関数におけるバッファ容量のチェックロジックの修正です。

元のコードでは、チャネルのバッファ容量hintMaxMem / elem->sizeを超えていないかをチェックしていました。これは、elem->sizeが0でない場合に、hint * elem->sizeMaxMemを超えないようにするための基本的なチェックです。しかし、この計算には重要な考慮事項が欠けていました。それは、チャネルのバッファだけでなく、チャネル構造体自体(sizeof(*c))もメモリを消費するという点です。

32ビットシステムでは、MaxMemの値が4GBに近くなることがあり、hint * elem->sizeの計算がMaxMemの範囲内に収まっていても、チャネル構造体自体のサイズを考慮すると、合計のメモリ要求量がMaxMemを超えてしまう可能性がありました。この場合、メモリ割り当てが失敗するか、あるいは不正なメモリ領域が返されることで、バッファオーバーフローが発生する余地が生まれていました。

修正後のコードでは、この問題を解決するために、MaxMemからチャネル構造体自体のサイズsizeof(*c)を差し引いた値でhintのチェックを行うように変更されました。

具体的には、以下の行が変更されました。

変更前:

if(hint < 0 || (intgo)hint != hint || (elem->size > 0 && hint > MaxMem / elem->size))

変更後:

if(hint < 0 || (intgo)hint != hint || (elem->size > 0 && hint > (MaxMem - sizeof(*c)) / elem->size))

この変更により、チャネルのバッファに割り当てられるメモリとチャネル構造体自体のメモリの合計が、システムが利用可能な最大メモリ量MaxMemを超えないように、より厳密なチェックが行われるようになりました。MaxMem - sizeof(*c)とすることで、チャネルバッファに利用できる実質的な最大メモリ量を正確に計算し、その範囲内でhintが適切であるかを検証しています。これにより、32ビットシステムにおける整数オーバーフローや、それに起因するバッファオーバーフローの脆弱性が効果的に防止されます。

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

--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -104,7 +104,7 @@ runtime·makechan_c(ChanType *t, int64 hint)
 	if((sizeof(*c)%MAXALIGN) != 0 || elem->align > MAXALIGN)
 		runtime·throw("makechan: bad alignment");
 
-	if(hint < 0 || (intgo)hint != hint || (elem->size > 0 && hint > MaxMem / elem->size))
+	if(hint < 0 || (intgo)hint != hint || (elem->size > 0 && hint > (MaxMem - sizeof(*c)) / elem->size))
 		runtime·panicstring("makechan: size out of range");
 
 	// allocate memory in one call

コアとなるコードの解説

変更された行は、runtime·makechan_c関数内で、hint(チャネルのバッファ容量)が有効な範囲内にあるかをチェックする条件式です。

  • hint < 0: hintが負の値でないことを確認します。負の容量は無効です。
  • (intgo)hint != hint: hintintgo型(Goのポインタサイズに合わせた整数型)にキャストしても値が変わらないことを確認します。これは、hintintgoの範囲に収まっているか、つまりオーバーフローしていないかをチェックします。
  • (elem->size > 0 && hint > (MaxMem - sizeof(*c)) / elem->size): この部分が修正の核心です。
    • elem->size > 0: 要素サイズが0より大きい場合(非ゼロサイズの要素を持つチャネルの場合)にのみ、このチェックを行います。要素サイズが0のチャネル(例: chan struct{})はバッファにメモリを必要としないためです。
    • (MaxMem - sizeof(*c)): ここが重要な変更点です。システムが利用可能な最大メモリ量MaxMemから、チャネル構造体自体が消費するメモリ量sizeof(*c)を差し引いています。これにより、チャネルのバッファに実際に割り当て可能な残りのメモリ量を計算しています。
    • / elem->size: 残りのメモリ量を要素サイズで割ることで、そのメモリ量で最大いくつの要素をバッファに格納できるかを計算します。
    • hint > ...: 最終的に、指定されたhintが、計算された最大要素数を超えていないかをチェックします。もし超えている場合、それはメモリ割り当ての範囲外であり、makechan: size out of rangeというパニックを引き起こします。

この修正により、チャネルのバッファとチャネル構造体自体の両方が、利用可能なメモリ空間内に適切に収まることが保証され、特に32ビットシステムでのバッファオーバーフローの可能性が排除されました。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分 (d409e44cfb7b2a323658f4b6fd6d5bb3a9104889)
  • Go言語のチャネルに関する公式ドキュメントやチュートリアル(一般的な知識として)
  • バッファオーバーフローに関する一般的なセキュリティ情報(一般的な知識として)
  • 32ビット/64ビットアーキテクチャのメモリモデルに関する一般的な情報(一般的な知識として)