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

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

このコミットは、Go言語のsyscallパッケージにおいて、BSD系のシステムコールであるkeventの引数、およびLinuxのexec_linux.go内の特定の箇所でuintptr型を使用していた部分をunsafe.Pointer型に置き換える変更を加えています。この変更の主な目的は、Goのガベージコレクタ(GC)がポインタ引数を正確に追跡し、それらがGCによって誤って解放されることを防ぐことにあります。特に、Goランタイムが提供するネットワークポーラーを使用せず、syscallパッケージを直接利用するユーザーにとって、この変更は重要です。

コミット

commit ba8c92c1660bf86dfeedfc41ac39683c7e0d7607
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jan 22 10:35:41 2014 -0800

    syscall: use unsafe.Pointer in BSD kevent
    
    Doesn't really matter for the most part, since the runtime-integrated
    network poller uses its own kevent implementation, but for people using
    the syscall directly, we should use an unsafe.Pointer for the precise GC
    to retain the pointer arguments.
    
    Also push down unsafe.Pointer a bit further in exec_linux.go, not
    that there are any GC preemption points in the middle and sys
    is still live anyway.
    
    R=golang-codereviews, dvyukov
    CC=golang-codereviews, iant
    https://golang.org/cl/55520043

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

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

元コミット内容

このコミットは、Goのsyscallパッケージにおけるkeventシステムコール(BSD系OSで使用される)の引数、およびexec_linux.go内の特定のポインタ引数について、型をuintptrからunsafe.Pointerに変更することを目的としています。これにより、Goのガベージコレクタがこれらのポインタを正確に認識し、参照されているメモリが不適切に解放されることを防ぎます。コミットメッセージでは、Goランタイムが独自のkevent実装を使用しているため、通常の使用では影響が少ないものの、syscallパッケージを直接使用する場合にはこの変更が重要であると述べられています。また、exec_linux.goにおける変更は、GCのプリエンプションポイントがないため、それほど重要ではないが、unsafe.Pointerの使用をさらに推し進めるためのものであると説明されています。

変更の背景

この変更の背景には、Goのガベージコレクションの正確性と、システムコールにおけるポインタの扱いに関する課題がありました。

Goのガベージコレクタは、プログラムが使用しているメモリを自動的に管理し、不要になったメモリを解放します。GCが正しく機能するためには、プログラム内のすべてのポインタを正確に追跡できる必要があります。しかし、uintptr型は、ポインタを整数として扱うため、GCはその値がメモリ上のアドレスを指していることを認識できません。

keventのようなシステムコールは、カーネルに直接メモリ上のデータ構造へのポインタを渡すことがよくあります。もしこれらのポインタがuintptrとして扱われ、GoのGCがそれらを追跡できない場合、GCはポインタが指すメモリがもはや使用されていないと誤って判断し、そのメモリを解放してしまう可能性があります。これにより、システムコールが不正なメモリにアクセスしようとしてクラッシュしたり、予期せぬ動作を引き起こしたりする「Use-After-Free」のような深刻なバグが発生する可能性があります。

このコミットは、特にGoランタイムのネットワークポーラーが独自のkevent実装を持っているため、Goの標準的なネットワークI/Oではこの問題が表面化しにくいことを指摘しています。しかし、開発者がsyscallパッケージを直接使用してkeventを呼び出す場合、このGCの追跡問題が顕在化するリスクがありました。そのため、unsafe.Pointerを使用することで、GCに対して「この整数値は実際にはポインタであり、GCの対象となるメモリを指している」というヒントを与え、ポインタの正確な保持を保証する必要がありました。

exec_linux.goにおける変更も同様の理由ですが、コミットメッセージが示唆するように、その影響はkeventほど緊急性の高いものではありませんでした。これは、関連するコードパスにGCのプリエンプションポイントが少なく、sys変数が常にライブであるため、GCが誤ってメモリを解放する可能性が低いと判断されたためです。しかし、一貫性と将来的な安全性のために、ここでもunsafe.Pointerへの変更が適用されました。

前提知識の解説

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

Goのガベージコレクタは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの停止時間(STW: Stop-The-World)を最小限に抑えることを目指しています。GCは、プログラムがアクセス可能なすべてのオブジェクト(ポインタによって参照されているオブジェクト)を「ライブ」とみなし、それ以外のオブジェクトを「デッド」と判断して解放します。

GCがオブジェクトを正確に追跡するためには、プログラム内のすべてのポインタを識別できる必要があります。Goの型システムは通常、どの変数がポインタであるかをGCに伝えます。しかし、uintptr型は、ポインタの値を整数として表現するため、GCはそれがポインタであると自動的に認識できません。

