[インデックス 16911] ファイルの概要
このコミットは、Goランタイムにおけるtimediv
関数の呼び出しに関する修正です。特にNetBSDとOpenBSD環境での問題を対象としており、timespec
構造体のtv_nsec
フィールドの扱いにおけるエンディアンネスの仮定を明示しています。
コミット
commit 98cc58e2c71284c1f56cf27758091f6f1d7992bf
Author: Russ Cox <rsc@golang.org>
Date: Mon Jul 29 16:31:42 2013 -0400
runtime: fix timediv calls on NetBSD, OpenBSD
Document endian-ness assumption.
R=dvyukov
CC=golang-dev
https://golang.org/cl/12056044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/98cc58e2c71284c1f56cf27758091f6f1d7992bf
元コミット内容
このコミットの元のメッセージは以下の通りです。
runtime: fix timediv calls on NetBSD, OpenBSD
Document endian-ness assumption.
R=dvyukov
CC=golang-dev
https://golang.org/cl/12056044
これは、GoランタイムがNetBSDおよびOpenBSD上でtimediv
関数を呼び出す際の問題を修正し、エンディアンネスに関する仮定をドキュメント化したことを示しています。
変更の背景
このコミットの背景には、Goランタイムがシステムコールを通じて時間関連の操作を行う際の、特定のOS(NetBSD、OpenBSD)におけるtimediv
関数の誤った使用がありました。特に、timespec
構造体のtv_nsec
フィールド(ナノ秒部分)の型と、timediv
関数への引数の渡し方に問題がありました。
timespec
構造体は、秒とナノ秒で時間を表現するための標準的なC言語の構造体です。通常、tv_sec
(秒)とtv_nsec
(ナノ秒)の2つのフィールドを持ちます。tv_nsec
はナノ秒を表し、0から999,999,999の範囲の値を取ります。
amd64
アーキテクチャ(x64としても知られる)では、tv_nsec
フィールドは通常long
型であり、これは64ビット整数(int64
)として機能します。しかし、Goランタイム内のtimediv
関数への呼び出しにおいて、このtv_nsec
の値を32ビット整数へのポインタとして渡そうとしていました。
元のコードでは、timediv
関数にts.tv_nsec
の値をint32
へのポインタとしてキャストして渡していました(例: (int32*)ts.tv_nsec
)。これは、tv_nsec
の実際の値(ナノ秒数)をメモリアドレスとして解釈し、そのアドレスが指す場所を32ビット整数として扱おうとする、根本的に誤った型キャストです。このような操作は、無効なメモリアクセスを引き起こし、プログラムのクラッシュ(セグメンテーション違反など)や未定義の動作につながる可能性が非常に高いです。特に、時間計算のようなクリティカルな部分でのこのようなバグは、システム全体の不安定性やデッドロックなどの深刻な問題を引き起こす可能性があります。
この問題は、NetBSDとOpenBSD環境で顕在化していたようです。Goランタイムにおけるtimediv
関連のバグは、これらのOSでゼロ除算エラーとして現れることがあり、パニックやプログラムの誤動作につながっていました。このコミットは、この根本的な型キャストの誤りを修正し、同時にamd64
システムがリトルエンディアンであることを前提としていることを明示することで、コードの意図を明確にしています。
前提知識の解説
1. timespec
構造体
timespec
は、POSIX標準で定義されている時間構造体で、秒とナノ秒の精度で時間を表現します。
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // ナノ秒 (0から999,999,999)
};
tv_sec
: エポック(通常は1970年1月1日00:00:00 UTC)からの経過秒数。tv_nsec
:tv_sec
で表される秒の後のナノ秒部分。
2. timediv
関数(Goランタイム内部関数)
Goランタイム内部のtimediv
関数は、おそらく大きな時間値(ナノ秒単位のint64
)を、秒とナノ秒に分割するためのユーティリティ関数です。例えば、ns
ナノ秒を1000000000
(10億)で割って秒数と残りのナノ秒数を計算するような処理を行います。
一般的なシグネチャは以下のようになっていると推測されます。
func timediv(val int64, div int64, rem *int32) int64
val
: 割られる値(例: 合計ナノ秒数)div
: 割る値(例: 10億)rem
: 剰余(ナノ秒部分)を格納するためのint32
へのポインタ。
3. エンディアンネス (Endianness)
エンディアンネスとは、マルチバイトのデータをメモリに格納する際のバイト順序のことです。
- リトルエンディアン (Little-endian): 最下位バイト (Least Significant Byte, LSB) が最も小さいメモリアドレスに格納されます。
amd64
アーキテクチャはリトルエンディアンです。 - ビッグエンディアン (Big-endian): 最上位バイト (Most Significant Byte, MSB) が最も小さいメモリアドレスに格納されます。
このコミットでは、tv_nsec
がint64
であるにもかかわらず、timediv
関数が剰余をint32
へのポインタで受け取る設計になっているため、エンディアンネスが重要になります。リトルエンディアンシステムでは、64ビット整数の下位32ビットがメモリの先頭に位置するため、int64
の先頭アドレスをint32*
としてキャストしても、そのint32
がint64
の下位32ビットを正しく指すことになります。tv_nsec
の値(0から999,999,999)は32ビット整数に収まるため、このアプローチが機能します。
4. ポインタと型キャスト
C言語(Goランタイムの一部はCで書かれているか、Cの概念を強く反映している)において、ポインタはメモリ上のアドレスを指します。型キャストは、ある型の値を別の型として解釈する操作です。
(int32*)ts.tv_nsec
:ts.tv_nsec
の値をint32
へのポインタとして解釈しようとする。これは、ts.tv_nsec
がメモリアドレスであるかのように扱うため、通常は誤りであり、クラッシュの原因となります。(int32*)&ts.tv_nsec
:ts.tv_nsec
のアドレスをint32
へのポインタとして解釈しようとする。これは、ts.tv_nsec
が格納されているメモリ位置を、int32
が格納されているかのように扱うことを意味します。tv_nsec
がint64
である場合、このキャストはint64
のメモリ領域の先頭をint32
として指すことになります。リトルエンディアンシステムでは、int64
の下位32ビットがその先頭に位置するため、timediv
がそこに32ビットの剰余を書き込むと、int64
のtv_nsec
の下位32ビットが更新されることになります。
技術的詳細
このコミットの核心は、Goランタイムがtimediv
関数を呼び出す際の引数の渡し方の修正と、その背後にあるエンディアンネスの仮定の明示です。
Goランタイムは、OSのシステムコール(FreeBSDのumtx_op
、Linuxのfutex
、NetBSD/OpenBSDのlwp_park
/thrsleep
など)と連携して、スレッドの待機やタイムアウト処理を行います。これらのシステムコールは、タイムアウト値をtimespec
構造体で受け取ることが一般的です。
問題となっていたのは、timespec
構造体のtv_nsec
フィールドがamd64
アーキテクチャではint64
型であるにもかかわらず、Goランタイム内部のtimediv
関数にその剰余部分を格納するための引数として、誤ったポインタ型を渡していた点です。
修正前:
ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)ts.tv_nsec);
このコードでは、ts.tv_nsec
の値をint32
へのポインタとしてキャストしていました。これは、ts.tv_nsec
に格納されているナノ秒の数値(例えば500,000,000)を、メモリ上のアドレス0x1dcd6500
として解釈し、そのアドレスにtimediv
が計算した剰余を書き込もうとします。しかし、0x1dcd6500
は有効なメモリ領域ではない可能性が高く、結果としてセグメンテーション違反やメモリ破壊を引き起こします。これは非常に危険なバグであり、プログラムのクラッシュや予測不能な動作の原因となります。
修正後:
ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)&ts.tv_nsec);
この修正では、ts.tv_nsec
のアドレスをint32
へのポインタとしてキャストしています。&ts.tv_nsec
はint64*
型ですが、これを(int32*)
にキャストすることで、timediv
関数はts.tv_nsec
がメモリに格納されている場所の先頭32ビットに剰余を書き込むことになります。
この修正が正しく機能するためには、以下の2つの条件が満たされる必要があります。
- 剰余が32ビット整数に収まること:
tv_nsec
はナノ秒を表し、その値は0から999,999,999の範囲です。この値は32ビット符号付き整数(最大約20億)に収まるため、この条件は満たされます。 - システムがリトルエンディアンであること:
amd64
アーキテクチャはリトルエンディアンです。リトルエンディアンシステムでは、64ビット整数(int64
)がメモリに格納される際、その最下位バイト(LSB)が最も低いアドレスに配置されます。したがって、int64
のアドレスをint32*
としてキャストすると、そのポインタはint64
の下位32ビットが格納されているメモリ領域の先頭を指すことになります。timediv
がそこに32ビットの剰余を書き込むと、int64
であるts.tv_nsec
のナノ秒部分が正しく更新されます。
このコミットは、このエンディアンネスの仮定をコードコメントとして明示することで、将来の読者や異なるアーキテクチャへの移植性を考慮した際に、この挙動が意図的であることを明確にしています。
このバグは、特にNetBSDとOpenBSDで問題を引き起こしていたとコミットメッセージに記載されています。これは、これらのOSの特定のシステムコールやランタイムの挙動が、この誤ったポインタキャストによるメモリ破壊をより顕著に表面化させたためと考えられます。
コアとなるコードの変更箇所
変更は以下の4つのファイルにわたります。
src/pkg/runtime/os_freebsd.c
src/pkg/runtime/os_linux.c
src/pkg/runtime/os_netbsd.c
src/pkg/runtime/os_openbsd.c
各ファイルでの変更は非常に類似しており、主にruntime·timediv
関数の第3引数の渡し方と、エンディアンネスに関するコメントの追加です。
src/pkg/runtime/os_freebsd.c
および src/pkg/runtime/os_linux.c
:
--- a/src/pkg/runtime/os_freebsd.c
+++ b/src/pkg/runtime/os_freebsd.c
@@ -54,6 +54,7 @@ runtime·futexsleep(uint32 *addr, uint32 val, int64 ns)
return;
goto fail;
}
+ // NOTE: tv_nsec is int64 on amd64, so this assumes a little-endian system.
ts.tv_nsec = 0;
ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)&ts.tv_nsec);
ret = runtime·sys_umtx_op(addr, UMTX_OP_WAIT_UINT, val, nil, &ts);
src/pkg/runtime/os_netbsd.c
および src/pkg/runtime/os_openbsd.c
:
--- a/src/pkg/runtime/os_netbsd.c
+++ b/src/pkg/runtime/os_netbsd.c
@@ -95,8 +95,9 @@ runtime·semasleep(int64 ns)
runtime·lwp_park(nil, 0, &m->waitsemacount, nil);
} else {
ns += runtime·nanotime();
+ // NOTE: tv_nsec is int64 on amd64, so this assumes a little-endian system.
ts.tv_nsec = 0;
- ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)ts.tv_nsec);
+ ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)&ts.tv_nsec);
// TODO(jsing) - potential deadlock!
// See above for details.
runtime·atomicstore(&m->waitsemalock, 0);
コアとなるコードの解説
各ファイルにおける変更は、以下の2点に集約されます。
-
timediv
関数の第3引数の修正:- 変更前:
(int32*)ts.tv_nsec
- これは
ts.tv_nsec
の値をint32
へのポインタとしてキャストしていました。例えば、ts.tv_nsec
が500000000
という値を持っていた場合、コードはメモリアドレス0x500000000
(約21GB)にアクセスしようとします。これは通常、無効なメモリアクセスであり、プログラムのクラッシュ(セグメンテーション違反)を引き起こします。
- これは
- 変更後:
(int32*)&ts.tv_nsec
- これは
ts.tv_nsec
のアドレスをint32
へのポインタとしてキャストしています。&ts.tv_nsec
はts.tv_nsec
変数がメモリに格納されている実際の場所を指すポインタです。ts.tv_nsec
はint64
型ですが、amd64
アーキテクチャがリトルエンディアンであるため、その下位32ビットがメモリの先頭に位置します。timediv
関数が剰余を32ビット整数としてそのポインタ先に書き込むことで、ts.tv_nsec
のナノ秒部分が正しく更新されます。
- これは
- 変更前:
-
エンディアンネスに関するコメントの追加:
// NOTE: tv_nsec is int64 on amd64, so this assumes a little-endian system.
- このコメントは、
tv_nsec
がamd64
上でint64
型であること、そして上記のポインタキャストが正しく機能するためにはシステムがリトルエンディアンであるという前提があることを明示しています。これにより、コードの意図が明確になり、将来的に異なるアーキテクチャへの移植や、エンディアンネスに関するデバッグを行う際に役立ちます。
- このコメントは、
この修正により、Goランタイムはtimespec
構造体のナノ秒部分を正しく計算し、OSのシステムコールに渡すことができるようになり、NetBSDやOpenBSD環境での時間関連のバグ(特にゼロ除算エラーなど)が解消されました。
関連リンク
- Go issue tracker: https://golang.org/cl/12056044 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
参考にした情報源リンク
timespec
構造体に関する情報:- エンディアンネスに関する情報:
- Goランタイムにおける
timediv
関連のバグ(一般的な情報):- Web検索結果より、Go 1.14.2およびGo 1.13.10で
timediv
関連の修正が行われたという情報。このコミットはそれ以前の修正であり、同様の問題が繰り返し発生していた可能性を示唆しています。
- Web検索結果より、Go 1.14.2およびGo 1.13.10で