[インデックス 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
に渡されるchanges
とevents
の配列が、システムコールが完了するまで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.go
、zsyscall_freebsd_amd64.go
などのzsyscall_*.go
という命名規則に従っています。これらのファイルは、通常、Goのツールチェーンによって自動生成されます。これは、Goのsyscall
パッケージが、各OSおよびアーキテクチャ固有のシステムコール定義を、Cのヘッダーファイルなどから自動的にGoのコードに変換していることを示唆しています。
このコミットでこれらのファイルが変更されているということは、syscall
パッケージの生成ロジック自体、または生成元となるテンプレートや定義が更新され、uintptr
の代わりにunsafe.Pointer
を使用するように変更されたことを意味します。これにより、Goのシステムコールバインディング全体で、ポインタの安全な扱いが一貫して適用されるようになります。
コアとなるコードの変更箇所
このコミットでは、主にsrc/pkg/syscall/syscall_bsd.go
とsrc/pkg/syscall/exec_linux.go
、そして多数のsrc/pkg/syscall/zsyscall_*.go
ファイルが変更されています。ここでは、代表的な変更箇所としてsyscall_bsd.go
とexec_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
関数に焦点を当てています。
-
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
システムコールに渡されるchange
とevent
の引数が、uintptr
ではなくunsafe.Pointer
として扱われるように変更されたことを示しています。これにより、GoのGCがこれらのポインタを追跡できるようになります。 -
Kevent
関数内の変数宣言の変更: 元の宣言:var change, event uintptr
変更後:var change, event unsafe.Pointer
Kevent
関数内でkevent
システムコールに渡すためのローカル変数change
とevent
も、uintptr
からunsafe.Pointer
に変更されています。これは、kevent
関数のシグネチャ変更と整合性を保つためです。 -
ポインタ変換の変更: 元のコード:
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
システムコールに渡されるchanges
とevents
の配列が、GoのGCによって正しく認識され、システムコール実行中に誤って解放されるリスクがなくなります。
exec_linux.go
の変更解説
この変更は、Linuxシステムにおけるプロセス実行(forkAndExecInChild
関数)の一部で、SYS_SETGROUPS
システムコールに渡されるグループIDの配列に関連しています。
-
groups
変数の宣言変更: 元の宣言:groups := uintptr(0)
変更後:var groups unsafe.Pointer
SYS_SETGROUPS
システムコールに渡すグループIDの配列のポインタを保持するgroups
変数が、uintptr
からunsafe.Pointer
に変更されています。 -
ポインタ変換の変更: 元のコード:
groups = uintptr(unsafe.Pointer(&cred.Groups[0]))
変更後:groups = unsafe.Pointer(&cred.Groups[0])
cred.Groups
スライス(グループIDの配列)の最初の要素のアドレスをunsafe.Pointer
に変換し、それをgroups
変数に代入しています。これにより、cred.Groups
配列がGCによって保護されるようになります。 -
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言語の
unsafe
パッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe - Go言語の
syscall
パッケージに関する公式ドキュメント: https://pkg.go.dev/syscall - Go言語のガベージコレクションに関する情報 (Goの公式ブログなど):
- Go's Garbage Collector: https://go.dev/blog/go15gc (Go 1.5のGCに関する記事ですが、基本的な概念は共通です)
- kqueue(2) - FreeBSD System Calls Manual: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のガベージコレクションに関する技術記事
- BSD系OSの
kevent
システムコールに関するマニュアルページ unsafe.Pointer
とuintptr
の違いに関する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.