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

[インデックス 19032] ファイルの概要

このコミットは、GoランタイムにおけるOS X (特に10.9.2) 上での不正なメモリアドレスアクセス時のSIGSEGV(セグメンテーション違反)ハンドリングの問題を修正し、関連するテストを追加するものです。Goのガベージコレクタが使用する「ポイズンポインタ」が、OS X上で特定の種類のハードウェアフォールトを引き起こした際に、OSが誤ったシグナル情報を報告し、結果としてGoプログラムがハングアップする問題を解決します。

コミット

commit 17f9423e75db40a08369c7ea23449db1c26a4890
Author: Russ Cox <rsc@golang.org>
Date:   Thu Apr 3 19:07:33 2014 -0400

    runtime: test malformed address fault and fix on OS X
    
    The garbage collector poison pointers
    (0x6969696969696969 and 0x6868686868686868)
    are malformed addresses on amd64.
    That is, they are not 48-bit addresses sign extended
    to 64 bits. This causes a different kind of hardware fault
    than the usual 'unmapped page' when accessing such
    an address, and OS X 10.9.2 sends the resulting SIGSEGV
    incorrectly, making it look like it was user-generated
    rather than kernel-generated and does not include the
    faulting address. This means that in GODEBUG=gcdead=1
    mode, if there is a bug and something tries to dereference
    a poisoned pointer, the runtime delivers the SIGSEGV to
    os/signal and returns to the faulting code, which faults
    again, causing the process to hang instead of crashing.
    
    Fix by rewriting "user-generated" SIGSEGV on OS X to
    look like a kernel-generated SIGSEGV with fault address
    0xb01dfacedebac1e.
    
    I chose that address because (1) when printed in hex
    during a crash, it is obviously spelling out English text,
    (2) there are no current Google hits for that pointer,
    which will make its origin easy to find once this CL
    is indexed, and (3) it is not an altogether inaccurate
    description of the situation.
    
    Add a test. Maybe other systems will break too.
    
    LGTM=khr
    R=golang-codereviews, khr
    CC=golang-codereviews, iant, ken
    https://golang.org/cl/83270049

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/17f9423e75db40a08369c7ea23449db1c26a4890

元コミット内容

runtime: test malformed address fault and fix on OS X

The garbage collector poison pointers
(0x6969696969696969 and 0x6868686868686868)
are malformed addresses on amd64.
That is, they are not 48-bit addresses sign extended
to 64 bits. This causes a different kind of hardware fault
than the usual 'unmapped page' when accessing such
an address, and OS X 10.9.2 sends the resulting SIGSEGV
incorrectly, making it look like it was user-generated
rather than kernel-generated and does not include the
faulting address. This means that in GODEBUG=gcdead=1
mode, if there is a bug and something tries to dereference
a poisoned pointer, the runtime delivers the SIGSEGV to
os/signal and returns to the faulting code, which faults
again, causing the process to hang instead of crashing.

Fix by rewriting "user-generated" SIGSEGV on OS X to
look like a kernel-generated SIGSEGV with fault address
0xb01dfacedebac1e.

I chose that address because (1) when printed in hex
during a crash, it is obviously spelling out English text,
(2) there are no current Google hits for that pointer,
which will make its origin easy to find once this CL
is indexed, and (3) it is not an altogether inaccurate
description of the situation.

Add a test. Maybe other systems will break too.

LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews, iant, ken
https://golang.org/cl/83270049

変更の背景

Goのガベージコレクタ(GC)は、解放されたメモリ領域への不正なアクセスを検出するために「ポイズンポインタ」という特殊な値をメモリに書き込みます。これらのポイズンポインタ(例: 0x69696969696969690x6868686868686868)は、amd64アーキテクチャにおいて意図的に不正なアドレスとして設計されています。具体的には、これらは64ビットアドレス空間において、有効な48ビット仮想アドレスが符号拡張された形式ではありません。

OS X 10.9.2では、このような「不正な形式のアドレス」へのアクセスが発生した場合、通常の「マップされていないページ」へのアクセスとは異なる種類のハードウェアフォールトが発生します。この際、OSは結果として発生するSIGSEGVシグナルを誤って処理し、あたかもユーザープロセスがkill -SEGVコマンドなどで生成したかのような「user-generated」なシグナルとして報告してしまいます。さらに、この誤った報告では、本来含まれるべきフォールトアドレスの情報が欠落していました。

