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

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

このコミットは、Goランタイムにおける以前の変更(CL 8954044 / ad3c2ffb16d7)を元に戻すものです。元の変更は、ガベージコレクタ(GC)がrunfinq()関数内のフレームの内容を誤って参照するのを防ぐことを目的としていましたが、i386アーキテクチャでは動作したものの、amd64およびarmアーキテクチャで失敗したため、このコミットでその変更が取り消されました。これにより、特定のアーキテクチャでのランタイムの安定性が回復されました。

コミット

  • コミットハッシュ: db1c218d4f2ce63196aa162ca0743e08e4ae9c9c
  • Author: Jan Ziak 0xe2.0x9a.0x9b@gmail.com
  • Date: Thu Apr 25 18:12:09 2013 +0200

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

https://github.com/golang/go/commit/db1c218d4f2ce63196aa162ca0743e08e4ae9c9c

元コミット内容

runtime: prevent the GC from seeing the content of a frame in runfinq()

Fixes #5348.

R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/8954044

変更の背景

元のコミット(CL 8954044)は、Goランタイムのガベージコレクション(GC)に関する特定のバグ(Issue 5348)を修正しようとしました。このバグは、runfinq()関数が実行されている際に、GCがスタックフレーム内のメモリ領域を誤ってスキャンし、その結果、本来は到達不可能であるはずのオブジェクトがGCによって解放されずに残ってしまう、あるいは不正確なポインタ情報に基づいてGCが誤動作する可能性があったためです。

元の修正は、runfinq()内で動的に確保されるフレームバッファのメモリ割り当て方法を変更することで、この問題を解決しようとしました。具体的には、GCがポインタとして解釈すべきでないメモリ領域をスキャンしないように、runtime·mallocgcの代わりにruntime·malを使用する変更が導入されました。

しかし、この修正はi386アーキテクチャでは期待通りに動作したものの、amd64およびarmアーキテクチャでは予期せぬ失敗を引き起こしました。コミットメッセージには具体的な失敗の詳細は記載されていませんが、異なるアーキテクチャ間でのメモリレイアウト、ポインタの扱い、またはGCの実装の詳細の違いが原因である可能性が高いです。このアーキテクチャ固有の不具合のため、元の修正を元に戻すことが決定されました。

前提知識の解説

Goのガベージコレクション (GC)

Goのガベージコレクタは、プログラムが動的に確保したメモリのうち、もはや参照されなくなった(到達不可能になった)オブジェクトを自動的に解放するシステムです。GoのGCは並行マーク・スイープ方式を採用しており、プログラムの実行と並行してGCが動作することで、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えています。GCは、ヒープ上のオブジェクトをスキャンして到達可能性を判断しますが、スタック上のポインタも重要なルートとしてスキャンします。

runtime.SetFinalizer

runtime.SetFinalizerは、Goの組み込み関数で、特定のオブジェクトがGCによって回収される直前に実行される関数(ファイナライザ)を設定するために使用されます。ファイナライザは、ファイルハンドルやネットワーク接続などの外部リソースをクリーンアップするのに役立ちますが、GCの動作に影響を与える可能性があり、注意して使用する必要があります。Issue 5348は、ファイナライザに関連するGCの挙動のバグであったと推測されます。

runfinq()

runfinq()は、Goランタイム内部の関数で、ファイナライザキュー(finalizer queue)を処理する役割を担っています。GCによって回収される準備ができたオブジェクトにファイナライザが設定されている場合、そのファイナライザはrunfinq()によって実行されます。この関数は、ファイナライザに渡す引数などを一時的に保持するためのフレームバッファを動的に確保することがあります。

runtime·mallocgcruntime·mal

これらはGoランタイム内部のメモリ割り当て関数です。

  • runtime·mallocgc(size, flags, typ, noscan): これはGCによってスキャンされる可能性のあるメモリを割り当てるための関数です。flagstyp引数を通じて、割り当てられるメモリがポインタを含むかどうか(FlagNoPointersなど)や、その型情報などをGCに伝えることができます。noscan1の場合、GCはそのメモリ領域をポインタとしてスキャンしません。
  • runtime·mal(size): これはGCによってスキャンされない(ポインタを含まない)メモリを割り当てるための、より低レベルな関数です。通常、ポインタを含まないデータ構造や、GCが関与すべきでない一時的なバッファなどに使用されます。