unsafe.Pointer

unsafe.Pointerは、Goの型システムを迂回して、任意の型のポインタを保持できる特殊な型です。これは、C言語のvoid*に似ていますが、GoのGCとの連携において重要な役割を果たします。

unsafe.Pointerの主な特性は以下の通りです。

  • 任意の型への変換: *T(任意の型Tへのポインタ)からunsafe.Pointerへ、またはその逆へ変換できます。
  • uintptrとの相互変換: unsafe.Pointerからuintptrへ、またはその逆へ変換できます。

unsafe.Pointerが重要なのは、uintptrとは異なり、GCがunsafe.Pointerを介して参照されているメモリを追跡できる点です。つまり、unsafe.PointerはGCに対して「この値はポインタであり、GCの対象となるメモリを指している可能性がある」というヒントを与えます。これにより、GCはunsafe.Pointerが指すメモリを解放しないように動作します。

ただし、unsafe.Pointerはその名の通り「unsafe(安全でない)」であり、誤用するとメモリ安全性違反やGCの誤動作を引き起こす可能性があります。例えば、unsafe.Pointerを介して無効なメモリにアクセスしたり、GCが追跡できない方法でポインタを操作したりすると、プログラムがクラッシュしたり、データが破損したりする可能性があります。

uintptr

uintptrは、ポインタの値を保持できる符号なし整数型です。これは、ポインタを整数として扱う必要がある場合に便利ですが、GCはuintptrが指すメモリを追跡しません。つまり、uintptrにポインタの値を格納しても、GCはそのポインタが指すメモリがまだ使用中であるとは認識せず、そのメモリを解放してしまう可能性があります。これが、このコミットでuintptrからunsafe.Pointerへの変更が必要とされた主な理由です。

syscallパッケージ

syscallパッケージは、Goプログラムからオペレーティングシステム(OS)のシステムコールを直接呼び出すための機能を提供します。システムコールは、ファイルI/O、ネットワーク通信、プロセス管理など、OSカーネルが提供する低レベルな機能にアクセスするために使用されます。

Goの標準ライブラリの多くは、内部的にsyscallパッケージを利用していますが、開発者が特定のOS機能に直接アクセスしたい場合にも使用されます。システムコールは通常、C言語の関数として定義されており、ポインタを引数として受け取ることが多いため、Goからこれらのシステムコールを呼び出す際には、ポインタの扱いが重要になります。

kevent (kqueue)

keventは、BSD系OS(FreeBSD, OpenBSD, NetBSD, macOS, Dragonfly BSDなど)で利用される、効率的なイベント通知メカニズムであるkqueue(カーネルキュー)の一部です。keventシステムコールは、ファイルディスクリプタのI/O準備完了、プロセスの状態変化、タイマーイベントなど、様々な種類のイベントを監視し、通知を受け取るために使用されます。

keventは、監視したいイベントのリスト(struct keventの配列)と、発生したイベントを受け取るためのバッファ(これもstruct keventの配列)をポインタとして引数に取ります。これらのポインタがGCによって正しく保持されることが、keventベースのイベントループが安定して動作するために不可欠です。

Goランタイムのネットワークポーラー

Goランタイムには、効率的なネットワークI/Oを実現するための組み込みのネットワークポーラーがあります。これは、OSが提供する非同期I/Oメカニズム(Linuxのepoll、BSD系のkqueue、WindowsのIOCPなど)を利用して、多数のネットワーク接続を同時に処理します。Goのネットワークポーラーは、内部的にこれらのOS固有のシステムコールを呼び出しますが、その実装はGoランタイムのGCと密接に連携しており、ポインタのライフタイム管理が適切に行われています。

このコミットメッセージが示唆するように、Goランタイムのネットワークポーラーは独自のkevent実装を使用しており、これはsyscallパッケージを直接使用するケースとは異なるパスを通ります。そのため、このコミットの変更は、主にsyscallパッケージを直接利用するユーザーに影響を与えます。

技術的詳細

このコミットの核心は、Goのガベージコレクタがポインタを正確に追跡できるように、システムコールに渡されるポインタの型をuintptrからunsafe.Pointerに変更することです。

uintptrからunsafe.Pointerへの変更がGCに与える影響

前述の通り、uintptrは単なる整数であり、GCはそれがメモリ上のアドレスを指していることを認識しません。したがって、uintptrに格納されたポインタが指すメモリは、GCによって「到達不可能」と判断され、誤って解放される可能性があります。

