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

[インデックス 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の戻り値を変更せずにそのまま扱い、エラーチェックをより正確に行うことで、プラットフォーム間の挙動の統一と堅牢性の向上を図っています。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念を把握しておく必要があります。

  1. mmapシステムコール: mmap(memory map)は、Unix系オペレーティングシステムで提供されるシステムコールの一つで、ファイルやデバイス、あるいは匿名メモリ領域をプロセスのアドレス空間にマッピングするために使用されます。これにより、ファイルの内容をメモリとして直接アクセスしたり、プロセス間でメモリを共有したり、効率的なメモリ割り当てを行ったりすることが可能になります。 mmapは通常、成功した場合はマッピングされたメモリ領域の開始アドレス(ポインタ)を返し、失敗した場合はMAP_FAILEDという特殊な値(通常は(void *)-1)を返します。エラーの具体的な原因は、errno変数に設定されます。

  2. ENOMEM: ENOMEMは、Unix系システムで定義されているエラーコードの一つで、"Out of memory"(メモリ不足)を意味します。mmapシステムコールがメモリ割り当てに失敗した場合など、システムが要求されたメモリを供給できない場合に設定されることがあります。C言語の標準ライブラリでは、通常、errno.hで定義されており、正の整数値です。

  3. 符号付き整数と符号なし整数: コンピュータの数値表現において、整数は「符号付き(signed)」と「符号なし(unsigned)」の2種類があります。

    • 符号付き整数: 正の値、負の値、ゼロを表現できます。最上位ビット(MSB)が符号ビットとして使われ、0なら正、1なら負(通常は2の補数表現)を示します。
    • 符号なし整数: 負の値を表現できず、ゼロと正の値のみを表現します。すべてのビットが数値の大きさを表すために使われるため、同じビット幅であれば符号付き整数よりも表現できる正の数の範囲が広くなります。 問題は、これらの型が混在する演算や比較において発生します。C言語などでは、符号付きと符号なしのオペランドが混在する場合、符号付きオペランドが符号なしに昇格(型変換)されるルールがあります。この際、負の符号付き整数は非常に大きな正の符号なし整数として解釈されるため、意図しない比較結果を生むことがあります。
  4. Goランタイムのメモリ管理: Go言語は独自のランタイムを持っており、ガベージコレクションやスケジューラ、そしてメモリ管理を自身で行います。Goランタイムは、OSから大きなメモリブロックをmmapなどで確保し、それを自身のヒープとして管理し、Goプログラムのオブジェクトに割り当てます。このため、mmapの挙動はGoランタイムの安定性とパフォーマンスに直結します。

  5. アセンブリ言語とシステムコール: Goランタイムの低レベルな部分、特にOSとのインタフェース部分は、C言語やアセンブリ言語で実装されています。システムコールを直接呼び出す部分は、通常、アセンブリ言語で記述されます。このコミットで変更されている.sファイルは、アセンブリ言語で書かれたシステムコールラッパーです。

技術的詳細

このコミットの技術的な核心は、NetBSDおよびOpenBSDにおけるmmapシステムコールの戻り値の処理方法の変更にあります。

従来のGoランタイムの実装では、NetBSDおよびOpenBSDのmmapシステムコールがエラーを返した場合、そのエラーコード(通常は負の値)をアセンブリレベルで正の値に変換(符号を反転)していました。これは、一部のシステムコールがエラー時に負の値を返す慣習に合わせたものかもしれません。

しかし、GoランタイムのCコード(mem_netbsd.cmem_openbsd.c)では、mmapの戻り値(ポインタ型)をENOMEM(正の整数値)と比較していました。ここで問題が発生します。

  • アセンブリでのエラーコードの反転: mmapがエラー(例: -ENOMEM)を返すと、アセンブリコードでNEG命令などを使ってその値を正に反転させていました。例えば、-12ENOMEMが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*)-1MAP_FAILED)が、符号付き整数として解釈された後に符号なし整数と比較されると、負の値が非常に大きな正の値として扱われるため、v < 4096のような比較が常に偽になるという状況が発生します。これは、mmapが返すエラーを示すポインタ値が、実際には有効なアドレス範囲外の負の値であるにもかかわらず、符号なしとして解釈されることで、非常に大きな正のアドレスと見なされてしまうためです。

このコミットでは、この問題を解決するために以下の変更を行っています。

  1. アセンブリコードからのエラーコード反転の削除: sys_netbsd_386.ssys_netbsd_amd64.ssys_openbsd_386.ssys_openbsd_amd64.sの各ファイルから、mmapシステムコールの戻り値(AXレジスタ)を反転させる命令(NEGL AXNEGQ AX)と、条件分岐命令(JCC 2(PC))が削除されました。これにより、mmapが返す生の値がそのままGoランタイムのCコードに渡されるようになります。
  2. Cコードでのエラーチェックの修正: mem_netbsd.cmem_openbsd.cでは、mmapの戻り値pENOMEMの比較が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を返し、errnoENOMEMを設定します。この変更は、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.ssys_netbsd_amd64.ssys_openbsd_386.ssys_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ランタイムのメモリ管理(SysReserveSysMap)を担当する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の戻り値pENOMEMの比較方法です。 変更前は((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.ssrc/pkg/runtime/sys_netbsd_amd64.ssrc/pkg/runtime/sys_openbsd_386.ssrc/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レジスタの値(エラーコード)の符号が反転されていました。例えば、-1212になります。 変更後は、JCCNEGL AXが削除されたため、mmapシステムコールが返した生の値がそのままAXレジスタに残り、GoランタイムのCコードに渡されるようになります。これにより、Cコードでのエラーチェックが、OSが返すmmapの標準的なエラー挙動と一致するようになりました。

この一連の変更により、NetBSDおよびOpenBSD上でのGoランタイムのメモリ管理がより堅牢になり、mmapシステムコールが返すエラーを正確に検出できるようになりました。

関連リンク

参考にした情報源リンク