GCが「フレームの内容を見る」ことの危険性

GCがスタックフレームの内容をスキャンする際、そのフレーム内のメモリがポインタであると誤って判断したり、無効なポインタを読み取ったりすると、GCがクラッシュしたり、誤ったオブジェクトを保持したり、解放したりする可能性があります。特に、一時的なバッファや、GCが管理すべきでない生データが置かれている領域をGCがポインタとしてスキャンしてしまうと、深刻な問題を引き起こす可能性があります。元のCLは、この問題を回避するために、runfinq()内で使用されるフレームバッファがGCによってスキャンされないように変更しようとしました。

技術的詳細

元のCL 8954044は、src/pkg/runtime/mgc0.c内のrunfinq関数において、ファイナライザの引数を格納するための一時的なフレームバッファの割り当て方法を変更しました。

変更前:

frame = runtime·mallocgc(framesz, FlagNoPointers, 0, 1);

この行は、frameszサイズのメモリを割り当て、FlagNoPointersフラグとnoscan=1を指定しています。FlagNoPointersは、割り当てられたメモリがポインタを含まないことをGCに示し、noscan=1はGCがこのメモリ領域をスキャンしないように指示します。これは、このバッファがファイナライザの引数(ポインタを含む可能性がある)を一時的に保持するものの、GCがこのバッファ自体をポインタのルートとしてスキャンすべきではない、という意図があったと考えられます。

変更後(元のCL 8954044による変更):

frame = runtime·mal(framesz);

この変更は、runtime·mallocgcの代わりにruntime·malを使用することで、GCがこのメモリ領域を完全に無視するように意図されました。runtime·malは、GCに認識されない(GCによってスキャンされない)メモリを割り当てるため、GCがこのフレームバッファの内容をポインタとして誤って解釈するリスクを排除できると考えられました。

しかし、この変更がi386では動作したものの、amd64とarmで失敗した原因は、以下の点が考えられます。

  1. アーキテクチャ固有のメモリレイアウトとアライメント: 異なるアーキテクチャでは、スタックフレームのレイアウトやメモリのアライメント要件が異なる場合があります。runtime·malで割り当てられたメモリが、特定のアーキテクチャでGCが期待するアライメントや構造と合致せず、問題を引き起こした可能性があります。
  2. GCの実装の詳細: GoのGCは、アーキテクチャごとに最適化された部分があります。runtime·malで割り当てられたメモリが、特定のアーキテクチャのGC実装において、予期せぬ方法でアクセスされたり、GCの内部状態と矛盾したりした可能性があります。例えば、GCがスタックウォークを行う際に、runtime·malで割り当てられた領域を誤ってスタックの一部として解釈し、その内容をスキャンしようとした結果、クラッシュや不正な動作を引き起こしたなどが考えられます。
  3. ポインタの隠蔽と露出: runfinq内でファイナライザの引数(ポインタを含む)がこのフレームバッファにコピーされる際、runtime·malで割り当てられたメモリはGCから見えないため、GCがこれらのポインタを追跡できなくなります。i386ではたまたま問題にならなかったが、amd64やarmでは、このポインタの隠蔽がGCの正確な動作を妨げ、メモリリークやクラッシュにつながった可能性があります。元のruntime·mallocgcFlagNoPointersnoscan=1を指定していたのは、このバッファ自体はスキャンしないが、その中に含まれるポインタはGCが別の方法で認識する必要がある、というニュアンスがあったのかもしれません。runtime·malは、そのニュアンスを完全に失わせた可能性があります。

このコミットは、これらのアーキテクチャ固有の問題を解決するために、元の変更を元に戻し、runtime·mallocgcの使用に戻すことで、ランタイムの安定性を優先しました。

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

