[インデックス 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
の内部実装におけるゼロサイズの型に対する挙動の修正と、関連するドキュメントの更新にあります。
-
runtime.SetFinalizer
の修正 (src/pkg/runtime/malloc.goc
):SetFinalizer
関数は、引数として渡されたオブジェクトの型情報を内部的に解析します。修正前は、オブジェクトがポインタ型であり、かつそれが割り当てられたブロックの先頭にあることを確認していました。しかし、ゼロサイズの型の場合、sizeof *x
(x
がポインタの場合の*x
のサイズ)がゼロになります。このようなオブジェクトは、ガベージコレクタが管理する通常のヒープメモリ上に存在しないか、特殊な方法で扱われるため、SetFinalizer
が内部で期待するメモリレイアウトやポインタの有効性チェックが破綻し、クラッシュを引き起こす可能性がありました。このコミットでは、
SetFinalizer
の冒頭に以下のチェックを追加しました。ot = (PtrType*)obj.type; if(ot->elem != nil && ot->elem->size == 0) { return; }
このコードは、
obj
がポインタ型であり(ot->elem != nil
)、かつそのポインタが指す要素のサイズがゼロである場合(ot->elem->size == 0
)に、ファイナライザの設定をスキップして関数から即座にリターンするようにします。これにより、ゼロサイズのオブジェクトに対してファイナライザを設定しようとした際に、後続のメモリ関連のチェックや操作でクラッシュするのを防ぎます。ファイナライザが設定されないことで、ゼロサイズのオブジェクトに対するファイナライザの実行が保証されないという既存の仕様とも整合します。 -
ドキュメントの更新 (
src/pkg/runtime/extern.go
):runtime.SetFinalizer
のドキュメントに、以下の文言が追加されました。// It is not guaranteed that a finalizer will run if the size of *x is // zero bytes.
これは、ゼロサイズのオブジェクトに対するファイナライザの実行が保証されないという既存の暗黙的な挙動を、より明示的に開発者に伝えるためのものです。これにより、開発者はゼロサイズの型に対してファイナライザを設定する際に、そのファイナライザが実行されない可能性があることを認識し、それに応じた設計を行うことができます。
-
テストケースの追加 (
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
が実際にポインタ型であることを確認しています。ポインタ型でない場合、elem
はnil
になります。ot->elem->size == 0
: この条件は、ポインタが指す要素のサイズがゼロバイトであるかどうかをチェックしています。Goにおいて、struct{}
のようなゼロサイズの型は、size
が0として表現されます。
このif
文全体は、「もしobj
がポインタであり、かつそのポインタが指す型がゼロサイズであるならば」という条件を意味します。この条件が真である場合、関数はreturn;
によって即座に終了します。これにより、ゼロサイズのオブジェクトに対してファイナライザを設定しようとした際に、それ以降のメモリ関連の処理(例えば、runtime·mlookup
によるメモリブロックの検索など)が実行されるのを防ぎ、クラッシュを回避します。
この変更は、ゼロサイズのオブジェクトがGoのメモリモデルにおいて特殊な振る舞いをすること、特にガベージコレクタの管理対象外となることが多いという事実に基づいています。メモリを消費しないオブジェクトに対してファイナライザを設定しても、ガベージコレクタがそのオブジェクトを「解放」する機会がないため、ファイナライザが実行されることは期待できません。この修正は、この現実をコードに反映させ、不要な処理をスキップし、同時にランタイムの安定性を向上させています。
関連リンク
- Go issue #6857: https://github.com/golang/go/issues/6857
- Go CL 43580043: https://golang.org/cl/43580043
参考にした情報源リンク
- Go言語の公式ドキュメント (runtime.SetFinalizer): https://pkg.go.dev/runtime#SetFinalizer
- Go言語におけるゼロサイズ型 (struct{}): 一般的なGoの言語仕様に関する情報源
- Go言語のガベージコレクションに関する情報源