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

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

このコミットは、Goランタイムにおけるruntime.SetFinalizer関数の堅牢性を向上させるものです。具体的には、ゼロサイズの型(例えば空の構造体struct{})に対してSetFinalizerが呼び出された際に発生する可能性のあるクラッシュを防ぎ、その挙動を明示的にドキュメントに追加しています。これにより、Goプログラムの安定性が向上し、開発者が予期せぬランタイムエラーに遭遇するリスクが低減されます。

コミット

commit 4b76a31c6d9fd9dd0c58b46a71c10d5061ed39eb
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Dec 17 14:18:58 2013 -0800

    runtime: don't crash in SetFinalizer if sizeof *x is zero
    
    And document it explicitly, even though it already said
    it wasn't guaranteed.
    
    Fixes #6857
    
    R=golang-dev, khr
    CC=golang-dev
    https://golang.org/cl/43580043

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

https://github.com/golang/go/commit/4b76a31c6d9fd9dd0c58b46a71c10d5061ed39eb

元コミット内容

runtime: don't crash in SetFinalizer if sizeof *x is zero And document it explicitly, even though it already said it wasn't guaranteed. Fixes #6857

変更の背景

このコミットは、GoランタイムのSetFinalizer関数が、ゼロサイズのオブジェクト(例えば、フィールドを持たない空の構造体struct{}のインスタンス)に対して呼び出された場合に、ランタイムがクラッシュする可能性があった問題を修正するために行われました。Goのガベージコレクタは、オブジェクトが到達不能になったときにファイナライザを実行しますが、ゼロサイズのオブジェクトはメモリを消費しないため、ガベージコレクタの管理対象から外れることがあり、そのライフサイクルが通常のオブジェクトとは異なる振る舞いをすることがあります。

具体的には、Go issue #6857で報告された問題に対応しています。この問題は、ゼロサイズの型に対するSetFinalizerの呼び出しが、内部的なポインタ操作やメモリ割り当てのロジックと整合せず、不正なメモリアクセスやパニックを引き起こす可能性を示唆していました。このコミットは、このようなエッジケースでのクラッシュを防ぎ、SetFinalizerの堅牢性を高めることを目的としています。また、ゼロサイズのオブジェクトに対するファイナライザの実行が保証されないという既存のドキュメントの記述を、より明確にする意図も含まれています。

前提知識の解説

Goのruntime.SetFinalizer

runtime.SetFinalizerは、Goの標準ライブラリruntimeパッケージが提供する関数で、特定のオブジェクトがガベージコレクタによってメモリから解放される直前に実行される関数(ファイナライザ)を設定するために使用されます。そのシグネチャは以下の通りです。

func SetFinalizer(obj interface{}, finalizer interface{})
  • obj: ファイナライザを設定する対象のオブジェクト。これはポインタである必要があります。
  • finalizer: objがガベージコレクトされる際に実行される関数。この関数は、objと同じ型のポインタを唯一の引数として受け取る必要があります。

ファイナライザは、リソースのクリーンアップ(例: ファイルハンドルのクローズ、ネットワーク接続の終了、CGoで確保したメモリの解放など)に利用されることがありますが、その実行タイミングはガベージコレクタに依存するため、予測不可能であり、確実なリソース管理には適していません。Goのドキュメントでは、ファイナライザに依存して重要なリソースをクリーンアップすることは推奨されていません。

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

Goは自動メモリ管理(ガベージコレクション)を採用しています。プログラムが動的にメモリを割り当てると、Goランタイムのガベージコレクタが、もはやプログラムから到達不能になったメモリ領域を自動的に識別し、解放します。これにより、開発者は手動でのメモリ管理から解放され、メモリリークのリスクが低減されます。

ゼロサイズの型とオブジェクト

Goにおいて、ゼロサイズの型とは、メモリを一切消費しない型のことです。最も一般的な例は空の構造体struct{}です。

type Empty struct{}
var e Empty // eはメモリを消費しない

