[インデックス 16184] ファイルの概要
コミット
このコミットは、Goランタイムのメモリ管理における重要な改善を導入しています。具体的には、ガベージコレクタがメモリ領域(スパン)を再利用する際に、そのスパンが以前に「スカベンジ」(OSに解放されたとマークされたが、実際にはまだ物理メモリに残っている可能性のある状態)されていた場合に、そのスパンがゼロクリアされる必要があることを明示的にマークする変更です。これにより、古いデータが誤って再利用されることを防ぎ、メモリの安全性を高めます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9ad236ab7215b406e867028ef295445a2c4b8b5d
元コミット内容
commit 9ad236ab7215b406e867028ef295445a2c4b8b5d
Author: Ian Lance Taylor <iant@golang.org>
Date: Tue Apr 16 09:08:06 2013 -0700
runtime: if span was scavenged, mark it as needing to be zeroed
Update #4979.
R=dvyukov, r, bradfitz
CC=golang-dev
https://golang.org/cl/8697045
---
src/pkg/runtime/mheap.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/src/pkg/runtime/mheap.c b/src/pkg/runtime/mheap.c
index 177f406596..f4fbbee7a4 100644
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -121,6 +121,25 @@ HaveSpan:
s->state = MSpanInUse;
mstats.heap_idle -= s->npages<<PageShift;
mstats.heap_released -= s->npreleased<<PageShift;
+ if(s->npreleased > 0) {
+ // We have called runtime·SysUnused with these pages, and on
+ // Unix systems it called madvise. At this point at least
+ // some BSD-based kernels will return these pages either as
+ // zeros or with the old data. For our caller, the first word
+ // in the page indicates whether the span contains zeros or
+ // not (this word was set when the span was freed by
+ // MCentral_Free or runtime·MCentral_FreeSpan). If the first
+ // page in the span is returned as zeros, and some subsequent
+ // page is returned with the old data, then we will be
+ // returning a span that is assumed to be all zeros, but the
+ // actual data will not be all zeros. Avoid that problem by
+ // explicitly marking the span as not being zeroed, just in
+ // case. The beadbead constant we use here means nothing, it
+ // is just a unique constant not seen elsewhere in the
+ // runtime, as a clue in case it turns up unexpectedly in
+ // memory or in a stack trace.
+ *(uintptr*)(s->start<<PageShift) = (uintptr)0xbeadbeadbeadbeadULL;
+ }
s->npreleased = 0;
if(s->npages > npage) {
変更の背景
この変更は、Goランタイムのメモリ管理における潜在的なデータ整合性の問題を解決するために行われました。Goのガベージコレクタは、不要になったメモリ領域をOSに解放する際に runtime·SysUnused
関数を呼び出します。Unix系システムでは、この関数は通常 madvise
システムコールを使用します。madvise
はOSに対してメモリ領域の利用方法に関するヒントを与えるもので、OSはヒントに基づいて物理メモリを解放したり、ページをスワップアウトしたりします。
問題は、一部のBSD系カーネル(および他のOSでも同様の挙動を示す可能性)において、madvise
で解放されたとマークされたページが、再利用時に必ずしもゼロクリアされて返されるとは限らない点にありました。Goランタイムは、特定のメモリ領域(スパン)が解放された際に、そのスパンの最初のワードにゼロクリアされているかどうかの情報(または、ゼロクリアされていることを前提とした状態)を保持していました。しかし、もしスパンの一部がゼロクリアされずに古いデータが残ったまま返されると、ランタイムがそのスパンを「ゼロクリアされている」と誤って判断し、古いデータが新しいオブジェクトに割り当てられてしまう可能性がありました。これは、セキュリティ上のリスクや、プログラムの予期せぬ動作を引き起こす可能性があります。
このコミットは、このような状況を防ぐために、runtime·SysUnused
によって解放された(スカベンジされた)スパンが再利用される際に、そのスパンの最初のワードに特定の非ゼロ値(0xbeadbeadbeadbeadULL
)を書き込むことで、明示的に「ゼロクリアされていない」状態であることをマークします。これにより、ランタイムはスパンが完全にゼロクリアされていないことを認識し、必要に応じてゼロクリア処理を行うことができます。
なお、コミットメッセージに記載されている Update #4979
については、現在のGoのGitHubリポジトリのIssueトラッカーでは直接関連するIssueが見つかりませんでした。これは、当時のGoのIssueトラッキングシステムが現在とは異なっていたか、または内部的なIssue番号であった可能性が考えられます。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムのメモリ管理に関する基本的な概念を理解しておく必要があります。
- ヒープ (Heap): プログラムが動的にメモリを割り当てる領域です。Goでは、ガベージコレクタがヒープの管理を行います。
- スパン (Span): Goランタイムのメモリ管理における基本的な単位です。ヒープは複数のスパンに分割され、各スパンは連続したページ(通常は4KB)の集まりです。オブジェクトはスパン内に割り当てられます。
- ページ (Page): OSが管理するメモリの最小単位です。通常4KBです。
- ガベージコレクタ (GC): 不要になったメモリを自動的に解放し、再利用可能にするシステムです。GoのGCは並行・世代別・三色マーク&スイープ方式を採用しています。
mheap.c
: Goランタイムのヒープ管理に関するC言語のソースファイルです。メモリの割り当て、解放、スパンの管理など、低レベルなメモリ操作が実装されています。MSpanInUse
: スパンの状態を示す定数の一つで、スパンが現在使用中であることを示します。mstats.heap_idle
: ヒープ内で現在アイドル状態(使用されていない)のメモリ量を示す統計情報です。mstats.heap_released
: OSに解放されたとマークされたメモリ量を示す統計情報です。runtime·SysUnused
: GoランタイムがOSに対して、特定のメモリ領域が不要になったことを通知するために呼び出す関数です。madvise
: Unix系システムコールの一つで、プロセスがメモリ領域をどのように使用するかについてOSに助言(アドバイス)を与えるために使用されます。例えば、MADV_DONTNEED
を指定すると、そのメモリ領域のページはすぐに解放してもよいことをOSに伝えます。- ゼロクリア (Zeroing): メモリ領域の内容をすべてゼロで埋めることです。新しいオブジェクトにメモリを割り当てる際、以前のデータが残っているとセキュリティ上の問題やバグの原因となるため、通常はゼロクリアされます。
uintptr
: Goにおけるポインタを保持できる整数型です。
技術的詳細
このコミットの核心は、src/pkg/runtime/mheap.c
内の MHeap_Alloc
関数(またはそれに相当するメモリ割り当てロジックの一部)にあります。MHeap_Alloc
は、新しいオブジェクトのためにヒープからメモリを割り当てる際に呼び出されます。
変更前の挙動では、スパンが runtime·SysUnused
によってOSに解放されたとマークされた後、そのスパンが再利用される際に、ランタイムはスパンの最初のワードがゼロクリアされていることを前提としていました。しかし、前述の通り、OSによっては madvise
の挙動により、物理メモリ上のデータが完全に消去されない場合がありました。
このコミットでは、s->npreleased > 0
という条件が追加されています。s->npreleased
は、そのスパンが以前にOSに解放された(スカベンジされた)ページを含んでいるかどうかを示すカウンタです。もし s->npreleased
が0より大きい場合、つまりスパンがスカベンジされたページを含んでいる場合、以下の処理が実行されます。
*(uintptr*)(s->start<<PageShift) = (uintptr)0xbeadbeadbeadbeadULL;
この行は、スパンの先頭アドレス(s->start<<PageShift
はスパンの開始アドレスをバイト単位で計算したもの)に、特定の定数 0xbeadbeadbeadbeadULL
を uintptr
型として書き込んでいます。この定数は、コミットメッセージにもあるように「意味のない、ランタイムの他の場所では見られないユニークな定数」であり、デバッグ時にメモリダンプやスタックトレースでこの値が見つかった場合に、それがスカベンジされたがゼロクリアされていないスパンであることを示す「手がかり」として機能します。
この変更の目的は、ランタイムがスパンを再利用する際に、そのスパンが以前にスカベンジされたものであり、かつ完全にゼロクリアされていない可能性があることを明示的に示すことです。これにより、ランタイムはスパンの最初のワードの状態をチェックし、もしこの特殊な値が検出された場合、スパン全体をゼロクリアするなどの適切な処理を行うことができます。これにより、古いデータが新しいオブジェクトに誤って露出するリスクが排除されます。
s->npreleased = 0;
の行は、スパンが再利用されるため、以前に解放されたページ数を示すカウンタをリセットしています。
コアとなるコードの変更箇所
変更は src/pkg/runtime/mheap.c
ファイルの MHeap_Alloc
関数(またはそれに相当するメモリ割り当てロジック)内で行われています。
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -121,6 +121,25 @@ HaveSpan:
s->state = MSpanInUse;
mstats.heap_idle -= s->npages<<PageShift;
mstats.heap_released -= s->npreleased<<PageShift;
+ if(s->npreleased > 0) {
+ // We have called runtime·SysUnused with these pages, and on
+ // Unix systems it called madvise. At this point at least
+ // some BSD-based kernels will return these pages either as
+ // zeros or with the old data. For our caller, the first word
+ // in the page indicates whether the span contains zeros or
+ // not (this word was set when the span was freed by
+ // MCentral_Free or runtime·MCentral_FreeSpan). If the first
+ // page in the span is returned as zeros, and some subsequent
+ // page is returned with the old data, then we will be
+ // returning a span that is assumed to be all zeros, but the
+ // actual data will not be all zeros. Avoid that problem by
+ // explicitly marking the span as not being zeroed, just in
+ // case. The beadbead constant we use here means nothing, it
+ // is just a unique constant not seen elsewhere in the
+ // runtime, as a clue in case it turns up unexpectedly in
+ // memory or in a stack trace.
+ *(uintptr*)(s->start<<PageShift) = (uintptr)0xbeadbeadbeadbeadULL;
+ }
s->npreleased = 0;
if(s->npages > npage) {
コアとなるコードの解説
追加されたコードブロックは以下の通りです。
if(s->npreleased > 0) {
// We have called runtime·SysUnused with these pages, and on
// Unix systems it called madvise. At this point at least
// some BSD-based kernels will return these pages either as
// zeros or with the old data. For our caller, the first word
// in the page indicates whether the span contains zeros or
// not (this word was set when the span was freed by
// MCentral_Free or runtime·MCentral_FreeSpan). If the first
// page in the span is returned as zeros, and some subsequent
// page is returned with the old data, then we will be
// returning a span that is assumed to be all zeros, but the
// actual data will not be all zeros. Avoid that problem by
// explicitly marking the span as not being zeroed, just in
// case. The beadbead constant we use here means nothing, it
// is just a unique constant not seen elsewhere in the
// runtime, as a clue in case it turns up unexpectedly in
// memory or in a stack trace.
*(uintptr*)(s->start<<PageShift) = (uintptr)0xbeadbeadbeadbeadULL;
}
if(s->npreleased > 0)
: この条件は、現在処理しているスパンs
が、以前にOSに解放された(スカベンジされた)ページを含んでいるかどうかをチェックします。npreleased
は、そのスパン内でOSに解放されたとマークされたページ数を表します。- コメントブロック: このコメントは、変更の背景と目的を詳細に説明しています。特に、
runtime·SysUnused
がmadvise
を呼び出すこと、一部のOSがゼロクリアせずに古いデータを返す可能性があること、そしてその結果としてランタイムがスパンをゼロクリアされていると誤解するリスクがあることを明確に述べています。また、この問題を防ぐためにスパンを明示的に「ゼロクリアされていない」とマークする必要があることを強調しています。 *(uintptr*)(s->start<<PageShift) = (uintptr)0xbeadbeadbeadbeadULL;
: これが実際の変更の中心です。s->start<<PageShift
: スパンs
の開始ページ番号s->start
を、バイトアドレスに変換しています。PageShift
はページサイズ(通常4KB)のビットシフト量です。(uintptr*)
: 結果のアドレスをuintptr
型のポインタにキャストしています。これにより、そのアドレスにuintptr
型の値を書き込むことができます。0xbeadbeadbeadbeadULL
: これは、スパンがゼロクリアされていないことを示すために書き込まれるマジックナンバーです。この値自体に特別な意味はなく、単に他のランタイムデータと衝突しないユニークな値として選ばれています。この値がスパンの先頭に存在することで、ランタイムはスパンが完全にゼロクリアされていないことを認識し、適切な処理(例えば、スパン全体をゼロクリアする)を行うことができます。
この変更により、Goランタイムはメモリの再利用時におけるデータ整合性をより確実に保証できるようになります。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Goランタイムのソースコード: https://github.com/golang/go/tree/master/src/runtime
madvise
システムコールに関する情報 (Linux man page): https://man7.org/linux/man-pages/man2/madvise.2.html
参考にした情報源リンク
- Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go Change List (CL) 8697045: https://golang.org/cl/8697045 (このコミットの元となったGoのコードレビューシステム上の変更リスト)
- Goのメモリ管理に関するブログ記事やドキュメント (一般的な情報源):
- "Go's Memory Allocator" by Rick Hudson: https://go.dev/blog/go-memory-allocator
- "Go's garbage collector: in theory and in practice" by Daniel Martí: https://go.dev/blog/go-gc-from-the-ground-up
- "The Go Memory Model": https://go.dev/ref/mem
- BSD系カーネルの
madvise
の挙動に関する一般的な情報 (OSのドキュメントやフォーラムなど)- FreeBSD
madvise
man page: https://www.freebsd.org/cgi/man.cgi?query=madvise&sektion=2 - OpenBSD
madvise
man page: https://man.openbsd.org/madvise.2
- FreeBSD