この問題がGoランタイムに与える影響は深刻でした。GODEBUG=gcdead=1というデバッグモードが有効な場合、もしプログラムのバグによってポイズンポインタがデリファレンス(参照)されると、ランタイムはSIGSEGVを受け取ります。しかし、OSからの誤った情報により、ランタイムはこのシグナルをos/signalパッケージに渡し、その後フォールトが発生したコードに制御を戻してしまいます。これにより、同じ不正なアドレスへのアクセスが再度発生し、無限ループに陥ってプロセスがハングアップするという問題が発生していました。このコミットは、このOS固有の誤ったシグナルハンドリングを修正し、Goプログラムが予期せぬハングアップではなく、適切なクラッシュ動作をするようにすることを目的としています。

前提知識の解説

Goのガベージコレクタ (GC) とポイズンポインタ

Go言語は自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが不要になったメモリ領域を自動的に解放し、再利用可能にします。このプロセスにおいて、GoのGCはデバッグ目的で「ポイズンポインタ(Poison Pointers)」を使用することがあります。

ポイズンポインタとは、解放されたメモリ領域に書き込まれる特定の無効な値のことです。これらの値は、通常のアドレスとしては決して有効にならないように設計されています。もしプログラムが解放済みのメモリ(ダングリングポインタ)を誤って参照しようとすると、このポイズンポインタにアクセスすることになり、ハードウェアフォールト(通常はセグメンテーション違反)が発生します。これにより、プログラムのバグ(Use-After-Freeなど)を早期に、かつ明確に検出できるようになります。コミットメッセージに記載されている0x69696969696969690x6868686868686868がその例です。これらの値は、特定のビットパターンを持つことで、有効なアドレスとは異なる振る舞いを引き起こすように意図されています。

SIGSEGV (Segmentation Fault)

SIGSEGVは「Segmentation Fault」(セグメンテーション違反)の略で、UNIX系OSにおいて、プログラムがアクセスを許可されていないメモリ領域にアクセスしようとした際にカーネルがプロセスに送信するシグナルです。これは通常、不正なポインタのデリファレンス、配列の範囲外アクセス、解放済みメモリへのアクセスなど、メモリ保護違反によって引き起こされます。SIGSEGVを受け取ったプロセスは、通常、強制終了(クラッシュ)します。

OS XのシグナルハンドリングとSI_USER / SI_KERNEL

OSは、ハードウェアフォールトやソフトウェアイベントに応じてプロセスにシグナルを送信します。SIGSEGVの場合、シグナル情報(siginfo_t構造体)には、シグナルを生成した原因を示すコード(si_code)が含まれます。

  • SI_USER: シグナルがユーザープロセス(例: kill()システムコール)によって生成されたことを示します。
  • SI_KERNELまたはその他の非SI_USERコード: シグナルがカーネルによって生成されたこと(例: メモリ保護違反などのハードウェアフォールト)を示します。

OS X 10.9.2では、特定の種類のメモリフォールト(特に不正な形式のアドレスへのアクセス)に対して、本来カーネルが生成したものであるにもかかわらず、SI_USERとして誤って報告するというバグがありました。さらに、SI_USERシグナルの場合、通常はフォールトアドレス(si_addr)が含まれないため、Goランタイムはどのメモリアドレスが問題を引き起こしたのかを特定できませんでした。

amd64アーキテクチャの仮想アドレス空間

x86-64(amd64)アーキテクチャでは、64ビットのレジスタとポインタを使用しますが、現在のCPUが実際に使用する仮想アドレス空間は通常48ビットです。これは、物理メモリの量やアドレス変換の複雑さを考慮した設計です。48ビット仮想アドレスは、64ビットレジスタに格納される際に「符号拡張」される必要があります。つまり、アドレスの47ビット目(0から数えて)のビットが、上位16ビット(48ビット目から63ビット目)にコピーされて埋められます。これにより、有効な64ビットアドレスは、上位16ビットがすべて0x0000またはすべて0xFFFFのいずれかになります。

ポイズンポインタ0x69696969696969690x6868686868686868は、この48ビットアドレスの符号拡張ルールに違反する値です。例えば、0x6969696969696969は、上位16ビットが0x6969であり、これは47ビット目の値(この場合は1)が符号拡張されたものではありません。このような「不正な形式のアドレス」へのアクセスは、通常の「マップされていないページ」へのアクセスとは異なる種類のハードウェア例外を引き起こす可能性があります。

GODEBUG=gcdead=1

これはGoランタイムのデバッグ環境変数の一つです。gcdead=1を設定すると、ガベージコレクタが解放したメモリ領域にポイズンポインタを書き込むようになります。これにより、解放済みメモリへの不正なアクセス(Use-After-Free)が発生した場合に、即座にSIGSEGVを発生させ、バグを検出・デバッグしやすくします。