ゼロサイズの型は、主にセマンティックな目的や、チャネルの同期、セットの実装(マップのキーとして使用し、値にstruct{}を使う)などに利用されます。これらのオブジェクトはメモリを消費しないため、ガベージコレクタの通常のメモリ管理フローから外れることがあります。ガベージコレクタはメモリを解放することを目的としているため、そもそもメモリを消費しないオブジェクトに対しては、そのライフサイクル管理が特殊になります。

技術的詳細

このコミットの技術的詳細は、runtime.SetFinalizerの内部実装におけるゼロサイズの型に対する挙動の修正と、関連するドキュメントの更新にあります。

  1. runtime.SetFinalizerの修正 (src/pkg/runtime/malloc.goc): SetFinalizer関数は、引数として渡されたオブジェクトの型情報を内部的に解析します。修正前は、オブジェクトがポインタ型であり、かつそれが割り当てられたブロックの先頭にあることを確認していました。しかし、ゼロサイズの型の場合、sizeof *xxがポインタの場合の*xのサイズ)がゼロになります。このようなオブジェクトは、ガベージコレクタが管理する通常のヒープメモリ上に存在しないか、特殊な方法で扱われるため、SetFinalizerが内部で期待するメモリレイアウトやポインタの有効性チェックが破綻し、クラッシュを引き起こす可能性がありました。

    このコミットでは、SetFinalizerの冒頭に以下のチェックを追加しました。

    ot = (PtrType*)obj.type;
    if(ot->elem != nil && ot->elem->size == 0) {
        return;
    }
    

    このコードは、objがポインタ型であり(ot->elem != nil)、かつそのポインタが指す要素のサイズがゼロである場合(ot->elem->size == 0)に、ファイナライザの設定をスキップして関数から即座にリターンするようにします。これにより、ゼロサイズのオブジェクトに対してファイナライザを設定しようとした際に、後続のメモリ関連のチェックや操作でクラッシュするのを防ぎます。ファイナライザが設定されないことで、ゼロサイズのオブジェクトに対するファイナライザの実行が保証されないという既存の仕様とも整合します。

  2. ドキュメントの更新 (src/pkg/runtime/extern.go): runtime.SetFinalizerのドキュメントに、以下の文言が追加されました。

    // It is not guaranteed that a finalizer will run if the size of *x is
    // zero bytes.
    

    これは、ゼロサイズのオブジェクトに対するファイナライザの実行が保証されないという既存の暗黙的な挙動を、より明示的に開発者に伝えるためのものです。これにより、開発者はゼロサイズの型に対してファイナライザを設定する際に、そのファイナライザが実行されない可能性があることを認識し、それに応じた設計を行うことができます。

  3. テストケースの追加 (src/pkg/runtime/mfinal_test.go): TestFinalizerZeroSizedStructという新しいテスト関数が追加されました。

    func TestFinalizerZeroSizedStruct(t *testing.T) {
        type Z struct{}
        z := new(Z)
        runtime.SetFinalizer(z, func(*Z) {})
    }
    

    このテストは、ゼロサイズの構造体Zのインスタンスを作成し、それに対してruntime.SetFinalizerを呼び出しています。このテストの目的は、この操作がクラッシュを引き起こさないことを検証することです。ファイナライザが実際に実行されるかどうかはテストしていません(それは保証されないため)が、少なくともSetFinalizerの呼び出し自体が安全であることを確認しています。

