[インデックス 15892] ファイルの概要
このコミットは、Go言語のランタイムにおけるメモリマップ(mmap)の戻り値チェックを改善するものです。具体的には、NetBSDおよびOpenBSDシステムコールにおけるmmapの戻り値の検証を強化し、ENOMEM(メモリ不足)だけでなく、EACCES(アクセス拒否)やEINVAL(無効な引数)といった他のエラーも適切に捕捉できるように変更されています。
コミット
commit 28f65bf4a2ca23701f3c24c866b02bc473c0dd1e
Author: Joel Sing <jsing@google.com>
Date: Sat Mar 23 02:15:52 2013 +1100
runtime: improve mmap return value checking for netbsd/openbsd
Rather than just checking for ENOMEM, check for a return value of less
than 4096, so that we catch other errors such as EACCES and EINVAL.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7942043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/28f65bf4a2ca23701f3c24c866b02bc473c0dd1e
元コミット内容
runtime: improve mmap return value checking for netbsd/openbsd
Rather than just checking for ENOMEM, check for a return value of less
than 4096, so that we catch other errors such as EACCES and EINVAL.
変更の背景
Go言語のランタイムは、オペレーティングシステム(OS)からメモリを確保するためにmmapシステムコールを使用します。以前の実装では、NetBSDおよびOpenBSD環境において、mmapが失敗した場合のチェックがENOMEM(メモリ不足)エラーに限定されていました。しかし、mmapはメモリ不足以外にも、アクセス権の問題(EACCES)や不正な引数(EINVAL)など、様々な理由で失敗する可能性があります。
このコミットの背景には、これらの他のエラーケースが適切に処理されず、ランタイムが予期せぬ動作をする可能性があったという問題意識があります。mmapが成功した場合、通常はページ境界にアラインされた有効なアドレスを返しますが、失敗した場合はエラーを示す特定の値を返します。多くのシステムコールと同様に、mmapもエラー時には-1を返し、errnoにエラーコードを設定します。しかし、mmapの戻り値はポインタ型であるため、エラーを示す値がnil(Goにおけるnilポインタ)や特定の整数値として扱われることがあります。
この変更は、ENOMEM以外のエラーも捕捉することで、ランタイムの堅牢性を高め、より安定したメモリ管理を実現することを目的としています。
前提知識の解説
mmapシステムコール
mmap(memory map)は、Unix系OSで利用されるシステムコールで、ファイルやデバイスをプロセスのアドレス空間にマッピングするために使用されます。これにより、ファイルの内容をメモリとして直接アクセスできるようになり、I/O操作の効率化やプロセス間通信(IPC)などに利用されます。
mmapの主な引数は以下の通りです。
addr: マッピングを開始するアドレスのヒント。通常はNULLを指定し、OSに適切なアドレスを選択させます。len: マッピングするバイト数。prot: メモリ領域の保護(パーミッション)。PROT_NONE: アクセス不可。PROT_READ: 読み取り可能。PROT_WRITE: 書き込み可能。PROT_EXEC: 実行可能。
flags: マッピングの動作を制御するフラグ。MAP_ANON(またはMAP_ANONYMOUS): ファイルではなく、ゼロで初期化された匿名メモリ領域をマッピングします。これは、ヒープやスタックなどの動的メモリ割り当てによく使用されます。MAP_PRIVATE: マッピングがプライベートであり、書き込み時にコピーオンライト(Copy-on-Write)が発生します。元のファイルや他のプロセスには変更が反映されません。MAP_SHARED: マッピングが共有され、変更が元のファイルや他のプロセスに反映されます。
fd: マッピングするファイルのファイルディスクリプタ。MAP_ANONの場合は-1。offset: ファイル内のマッピング開始位置のオフセット。MAP_ANONの場合は0。
mmapは成功するとマッピングされた領域の開始アドレスを返し、失敗するとMAP_FAILED(通常は(void*)-1)を返し、errnoにエラーコードを設定します。
エラーコード
Unix系OSでは、システムコールが失敗した場合にグローバル変数errnoにエラーコードが設定されます。このコミットで言及されているエラーコードは以下の通りです。
ENOMEM(No memory): システムが要求されたメモリを割り当てることができない場合に発生します。これは、物理メモリやスワップ領域が不足している場合、またはプロセスが利用可能な仮想アドレス空間の制限に達した場合に起こり得ます。EACCES(Permission denied): アクセス権がない場合に発生します。例えば、読み取り専用のファイルに書き込みマッピングを試みた場合や、セキュリティポリシーによってメモリ領域へのアクセスが拒否された場合などです。EINVAL(Invalid argument): システムコールに無効な引数が渡された場合に発生します。例えば、mmapのlen引数に負の値を指定したり、offsetがページサイズのアラインメントに違反したりした場合などです。
runtime.SysReserve
Go言語のランタイムには、OSからメモリを予約するためのruntime.SysReserveという関数があります。これは、Goのガベージコレクタ(GC)が管理するヒープ領域を確保する際に使用されます。SysReserveは、実際にメモリを使用する前に、将来使用する可能性のある大きな仮想アドレス空間を予約する役割を担います。この予約は、物理メモリをすぐに割り当てるわけではなく、アドレス空間を確保するだけです。これにより、GCは連続した大きなメモリ領域を効率的に管理できます。
技術的詳細
mmapシステムコールは、成功時にはマッピングされたメモリ領域の開始アドレスを返します。このアドレスは、通常、システムのページサイズ(多くのシステムで4096バイト、つまり4KB)の倍数にアラインされます。つまり、有効なアドレスは0x1000、0x2000などのように、4096以上の値になります。
一方、mmapが失敗した場合、標準的にはMAP_FAILEDという特殊な値を返します。このMAP_FAILEDは、通常、(void*)-1として定義されています。C言語では、ポインタを整数にキャストすると、この-1は符号なし整数として非常に大きな値(例えば0xFFFFFFFFや0xFFFFFFFFFFFFFFFF)になります。しかし、Goのランタイムコードでは、mmapの戻り値がvoid*として扱われ、それを直接ENOMEMと比較していました。
問題は、ENOMEMが通常、小さな正の整数値(例えば12)であることです。したがって、p == (void*)ENOMEMという比較は、mmapが実際にENOMEMエラーで失敗した場合でも、ポインタpがたまたまENOMEMの値と同じアドレスを指すことは極めて稀であるため、ほとんどの場合にfalseとなります。これは、mmapがエラー時に返すMAP_FAILEDの値とは異なるため、ENOMEM以外のエラーはもちろん、ENOMEMエラー自体も適切に捕捉できていなかったことを意味します。
このコミットでは、この問題を解決するために、p == (void*)ENOMEMという特定のerrno値との比較を、p < (void*)4096というより一般的なチェックに変更しています。
なぜ4096なのか?
- ページサイズ: 多くのシステムでは、メモリのページサイズが4096バイト(4KB)です。
mmapが成功した場合に返すアドレスは、このページサイズの倍数にアラインされます。したがって、有効なメモリ領域の開始アドレスは、0(NULLポインタ)を除いて、常に4096以上の値になります。 - エラー値の特性:
mmapが失敗した場合に返すMAP_FAILED(通常は(void*)-1)は、ポインタとして解釈すると、非常に大きな値になります。しかし、一部のシステムや特定の状況下では、mmapがエラーを示すために、0から4095の範囲内の小さな非NULLポインタ値を返す可能性も理論上は考えられます(これは非常に稀ですが、堅牢なチェックのためには考慮すべきです)。 - 堅牢なチェック:
p < (void*)4096という条件は、mmapが成功して有効なメモリ領域を返した場合には常にfalseとなり、mmapが失敗してMAP_FAILED((void*)-1)や、もし仮に0から4095の範囲内のエラー値が返された場合にもtrueとなります。これにより、ENOMEMだけでなく、EACCESやEINVALなど、mmapが失敗する可能性のある全てのエラーケースを、errnoの具体的な値に依存せずに捕捉できるようになります。
この変更は、mmapの戻り値がポインタ型であることと、OSがエラーを示すために返す値の特性を考慮した、より汎用的で堅牢なエラーチェックメカニズムを導入しています。
コアとなるコードの変更箇所
変更は、src/pkg/runtime/mem_netbsd.cとsrc/pkg/runtime/mem_openbsd.cの2つのファイルで行われています。両ファイルで同じ変更が適用されています。
--- a/src/pkg/runtime/mem_netbsd.c
+++ b/src/pkg/runtime/mem_netbsd.c
@@ -50,7 +50,7 @@ runtime·SysReserve(void *v, uintptr n)
return v;
p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
- if(p == (void*)ENOMEM)
+ if(p < (void*)4096)
return nil;
return p;
}
--- a/src/pkg/runtime/mem_openbsd.c
+++ b/src/pkg/runtime/mem_openbsd.c
@@ -50,7 +50,7 @@ runtime·SysReserve(void *v, uintptr n)
return v;
p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
- if(p == (void*)ENOMEM)
+ if(p < (void*)4096)
return nil;
return p;
}
コアとなるコードの解説
変更されたコードは、runtime·SysReserve関数内にあります。この関数は、GoランタイムがOSからメモリを予約する際に呼び出されます。
元のコードでは、runtime·mmapの呼び出し後、戻り値pがENOMEMと等しいかどうかをチェックしていました。
if(p == (void*)ENOMEM)
これは、前述の通り、ENOMEMが通常小さな整数値であり、mmapがエラー時に返すMAP_FAILED(通常は(void*)-1)とは異なるため、不適切なチェックでした。
新しいコードでは、この条件が以下のように変更されています。
if(p < (void*)4096)
この変更により、mmapが返すポインタpが、0(nil)または0から4095の範囲内の値である場合にtrueとなります。
mmapが成功した場合、pは通常4096以上の有効なアドレスを指すため、この条件はfalseとなり、pが返されます。mmapが失敗した場合、pはMAP_FAILED((void*)-1)となることが多く、これを符号なしポインタとして比較すると非常に大きな値になりますが、C言語のポインタ比較のセマンティクスや、一部のシステムでエラーを示すために小さなポインタ値が返される可能性を考慮すると、p < (void*)4096というチェックは、MAP_FAILEDを含む全てのエラーケースをより確実に捕捉できます。特に、nilポインタ(0)が返された場合もtrueとなり、nilが返されるべき状況で適切に処理されます。
この変更によって、ENOMEMだけでなく、EACCESやEINVALなど、mmapが失敗する可能性のある全てのエラーが、より汎用的な方法で捕捉され、runtime·SysReserve関数がnilを返すことで、Goランタイムがメモリ予約の失敗を適切に処理できるようになります。
関連リンク
- Go issue: runtime: improve mmap return value checking for netbsd/openbsd (このコミットに関連するGoのIssueが見つかる可能性がありますが、直接的なIssue番号はコミットメッセージにはありません。CLのリンクから辿るのが確実です。)
- golang/go commit 28f65bf4a2ca23701f3c24c866b02bc473c0dd1e on GitHub
参考にした情報源リンク
- mmap(2) - Linux man page
- errno(3) - Linux man page
- Go runtime source code (relevant files: mem_netbsd.c, mem_openbsd.c)
- Go CL 7942043 (コミットメッセージに記載されているChangeListのURL)
- Understanding Go's Memory Allocator (Goのメモリ管理に関する一般的な情報源)
- Go's runtime memory allocation (Goのランタイムメモリ割り当てに関する詳細な記事)
- What is the significance of 4096 in memory allocation? (4096バイトがメモリ割り当てにおいてなぜ重要かについてのStack Overflowの議論)
- Why does mmap return -1 on error? (mmapがエラー時に-1を返す理由についてのStack Overflowの議論)
- Casting (void*)-1 to a pointer type (C言語で
(void*)-1をポインタ型にキャストすることに関するStack Overflowの議論) - Go runtime: SysReserve (Goのソースコード検索、
SysReserveの定義) - Go runtime: mmap (Goのソースコード検索、
mmapの定義)