技術的詳細

このコミットが対処する問題は、OS X 10.9.2におけるSIGSEGVシグナルの誤った報告に起因します。

  1. 不正なアドレスへのアクセス: GoのGCが使用するポイズンポインタ(例: 0x6969696969696969)は、amd64アーキテクチャの48ビット仮想アドレスの符号拡張ルールに違反する「不正な形式のアドレス」です。
  2. OS Xの特殊なフォールトハンドリング: OS X 10.9.2は、このような不正な形式のアドレスへのアクセスに対して、通常の「マップされていないページ」へのアクセスとは異なる種類のハードウェアフォールトを発生させます。
  3. 誤ったシグナル報告: このハードウェアフォールトの結果として発生するSIGSEGVシグナルが、OSによって誤ってsi_codeSI_USER(ユーザー生成シグナル)として報告されていました。本来、これはカーネルが生成したものであるため、SI_KERNELまたはそれに類するコードであるべきです。
  4. フォールトアドレスの欠落: SI_USERとして報告されるシグナルには、通常、フォールトアドレス(si_addr)が含まれません。これにより、Goランタイムはどのメモリアドレスが問題を引き起こしたのかを特定できませんでした。
  5. Goランタイムのハングアップ:
    • GODEBUG=gcdead=1が有効な状態で、プログラムがポイズンポインタをデリファレンスすると、SIGSEGVが発生します。
    • Goランタイムのシグナルハンドラは、このSIGSEGVを受け取ります。
    • OSからの情報がSI_USERであるため、ランタイムはこれをユーザーが意図的に送信したシグナルと誤解し、os/signalパッケージに処理を委譲します。
    • os/signalパッケージは、シグナルを処理した後、通常はフォールトが発生したコードに制御を戻します。
    • しかし、フォールトアドレスが修正されていないため、制御が戻されたコードは再び同じ不正なアドレスにアクセスし、再度SIGSEGVが発生します。
    • このプロセスが無限に繰り返され、結果としてGoプログラムがハングアップしてしまいます。本来であれば、このようなバグはプログラムのクラッシュとして明確に報告されるべきです。

このコミットの修正は、この無限ループを防ぐために、OS X上で発生したSIGSEGVがSI_USERとして報告された場合に、Goランタイムがそのシグナル情報を「書き換える」というアプローチを取っています。具体的には、si_codeSI_USER+1(これによりSI_USERではないと認識される)に変更し、si_addr0xb01dfacedebac1eULLという特定の値に設定します。

このマジックナンバー0xb01dfacedebac1eは、以下の理由で選ばれました。

  1. 可読性: 16進数で表示された際に「b01d face de bac1e」("bold face debacles" のような意味合い)と読めるため、クラッシュダンプなどで目にした際に人間がその意味を推測しやすい。
  2. 検索性: コミット当時、この値でGoogle検索してもヒットしないため、将来的にこのアドレスがクラッシュログに現れた場合、このコミットが原因であることを容易に特定できる。
  3. 状況の表現: このアドレス自体が不正な形式であり、状況をある程度正確に表現している。

この修正により、OS Xが誤ったシグナル情報を送ってきても、Goランタイムはそれを「カーネルが生成した、フォールトアドレスを持つSIGSEGV」として適切に処理できるようになり、プログラムはハングアップする代わりに正常にクラッシュするようになります。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/runtime/runtime_test.go:

    • faultAddrsというuint64のスライスが追加されました。これには、010xfffのような低アドレス、および0xffffffffffffffff0xfffffffffffff0010xb01dfacedebac1eなど、様々な高アドレスや不正な形式のアドレスが含まれています。
    • TestSetPanicOnFault関数が修正され、新しく追加されたfaultAddrsの各アドレスに対してtestSetPanicOnFault関数を呼び出すようになりました。
    • testSetPanicOnFault関数が新しく追加されました。この関数は、引数として与えられたaddrunsafe.Pointerでポインタにキャストし、そのポインタをデリファレンスすることで意図的にセグメンテーション違反を発生させます。panicが捕捉されることを期待し、捕捉されなかった場合はテストが失敗します。
  2. src/pkg/runtime/signal_amd64x.c:

    • runtime·sighandler関数内に#ifdef GOOS_darwinブロックが追加されました。
    • このブロック内で、受け取ったシグナルがSIGSEGVであり、かつSIG_CODE0(info, ctxt)SI_USERである場合に、以下の処理を行います。
      • SIG_CODE0(info, ctxt)SI_USER+1に書き換えます。
      • info->si_addr(フォールトアドレス)を0xb01dfacedebac1eULLに設定します。