これらの変更により、Goランタイムはゼロサイズの型に対するSetFinalizerの呼び出しを安全に処理できるようになり、関連するドキュメントも明確化されました。

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

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -760,12 +760,15 @@ func SetFinalizer(obj Eface, finalizer Eface) {
 	\truntime·printf(\"runtime.SetFinalizer: first argument is %S, not pointer\\n\", *obj.type->string);\n 	\tgoto throw;\n 	}\n+\tot = (PtrType*)obj.type;\n+\tif(ot->elem != nil && ot->elem->size == 0) {\n+\t\treturn;\n+\t}\n 	if(!runtime·mlookup(obj.data, &base, &size, nil) || obj.data != base) {\n 	\truntime·printf(\"runtime.SetFinalizer: pointer not at beginning of allocated block\\n\");\n 	\tgoto throw;\n 	}\n 	nret = 0;\n-\tot = (PtrType*)obj.type;\n 	fint = nil;\n 	if(finalizer.type != nil) {\n 	\tif(finalizer.type->kind != KindFunc)

src/pkg/runtime/extern.go

--- a/src/pkg/runtime/extern.go
+++ b/src/pkg/runtime/extern.go
@@ -160,6 +160,9 @@ func funcentry_go(*Func) uintptr
 // to depend on a finalizer to flush an in-memory I/O buffer such as a
 // bufio.Writer, because the buffer would not be flushed at program exit.\n //\n+// It is not guaranteed that a finalizer will run if the size of *x is\n+// zero bytes.\n+//\n // A single goroutine runs all finalizers for a program, sequentially.\n // If a finalizer must run for a long time, it should do so by starting\n // a new goroutine.

src/pkg/runtime/mfinal_test.go

--- a/src/pkg/runtime/mfinal_test.go
+++ b/src/pkg/runtime/mfinal_test.go
@@ -100,6 +100,13 @@ func TestFinalizerInterfaceBig(t *testing.T) {\n func fin(v *int) {\n }\n \n+// Verify we don\'t crash at least. golang.org/issue/6857\n+func TestFinalizerZeroSizedStruct(t *testing.T) {\n+\ttype Z struct{}\n+\tz := new(Z)\n+\truntime.SetFinalizer(z, func(*Z) {})\n+}\n+\n func BenchmarkFinalizer(b *testing.B) {\n \tconst CallsPerSched = 1000\n \tprocs := runtime.GOMAXPROCS(-1)\n```

## コアとなるコードの解説

このコミットの核心的な変更は、`src/pkg/runtime/malloc.goc`内の`SetFinalizer`関数に追加された以下のCコードスニペットです。

```c
ot = (PtrType*)obj.type;
if(ot->elem != nil && ot->elem->size == 0) {
    return;
}
  • ot = (PtrType*)obj.type;: ここでは、obj(ファイナライザを設定しようとしているオブジェクト)の型情報がPtrTypeにキャストされ、otという変数に格納されます。PtrTypeはGoのポインタ型を内部的に表現する構造体で、そのポインタが指す要素の型情報(elemフィールド)を含んでいます。
  • ot->elem != nil: この条件は、objが実際にポインタ型であることを確認しています。ポインタ型でない場合、elemnilになります。
  • ot->elem->size == 0: この条件は、ポインタが指す要素のサイズがゼロバイトであるかどうかをチェックしています。Goにおいて、struct{}のようなゼロサイズの型は、sizeが0として表現されます。

このif文全体は、「もしobjがポインタであり、かつそのポインタが指す型がゼロサイズであるならば」という条件を意味します。この条件が真である場合、関数はreturn;によって即座に終了します。これにより、ゼロサイズのオブジェクトに対してファイナライザを設定しようとした際に、それ以降のメモリ関連の処理(例えば、runtime·mlookupによるメモリブロックの検索など)が実行されるのを防ぎ、クラッシュを回避します。

この変更は、ゼロサイズのオブジェクトがGoのメモリモデルにおいて特殊な振る舞いをすること、特にガベージコレクタの管理対象外となることが多いという事実に基づいています。メモリを消費しないオブジェクトに対してファイナライザを設定しても、ガベージコレクタがそのオブジェクトを「解放」する機会がないため、ファイナライザが実行されることは期待できません。この修正は、この現実をコードに反映させ、不要な処理をスキップし、同時にランタイムの安定性を向上させています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (runtime.SetFinalizer): https://pkg.go.dev/runtime#SetFinalizer
  • Go言語におけるゼロサイズ型 (struct{}): 一般的なGoの言語仕様に関する情報源
  • Go言語のガベージコレクションに関する情報源