[インデックス 15813] ファイルの概要
このコミットは、GoランタイムにおけるNetBSDおよびOpenBSD上でのmmap
システムコールの戻り値のチェックに関するバグを修正するものです。具体的には、符号付き整数と符号なし整数の比較の誤りによって発生していたメモリ割り当ての失敗検出の問題を解決し、他のUnix系OS(Darwin, FreeBSD, Linux)と同様のmmap
戻り値処理モデルに統一することで、より堅牢なメモリ管理を実現しています。
コミット
commit 7f50c23e2d18c445bfe790692e998ff91b37ddc2
Author: Joel Sing <jsing@google.com>
Date: Mon Mar 18 12:18:49 2013 +1100
runtime: correct mmap return value checking on netbsd/openbsd
The current SysAlloc implementation suffers from a signed vs unsigned
comparision bug. Since the error code from mmap is negated, the
unsigned comparision of v < 4096 is always false on error. Fix this
by switching to the darwin/freebsd/linux mmap model and leave the mmap
return value unmodified.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7870044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7f50c23e2d18c445bfe790692e998ff91b37ddc2
元コミット内容
runtime: correct mmap return value checking on netbsd/openbsd
The current SysAlloc implementation suffers from a signed vs unsigned
comparision bug. Since the error code from mmap is negated, the
unsigned comparision of v < 4096 is always false on error. Fix this
by switching to the darwin/freebsd/linux mmap model and leave the mmap
return value unmodified.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7870044
変更の背景
この変更の背景には、GoランタイムがNetBSDおよびOpenBSD上でメモリを確保する際に使用するmmap
システムコールの戻り値の処理に潜在的なバグが存在していたことがあります。
従来のSysAlloc
(システムからのメモリ割り当て)の実装では、mmap
がエラーを返した場合、そのエラーコードが負の値として返されることがありました。しかし、Goランタイム内部でのエラーチェックにおいて、この負の値が符号なし整数として扱われ、特定の比較(例: v < 4096
)が行われると、常に偽(false)と評価されてしまうという問題がありました。これは、負の符号付き整数が符号なし整数として解釈されると非常に大きな正の値になるため、比較が意図した通りに機能しない「符号付き vs 符号なし比較バグ」として知られる典型的な問題です。
このバグにより、mmap
が実際にメモリ割り当てに失敗してエラーを返しているにもかかわらず、ランタイムがそれを正常な割り当てと誤認し、結果としてメモリ不足や不正なメモリアクセスといった深刻な問題を引き起こす可能性がありました。
この問題を解決するため、Goランタイムは、Darwin、FreeBSD、Linuxといった他の主要なUnix系OSで採用されているmmap
の戻り値処理モデルに合わせることを決定しました。このモデルでは、mmap
の戻り値を変更せずにそのまま扱い、エラーチェックをより正確に行うことで、プラットフォーム間の挙動の統一と堅牢性の向上を図っています。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念を把握しておく必要があります。
-
mmap
システムコール:mmap
(memory map)は、Unix系オペレーティングシステムで提供されるシステムコールの一つで、ファイルやデバイス、あるいは匿名メモリ領域をプロセスのアドレス空間にマッピングするために使用されます。これにより、ファイルの内容をメモリとして直接アクセスしたり、プロセス間でメモリを共有したり、効率的なメモリ割り当てを行ったりすることが可能になります。mmap
は通常、成功した場合はマッピングされたメモリ領域の開始アドレス(ポインタ)を返し、失敗した場合はMAP_FAILED
という特殊な値(通常は(void *)-1
)を返します。エラーの具体的な原因は、errno
変数に設定されます。 -
ENOMEM
:ENOMEM
は、Unix系システムで定義されているエラーコードの一つで、"Out of memory"(メモリ不足)を意味します。mmap
システムコールがメモリ割り当てに失敗した場合など、システムが要求されたメモリを供給できない場合に設定されることがあります。C言語の標準ライブラリでは、通常、errno.h
で定義されており、正の整数値です。 -
符号付き整数と符号なし整数: コンピュータの数値表現において、整数は「符号付き(signed)」と「符号なし(unsigned)」の2種類があります。
- 符号付き整数: 正の値、負の値、ゼロを表現できます。最上位ビット(MSB)が符号ビットとして使われ、0なら正、1なら負(通常は2の補数表現)を示します。
- 符号なし整数: 負の値を表現できず、ゼロと正の値のみを表現します。すべてのビットが数値の大きさを表すために使われるため、同じビット幅であれば符号付き整数よりも表現できる正の数の範囲が広くなります。 問題は、これらの型が混在する演算や比較において発生します。C言語などでは、符号付きと符号なしのオペランドが混在する場合、符号付きオペランドが符号なしに昇格(型変換)されるルールがあります。この際、負の符号付き整数は非常に大きな正の符号なし整数として解釈されるため、意図しない比較結果を生むことがあります。
-
Goランタイムのメモリ管理: Go言語は独自のランタイムを持っており、ガベージコレクションやスケジューラ、そしてメモリ管理を自身で行います。Goランタイムは、OSから大きなメモリブロックを
mmap
などで確保し、それを自身のヒープとして管理し、Goプログラムのオブジェクトに割り当てます。このため、mmap
の挙動はGoランタイムの安定性とパフォーマンスに直結します。 -
アセンブリ言語とシステムコール: Goランタイムの低レベルな部分、特にOSとのインタフェース部分は、C言語やアセンブリ言語で実装されています。システムコールを直接呼び出す部分は、通常、アセンブリ言語で記述されます。このコミットで変更されている
.s
ファイルは、アセンブリ言語で書かれたシステムコールラッパーです。
技術的詳細
このコミットの技術的な核心は、NetBSDおよびOpenBSDにおけるmmap
システムコールの戻り値の処理方法の変更にあります。
従来のGoランタイムの実装では、NetBSDおよびOpenBSDのmmap
システムコールがエラーを返した場合、そのエラーコード(通常は負の値)をアセンブリレベルで正の値に変換(符号を反転)していました。これは、一部のシステムコールがエラー時に負の値を返す慣習に合わせたものかもしれません。
しかし、GoランタイムのCコード(mem_netbsd.c
、mem_openbsd.c
)では、mmap
の戻り値(ポインタ型)をENOMEM
(正の整数値)と比較していました。ここで問題が発生します。
- アセンブリでのエラーコードの反転:
mmap
がエラー(例:-ENOMEM
)を返すと、アセンブリコードでNEG
命令などを使ってその値を正に反転させていました。例えば、-12
(ENOMEM
が12の場合)が12
になります。 - Cコードでの比較: Cコードでは、
p == (void*)-ENOMEM
のような比較が行われていました。ここで、(void*)-ENOMEM
は、ENOMEM
の値を負にしたものをポインタ型にキャストしたものです。- もしアセンブリがエラーコードを反転させていなかった場合、
mmap
が-ENOMEM
を返せば、この比較は正しく機能します。 - しかし、アセンブリがエラーコードを反転させて
ENOMEM
(正の値)を返していた場合、p
は正の値、比較対象は負の値となり、この比較は常に偽となります。
- もしアセンブリがエラーコードを反転させていなかった場合、
- 符号付き vs 符号なしのバグ: コミットメッセージにある「unsigned comparision of v < 4096 is always false on error」という記述は、この問題の別の側面を示唆しています。
mmap
が返すアドレスはポインタ型であり、これは通常符号なし整数として扱われます。エラーを示す特殊な値(例:(void*)-1
やMAP_FAILED
)が、符号付き整数として解釈された後に符号なし整数と比較されると、負の値が非常に大きな正の値として扱われるため、v < 4096
のような比較が常に偽になるという状況が発生します。これは、mmap
が返すエラーを示すポインタ値が、実際には有効なアドレス範囲外の負の値であるにもかかわらず、符号なしとして解釈されることで、非常に大きな正のアドレスと見なされてしまうためです。
このコミットでは、この問題を解決するために以下の変更を行っています。
- アセンブリコードからのエラーコード反転の削除:
sys_netbsd_386.s
、sys_netbsd_amd64.s
、sys_openbsd_386.s
、sys_openbsd_amd64.s
の各ファイルから、mmap
システムコールの戻り値(AX
レジスタ)を反転させる命令(NEGL AX
やNEGQ AX
)と、条件分岐命令(JCC 2(PC)
)が削除されました。これにより、mmap
が返す生の値がそのままGoランタイムのCコードに渡されるようになります。 - Cコードでのエラーチェックの修正:
mem_netbsd.c
とmem_openbsd.c
では、mmap
の戻り値p
とENOMEM
の比較がp == ((void *)-ENOMEM)
からp == (void*)ENOMEM
に変更されました。- アセンブリがエラーコードを反転しなくなったため、
mmap
がエラー時に返す値は、OSが定義するエラーを示すポインタ値(通常は(void*)-1
またはMAP_FAILED
)となります。 - Goランタイムは、Darwin/FreeBSD/Linuxモデルに倣い、
mmap
がエラー時に返す値がnil
(Goのnil
ポインタ)または特定のMAP_FAILED
に相当する値であるかをチェックするように変更されました。 - 特に、
p == (void*)ENOMEM
という比較は、mmap
がエラー時にENOMEM
という正の整数値をポインタとして返すと仮定しているように見えますが、これは一般的なmmap
の挙動とは異なります。通常、mmap
はエラー時にMAP_FAILED
を返し、errno
にENOMEM
を設定します。この変更は、Goランタイムがmmap
の戻り値をどのように解釈するかという内部的な規約の変更を反映していると考えられます。コミットメッセージの「leave the mmap return value unmodified」という記述と合わせて考えると、mmap
がエラー時に返すポインタ値が、Goランタイムの内部でENOMEM
と等価な値として扱われるようになった、あるいは、mmap
がエラー時に返すポインタ値が、ENOMEM
をポインタにキャストした値と一致する場合があるという、特定のOS(NetBSD/OpenBSD)におけるmmap
の実装の詳細に依存した修正である可能性が高いです。
- アセンブリがエラーコードを反転しなくなったため、
この修正により、mmap
が実際にメモリ割り当てに失敗した場合に、Goランタイムがそのエラーを正しく検出し、適切なエラーハンドリング(例: runtime·throw("runtime: out of memory")
)を実行できるようになりました。
コアとなるコードの変更箇所
このコミットでは、以下の6つのファイルが変更されています。
src/pkg/runtime/mem_netbsd.c
src/pkg/runtime/mem_openbsd.c
src/pkg/runtime/sys_netbsd_386.s
src/pkg/runtime/sys_netbsd_amd64.s
src/pkg/runtime/sys_openbsd_386.s
src/pkg/runtime/sys_openbsd_amd64.s
変更の概要は以下の通りです。
-
mem_netbsd.c
およびmem_openbsd.c
:runtime·SysReserve
関数とruntime·SysMap
関数内のmmap
呼び出し後のエラーチェックロジックが変更されました。if (p == ((void *)-ENOMEM))
がif(p == (void*)ENOMEM)
に変更されました。runtime·SysReserve
関数で、成功時のelse return p;
が削除され、単にreturn p;
となりました。
-
sys_netbsd_386.s
、sys_netbsd_amd64.s
、sys_openbsd_386.s
、sys_openbsd_amd64.s
:- 各アセンブリファイル内の
runtime·mmap
関数から、mmap
システムコールの戻り値(AX
レジスタ)を反転させる命令(NEGL AX
またはNEGQ AX
)と、その前の条件分岐命令(JCC 2(PC)
)が削除されました。
- 各アセンブリファイル内の
コアとなるコードの解説
src/pkg/runtime/mem_netbsd.c
および src/pkg/runtime/mem_openbsd.c
これらのファイルは、NetBSDおよびOpenBSDにおけるGoランタイムのメモリ管理(SysReserve
とSysMap
)を担当するCコードです。
変更前:
// runtime·SysReserve 関数内
p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
if (p == ((void *)-ENOMEM)) // ここが問題
return nil;
else
return p;
// runtime·SysMap 関数内 (64-bitの場合)
p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
if(p == (void*)-ENOMEM) // ここが問題
runtime·throw("runtime: out of memory");
// runtime·SysMap 関数内 (共通部分)
p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
if(p == (void*)-ENOMEM) // ここが問題
runtime·throw("runtime: out of memory");
変更後:
// runtime·SysReserve 関数内
p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
if(p == (void*)ENOMEM) // 修正後
return nil;
return p; // elseが削除され、直接return p;
// runtime·SysMap 関数内 (64-bitの場合)
p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
if(p == (void*)ENOMEM) // 修正後
runtime·throw("runtime: out of memory");
// runtime·SysMap 関数内 (共通部分)
p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
if(p == (void*)ENOMEM) // 修正後
runtime·throw("runtime: out of memory");
この変更のポイントは、mmap
の戻り値p
とENOMEM
の比較方法です。
変更前は((void *)-ENOMEM)
、つまりENOMEM
の負の値をポインタにキャストしたものと比較していました。これは、アセンブリコードがエラーコードを負の値として返すと期待していたためです。
しかし、アセンブリコードがエラーコードを正の値に反転させていたため、この比較は常に偽となり、エラーが正しく検出されませんでした。
変更後は((void *)ENOMEM)
、つまりENOMEM
の正の値をポインタにキャストしたものと比較しています。これは、アセンブリコードがエラーコードを反転させなくなったことと整合性が取れています。
また、runtime·SysReserve
関数では、成功時のelse return p;
が削除され、単にreturn p;
となっています。これは、if
ブロック内でreturn nil;
が実行されなければ、その後の行でreturn p;
が実行されるため、冗長なelse
を削除したものです。
src/pkg/runtime/sys_netbsd_386.s
、src/pkg/runtime/sys_netbsd_amd64.s
、src/pkg/runtime/sys_openbsd_386.s
、src/pkg/runtime/sys_openbsd_amd64.s
これらのファイルは、各アーキテクチャ(386, amd64)とOS(NetBSD, OpenBSD)におけるmmap
システムコールのラッパーを定義するアセンブリコードです。
変更前(例: sys_netbsd_386.s
):
TEXT runtime·mmap(SB),7,$36
// ... (mmapシステムコール呼び出しのための準備)
MOVL $197, AX // sys_mmap
INT $0x80 // システムコール実行
JCC 2(PC) // キャリーフラグがセットされていなければ(エラーでなければ)次の命令をスキップ
NEGL AX // エラーの場合、AX(戻り値)の符号を反転
RET // 関数から戻る
変更後(例: sys_netbsd_386.s
):
TEXT runtime·mmap(SB),7,$36
// ... (mmapシステムコール呼び出しのための準備)
MOVL $197, AX // sys_mmap
INT $0x80 // システムコール実行
// JCC 2(PC) と NEGL AX が削除された
RET // 関数から戻る
このアセンブリコードの変更が、このコミットの最も重要な部分です。
変更前は、mmap
システムコールがエラーを返した場合(通常、AX
レジスタに負の値が設定され、キャリーフラグがセットされる)、JCC
命令がスキップされ、NEGL AX
命令によってAX
レジスタの値(エラーコード)の符号が反転されていました。例えば、-12
が12
になります。
変更後は、JCC
とNEGL AX
が削除されたため、mmap
システムコールが返した生の値がそのままAX
レジスタに残り、GoランタイムのCコードに渡されるようになります。これにより、Cコードでのエラーチェックが、OSが返すmmap
の標準的なエラー挙動と一致するようになりました。
この一連の変更により、NetBSDおよびOpenBSD上でのGoランタイムのメモリ管理がより堅牢になり、mmap
システムコールが返すエラーを正確に検出できるようになりました。
関連リンク
- Go Change List 7870044: https://golang.org/cl/7870044
参考にした情報源リンク
- appspot.com (Web search result for golang.org/cl/7870044)
- golang.org (Web search result for golang.org/cl/7870044)
mmap
man page (一般的なUnix系OSのドキュメント)- C言語における符号付き/符号なし整数の変換ルールに関する情報