このコミットでは、以下の2つのファイルが変更されました。

  1. src/pkg/runtime/mgc0.c:

    • runfinq関数内のメモリ割り当ての行が変更されました。
    • - frame = runtime·mal(framesz); (削除された行)
    • + frame = runtime·mallocgc(framesz, FlagNoPointers, 0, 1); (追加された行)
    • これは、元のCL 8954044で行われた変更を正確に元に戻すものです。
  2. test/fixedbugs/issue5348.go:

    • このファイル全体が削除されました。
    • このテストファイルは、Issue 5348の修正を検証するために作成されたものですが、修正が元に戻されたため、このテストも不要となり削除されました。

コアとなるコードの解説

src/pkg/runtime/mgc0.cにおける変更は、runfinq関数内でファイナライザの引数を一時的に保持するために使用されるframeバッファのメモリ割り当て方法を、runtime·malからruntime·mallocgcに戻すものです。

元のCL 8954044は、GCがrunfinq()のフレームの内容を「見ない」ようにするために、runtime·malを使用しました。これは、runtime·malがGCに認識されないメモリを割り当てるため、GCがその内容をスキャンすることはない、という考えに基づいています。しかし、このアプローチはamd64とarmで問題を引き起こしました。

このコミットで元に戻されたruntime·mallocgc(framesz, FlagNoPointers, 0, 1)は、以下の意味を持ちます。

  • framesz: 割り当てるメモリのサイズ。
  • FlagNoPointers: このメモリ領域自体にはポインタが含まれていないことをGCに示します。これは、このバッファが主に生データや、GCがスキャンすべきでない一時的な値を保持するために使用されることを意味します。
  • 0: 型情報(typ)は指定されていません。
  • 1: noscanフラグが1に設定されています。これは、GCがこの割り当てられたメモリ領域をポインタとしてスキャンしないように明示的に指示します。

つまり、runtime·mallocgcを使用しつつも、FlagNoPointersnoscan=1を指定することで、GCに対して「このメモリはGCヒープの一部だが、その内容をポインタとしてスキャンする必要はない」と伝えています。これは、runtime·malがGCから完全に独立したメモリを割り当てるのとは異なり、GCの管理下に置きつつも、スキャン対象から除外するという、よりきめ細かい制御を可能にします。

amd64とarmでruntime·malが失敗した原因は、おそらくGCが完全に無視するメモリ領域にファイナライザの引数(ポインタ)が置かれることで、GCの内部的な整合性が崩れたり、特定のアーキテクチャでのGCの動作と矛盾したりしたためと考えられます。runtime·mallocgcに戻すことで、GCは少なくともこのメモリ領域の存在を認識し、その上でスキャンしないという指示に従うため、より安定した動作が期待できます。

test/fixedbugs/issue5348.goの削除は、この修正が元に戻されたため、関連するテストケースも不要になったことを示しています。これは、元の修正が意図した問題を完全に解決できなかったか、あるいは新たな問題を引き起こしたため、その修正とテストが一時的に破棄されたことを意味します。

関連リンク

  • 元の変更セット (CL): https://golang.org/cl/8954044 (このリンクはGoの内部CLシステムへのものであり、公開されているGitHubのコミットとは直接対応しない場合があります。)
  • 関連するIssue: #5348 (このIssue番号もGoの内部トラッカーのものである可能性があり、公開されているGitHubのIssueとは異なる場合があります。)

参考にした情報源リンク

  • Goのガベージコレクションに関する一般的な情報源(例: Go公式ドキュメント、GoブログのGCに関する記事など)
  • Goランタイムのソースコード(src/pkg/runtime/mgc0.cなど)
  • Goのメモリ割り当てに関する議論やドキュメント
  • Web search results for "Go issue 5348": I was unable to find a specific "Go issue 5348" within the official golang/go GitHub repository or other widely recognized Go-related issue trackers. The issue numbers in the main Go repository are significantly higher, suggesting that 5348 would be a very old issue if it existed there. It's possible that the issue belongs to a different, less prominent Go-related project, or the number may be incorrect.
  • Web search results for "golang CL 8954044": CL 8954044 in Go refers to a change in the go/types package. It specifically addresses the Interface.Complete() method. (この検索結果は、コミットメッセージに記載されているCL番号が、公開されているGoのCL番号と異なることを示唆しています。これは、内部的なCL番号であるか、非常に古いCLである可能性があります。)