一方、unsafe.PointerはGCに対して「これはポインタである」というシグナルを送ります。unsafe.Pointerを介して参照されているメモリは、GCによってライブであると見なされ、解放の対象から外されます。これにより、システムコールに渡されたデータ構造が、システムコールが完了する前にGCによって解放されてしまうという競合状態を防ぐことができます。

keventの引数がポインタであることの重要性

keventシステムコールは、イベントの変更リストとイベントの取得バッファをポインタとして受け取ります。これらのポインタは、Goのメモリ空間内のKevent_t構造体の配列を指しています。もしこれらのポインタがuintptrとして渡され、GCがそれらを追跡できない場合、kevent呼び出し中にGCが実行され、これらの配列が解放されてしまう可能性があります。そうなると、カーネルは解放済みのメモリにアクセスしようとし、パニックやセグメンテーション違反を引き起こすことになります。

unsafe.Pointerを使用することで、keventに渡されるchangeseventsの配列が、システムコールが完了するまでGCによって保護されることが保証されます。

exec_linux.goにおける変更の理由

exec_linux.goの変更は、SYS_SETGROUPSシステムコールに渡されるgroups引数に関連しています。ここでもcred.Groups[0]のアドレスをuintptrに変換していましたが、これをunsafe.Pointerを介してuintptrに変換するように変更しています。

コミットメッセージでは、「GC preemption points in the middle and sys is still live anyway」と述べられています。これは、この特定のコードパスでは、GCが実行される可能性のある「プリエンプションポイント」(GoルーチンがGCのために一時停止される場所)が少ないこと、そしてsys構造体(credを含む)がシステムコールが完了するまで確実にライブである(GCの対象にならない)ため、keventのケースほど緊急性がないことを意味します。しかし、コードの一貫性と、将来的にGCの動作が変更された場合の安全性を考慮して、ここでもunsafe.Pointerの使用が推奨されました。

zsyscall_*.goファイルが自動生成されることの示唆

変更されたファイルの多くがzsyscall_darwin_386.gozsyscall_freebsd_amd64.goなどのzsyscall_*.goという命名規則に従っています。これらのファイルは、通常、Goのツールチェーンによって自動生成されます。これは、Goのsyscallパッケージが、各OSおよびアーキテクチャ固有のシステムコール定義を、Cのヘッダーファイルなどから自動的にGoのコードに変換していることを示唆しています。

このコミットでこれらのファイルが変更されているということは、syscallパッケージの生成ロジック自体、または生成元となるテンプレートや定義が更新され、uintptrの代わりにunsafe.Pointerを使用するように変更されたことを意味します。これにより、Goのシステムコールバインディング全体で、ポインタの安全な扱いが一貫して適用されるようになります。

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

このコミットでは、主にsrc/pkg/syscall/syscall_bsd.gosrc/pkg/syscall/exec_linux.go、そして多数のsrc/pkg/syscall/zsyscall_*.goファイルが変更されています。ここでは、代表的な変更箇所としてsyscall_bsd.goexec_linux.goの差分を抜粋して示します。

src/pkg/syscall/syscall_bsd.go の変更

--- a/src/pkg/syscall/syscall_bsd.go
+++ b/src/pkg/syscall/syscall_bsd.go
@@ -426,15 +426,15 @@ func Sendmsg(fd int, p, oob []byte, to Sockaddr, flags int) (err error) {
 	return
 }
 
-//sys	kevent(kq int, change uintptr, nchange int, event uintptr, nevent int, timeout *Timespec) (n int, err error)
+//sys	kevent(kq int, change unsafe.Pointer, nchange int, event unsafe.Pointer, nevent int, timeout *Timespec) (n int, err error)
 
 func Kevent(kq int, changes, events []Kevent_t, timeout *Timespec) (n int, err error) {
-	var change, event uintptr
+	var change, event unsafe.Pointer
 	if len(changes) > 0 {
-		change = uintptr(unsafe.Pointer(&changes[0]))
+		change = unsafe.Pointer(&changes[0])
 	}
 	if len(events) > 0 {
-		event = uintptr(unsafe.Pointer(&events[0]))
+		event = unsafe.Pointer(&events[0])
 	}
 	return kevent(kq, change, len(changes), event, len(events), timeout)
 }

src/pkg/syscall/exec_linux.go の変更