コアとなるコードの解説

src/pkg/runtime/runtime_test.go

このテストファイルへの変更は、OS XにおけるSIGSEGVハンドリングの修正が正しく機能するかどうかを検証するために追加されました。

  • faultAddrs: このスライスには、様々な種類のメモリアドレスが含まれています。
    • 0, 1, 0xfff: 低いアドレス。これらは通常、NULLポインタデリファレンスや非常に低いアドレスへのアクセスをシミュレートします。
    • 0xffffffffffffffff (全ビットが1): これは~uintptr(0)に相当し、通常はアドレス空間の最上位に位置するか、無効なアドレスとして扱われます。
    • その他の0xff...で始まるアドレス: これらはamd64の48ビット仮想アドレス空間のルール(上位16ビットが47ビット目を符号拡張する)に違反する可能性のある、高位のアドレスや不正な形式のアドレスを意図しています。これらのアドレスへのアクセスは、OSによって異なる種類のハードウェアフォールトを引き起こす可能性があります。
  • TestSetPanicOnFaulttestSetPanicOnFault:
    • debug.SetPanicOnFault(true)を設定することで、GoランタイムがSIGSEGVなどのフォールトをGoのpanicに変換するようにします。
    • testSetPanicOnFault関数は、faultAddrs内の各アドレスをuintptrとして受け取り、それをunsafe.Pointer*intにキャストし、*pとしてデリファレンスします。これにより、意図的にメモリアクセス違反を発生させます。
    • defer func() { ... recover() ... }()ブロックは、このデリファレンスによって発生するpanicを捕捉し、テストが期待通りにpanicしたことを確認します。もしpanicが発生しなかった場合(つまり、プログラムがハングアップしたり、予期せぬ動作をした場合)、テストは失敗します。 このテストの目的は、Goランタイムが様々な種類の不正なアドレスへのアクセスに対して、OSの挙動に依存せず、一貫してpanicを発生させ、プログラムがハングアップすることなく終了することを確認することです。

src/pkg/runtime/signal_amd64x.c

このCファイルへの変更は、OS X 10.9.2のシグナル報告のバグを直接修正するGoランタイムの核心部分です。

  • runtime·sighandler関数: これはGoランタイムのシグナルハンドラであり、OSからシグナルを受け取った際に最初に実行されるC言語の関数です。
  • #ifdef GOOS_darwin: このプリプロセッサディレクティブにより、以下のコードブロックはOS X(Darwinカーネル)でのみコンパイル・実行されます。
  • if(sig == SIGSEGV && SIG_CODE0(info, ctxt) == SI_USER): この条件文が、問題の核心を捉えています。
    • sig == SIGSEGV: 受け取ったシグナルがセグメンテーション違反であること。
    • SIG_CODE0(info, ctxt) == SI_USER: シグナル情報が、OSによって「ユーザー生成」と誤って報告されていること。
  • SIG_CODE0(info, ctxt) = SI_USER+1;:
    • si_codeSI_USERからSI_USER+1に書き換えることで、Goランタイムはこれをユーザー生成シグナルではない(つまり、カーネル生成シグナルである)と認識するようになります。これにより、ランタイムはos/signalに処理を委譲した後、フォールトしたコードに制御を戻すのではなく、適切なクラッシュハンドリングパスに進むことができます。
  • info->si_addr = (void*)(uintptr)0xb01dfacedebac1eULL;:
    • si_addr(フォールトアドレス)を、コミットメッセージで説明されているマジックナンバー0xb01dfacedebac1eULLに設定します。OSがフォールトアドレスを提供しない場合でも、Goランタイムが有効な(ただし意図的に不正な)フォールトアドレスを持つようにすることで、デバッグ情報がより完全になります。このアドレスは、そのユニークな値と可読性から、クラッシュダンプなどでこの修正が適用されたことを容易に識別できるように設計されています。

この修正により、OS X 10.9.2の特定のバグによって引き起こされるGoプログラムのハングアップが解消され、不正なメモリアクセスが検出された際には、期待通りにプログラムがクラッシュするようになります。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分 (diff)
  • Go言語のドキュメント(ガベージコレクション、ランタイム、シグナルハンドリングに関する一般的な知識)
  • amd64アーキテクチャの仮想アドレス空間に関する一般的な情報
  • UNIX系OSにおけるSIGSEGVとシグナルハンドリングに関する一般的な情報
  • OS Xのシグナルハンドリングに関する一般的な情報
  • GODEBUG環境変数に関するGoのドキュメント