[インデックス 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
の定義)