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

[インデックス 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): システムコールに無効な引数が渡された場合に発生します。例えば、mmaplen引数に負の値を指定したり、offsetがページサイズのアラインメントに違反したりした場合などです。

runtime.SysReserve

Go言語のランタイムには、OSからメモリを予約するためのruntime.SysReserveという関数があります。これは、Goのガベージコレクタ(GC)が管理するヒープ領域を確保する際に使用されます。SysReserveは、実際にメモリを使用する前に、将来使用する可能性のある大きな仮想アドレス空間を予約する役割を担います。この予約は、物理メモリをすぐに割り当てるわけではなく、アドレス空間を確保するだけです。これにより、GCは連続した大きなメモリ領域を効率的に管理できます。

技術的詳細

mmapシステムコールは、成功時にはマッピングされたメモリ領域の開始アドレスを返します。このアドレスは、通常、システムのページサイズ(多くのシステムで4096バイト、つまり4KB)の倍数にアラインされます。つまり、有効なアドレスは0x10000x2000などのように、4096以上の値になります。

一方、mmapが失敗した場合、標準的にはMAP_FAILEDという特殊な値を返します。このMAP_FAILEDは、通常、(void*)-1として定義されています。C言語では、ポインタを整数にキャストすると、この-1は符号なし整数として非常に大きな値(例えば0xFFFFFFFF0xFFFFFFFFFFFFFFFF)になります。しかし、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だけでなく、EACCESEINVALなど、mmapが失敗する可能性のある全てのエラーケースを、errnoの具体的な値に依存せずに捕捉できるようになります。

この変更は、mmapの戻り値がポインタ型であることと、OSがエラーを示すために返す値の特性を考慮した、より汎用的で堅牢なエラーチェックメカニズムを導入しています。

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

変更は、src/pkg/runtime/mem_netbsd.csrc/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の呼び出し後、戻り値pENOMEMと等しいかどうかをチェックしていました。

if(p == (void*)ENOMEM)

これは、前述の通り、ENOMEMが通常小さな整数値であり、mmapがエラー時に返すMAP_FAILED(通常は(void*)-1)とは異なるため、不適切なチェックでした。

新しいコードでは、この条件が以下のように変更されています。

if(p < (void*)4096)

この変更により、mmapが返すポインタpが、0nil)または0から4095の範囲内の値である場合にtrueとなります。

  • mmapが成功した場合、pは通常4096以上の有効なアドレスを指すため、この条件はfalseとなり、pが返されます。
  • mmapが失敗した場合、pMAP_FAILED(void*)-1)となることが多く、これを符号なしポインタとして比較すると非常に大きな値になりますが、C言語のポインタ比較のセマンティクスや、一部のシステムでエラーを示すために小さなポインタ値が返される可能性を考慮すると、p < (void*)4096というチェックは、MAP_FAILEDを含む全てのエラーケースをより確実に捕捉できます。特に、nilポインタ(0)が返された場合もtrueとなり、nilが返されるべき状況で適切に処理されます。

この変更によって、ENOMEMだけでなく、EACCESEINVALなど、mmapが失敗する可能性のある全てのエラーが、より汎用的な方法で捕捉され、runtime·SysReserve関数がnilを返すことで、Goランタイムがメモリ予約の失敗を適切に処理できるようになります。

関連リンク

参考にした情報源リンク