--- a/src/pkg/syscall/exec_linux.go
+++ b/src/pkg/syscall/exec_linux.go
@@ -131,11 +131,11 @@ func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr
 	// User and groups
 	if cred := sys.Credential; cred != nil {
 		ngroups := uintptr(len(cred.Groups))
-		groups := uintptr(0)
+		var groups unsafe.Pointer
 		if ngroups > 0 {
-			groups = uintptr(unsafe.Pointer(&cred.Groups[0]))
+			groups = unsafe.Pointer(&cred.Groups[0])
 		}
-		_, _, err1 = RawSyscall(SYS_SETGROUPS, ngroups, groups, 0)
+		_, _, err1 = RawSyscall(SYS_SETGROUPS, ngroups, uintptr(groups), 0)
 		if err1 != 0 {
 			goto childerror
 		}

コアとなるコードの解説

syscall_bsd.go の変更解説

この変更は、keventシステムコールのGoラッパーであるKevent関数に焦点を当てています。

  1. kevent関数のシグネチャ変更: 元のシグネチャ: kevent(kq int, change uintptr, nchange int, event uintptr, nevent int, timeout *Timespec) (n int, err error) 変更後: kevent(kq int, change unsafe.Pointer, nchange int, event unsafe.Pointer, nevent int, timeout *Timespec) (n int, err error) これは、keventシステムコールに渡されるchangeeventの引数が、uintptrではなくunsafe.Pointerとして扱われるように変更されたことを示しています。これにより、GoのGCがこれらのポインタを追跡できるようになります。

  2. Kevent関数内の変数宣言の変更: 元の宣言: var change, event uintptr 変更後: var change, event unsafe.Pointer Kevent関数内でkeventシステムコールに渡すためのローカル変数changeeventも、uintptrからunsafe.Pointerに変更されています。これは、kevent関数のシグネチャ変更と整合性を保つためです。

  3. ポインタ変換の変更: 元のコード: change = uintptr(unsafe.Pointer(&changes[0])) 変更後: change = unsafe.Pointer(&changes[0]) 元のコードでは、changesスライス(Kevent_t構造体の配列)の最初の要素のアドレスをunsafe.Pointerに変換した後、さらにuintptrに変換していました。これは、kevent関数の引数がuintptrであったためです。 変更後では、kevent関数の引数がunsafe.Pointerになったため、unsafe.Pointer(&changes[0])で直接unsafe.Pointer型の値を取得し、それをchange変数に代入しています。これにより、changes配列がGCによって適切に保護されるようになります。event変数についても同様の変更が適用されています。

これらの変更により、keventシステムコールに渡されるchangeseventsの配列が、GoのGCによって正しく認識され、システムコール実行中に誤って解放されるリスクがなくなります。

exec_linux.go の変更解説

この変更は、Linuxシステムにおけるプロセス実行(forkAndExecInChild関数)の一部で、SYS_SETGROUPSシステムコールに渡されるグループIDの配列に関連しています。

  1. groups変数の宣言変更: 元の宣言: groups := uintptr(0) 変更後: var groups unsafe.Pointer SYS_SETGROUPSシステムコールに渡すグループIDの配列のポインタを保持するgroups変数が、uintptrからunsafe.Pointerに変更されています。

  2. ポインタ変換の変更: 元のコード: groups = uintptr(unsafe.Pointer(&cred.Groups[0])) 変更後: groups = unsafe.Pointer(&cred.Groups[0]) cred.Groupsスライス(グループIDの配列)の最初の要素のアドレスをunsafe.Pointerに変換し、それをgroups変数に代入しています。これにより、cred.Groups配列がGCによって保護されるようになります。

  3. RawSyscall呼び出しの変更: 元のコード: _, _, err1 = RawSyscall(SYS_SETGROUPS, ngroups, groups, 0) 変更後: _, _, err1 = RawSyscall(SYS_SETGROUPS, ngroups, uintptr(groups), 0) RawSyscall関数は、システムコールの引数をuintptrとして受け取ります。そのため、unsafe.Pointer型になったgroups変数をRawSyscallに渡す際には、再度uintptrに明示的に変換しています。この変換は、unsafe.PointerがGCにポインタであることを伝える役割を果たした後に行われるため、GCの追跡には影響しません。

この変更により、SYS_SETGROUPSシステムコールに渡されるグループIDの配列が、GCによって適切に保護され、システムコール実行中に誤って解放されるリスクが低減されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のガベージコレクションに関する技術記事
  • BSD系OSのkeventシステムコールに関するマニュアルページ
  • unsafe.Pointeruintptrの違いに関するGoコミュニティの議論や解説記事 (具体的なURLは省略しますが、一般的な知識として参照しました)
  • Goのsyscallパッケージの内部実装に関する情報 (具体的なURLは省略しますが、一般的な知識として参照しました) I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The output has been sent to standard output only, as requested.