[インデックス 15817] ファイルの概要
このコミットは、Go言語のランタイムにおけるスリープルーチン、特にfutex
関連のシステムコール呼び出しにおいて発生するtv_sec
の32ビットオーバーフローを修正するものです。これにより、非常に長いスリープ時間を指定した場合に、予期せぬ動作や即時タイムアウトが発生する可能性があったバグが解消されます。
コミット
commit ba50e4f1203ad5cc20b7ada2fce4da62ab195622
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Mar 18 20:11:11 2013 +0100
runtime: fix tv_sec 32-bit overflows in sleep routines.
Fixes #5063.
R=golang-dev, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/7876043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ba50e4f1203ad5cc20b7ada2fce4da62ab195622
元コミット内容
runtime: fix tv_sec 32-bit overflows in sleep routines.
Fixes #5063.
R=golang-dev, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/7876043
変更の背景
この変更は、Goランタイムが内部的に使用するスリープや待機のためのシステムコールにおいて、タイムアウト値を計算する際に発生する潜在的なバグを修正するために行われました。具体的には、timespec
構造体(またはそれに類するもの)の秒数部分であるtv_sec
が32ビット整数として扱われる環境において、非常に長い時間(例えば数十年以上)のスリープを要求した場合に、このtv_sec
がオーバーフローしてしまう問題がありました。
オーバーフローが発生すると、指定された長い時間が正しく表現できなくなり、結果としてスリープが即座に終了したり、予期せぬ短い時間で終了したりする可能性がありました。これは、Goプログラムが長時間待機するようなシナリオ(例: サーバーアプリケーションのアイドル状態、バックグラウンドジョブの待機)において、信頼性の問題を引き起こす可能性があります。
この問題はGoのIssue #5063として報告されており、このコミットはその問題を解決することを目的としています。
前提知識の解説
timespec
構造体とtv_sec
, tv_nsec
Unix系システムでは、時間間隔や絶対時刻を表現するためにstruct timespec
という構造体が広く用いられます。この構造体は通常、以下のように定義されます。
struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /* ナノ秒 (0から999,999,999まで) */
};
tv_sec
: 秒数を表します。型はtime_t
で、これはシステムによって32ビットまたは64ビット整数として定義されます。tv_nsec
: ナノ秒数を表します。
問題は、time_t
が32ビット整数として定義されているシステム(特に古いシステムや特定のアーキテクチャ)において発生します。32ビット符号付き整数の最大値は約2 * 10^9 (2^31 - 1) です。これは秒数に換算すると約68年です。もし、これを超える秒数をtv_sec
に設定しようとすると、オーバーフローが発生し、負の値になったり、非常に小さな正の値になったりします。システムコールによっては、負のタイムアウト値を「即時タイムアウト」と解釈するものもあり、これがバグの原因となります。
futex
(Fast Userspace muTEX)
futex
はLinuxカーネルが提供する同期プリミティブで、ユーザー空間での効率的なロックや待機メカニズムを実現するために使用されます。futex
は、競合が少ない場合にはカーネルへの移行(コンテキストスイッチ)を避けてユーザー空間で処理を完結させ、競合が発生した場合にのみカーネルの助けを借りてスレッドをスリープさせたり、ウェイクアップさせたりします。
Goランタイムは、ゴルーチンのスケジューリングや同期のために、内部的にfutex
のようなOSが提供する低レベルの同期メカニズムを利用しています。futexsleep
関数は、指定されたアドレスの値を監視し、特定の値になるまで、または指定されたタイムアウト期間が経過するまでゴルーチンをスリープさせるためのGoランタイム内部の関数です。
タイムアウトの計算
多くのシステムコールでは、タイムアウト値を秒とナノ秒に分けて指定します。Goランタイムでは、ナノ秒単位で指定されたスリープ時間(int64 ns
)を、これらのシステムコールに渡すために秒とナノ秒に変換する必要があります。
ns / 1000000000LL
で秒数を計算し、ns % 1000000000LL
でナノ秒の残りを計算します。ここで、1000000000LL
はリテラルがlong long
型であることを明示しています。
技術的詳細
このコミットの核心は、GoランタイムがOS固有のスリープ/待機関数を呼び出す際に、タイムアウトの秒数部分(tv_sec
に相当する値)が32ビット整数でオーバーフローするのを防ぐための対策です。
変更は、主に以下のOS固有のランタイムファイルに適用されています。
src/pkg/runtime/os_darwin.c
(macOS)src/pkg/runtime/os_freebsd.c
(FreeBSD)src/pkg/runtime/os_linux.c
(Linux)src/pkg/runtime/os_netbsd.c
(NetBSD)src/pkg/runtime/os_openbsd.c
(OpenBSD)
これらのファイルでは、ns
(ナノ秒単位のタイムアウト時間、int64
型)を秒数(secs
)とナノ秒に分解する際に、秒数部分に上限を設けるロジックが追加されています。
具体的には、ns / 1000000000LL
で計算された秒数secs
が、1LL<<30
(2の30乗、つまり1,073,741,824秒)を超える場合に、secs
の値を1LL<<30
にクランプ(上限を設定)しています。
int64 secs;
// ...
secs = ns / 1000000000LL;
// Avoid overflow
if(secs > 1LL<<30)
secs = 1LL<<30;
// ... ts.tv_sec = secs;
なぜ1LL<<30
という値が選ばれたのでしょうか?
32ビット符号付き整数の最大値は2^31 - 1
です。1LL<<30
は2^30
であり、これは2^31 - 1
よりも小さい値です。この値にクランプすることで、tv_sec
が32ビット整数であるシステムでもオーバーフローすることなく、表現可能な最大値に近い、しかし安全な範囲に収めることができます。
1LL<<30
秒は約34年です。これは、Goランタイムが通常扱うタイムアウトとしては十分に長い時間であり、これを超えるような非常に長いタイムアウトは、実用上は無限大に近いものとして扱っても問題ないという判断があったと考えられます。また、一部のシステムコール実装では、非常に大きなタイムアウト値に対して特別な振る舞いをすることがあるため、このような上限設定が安定性向上に寄与する可能性もあります。
新しいテストケース
このコミットでは、export_futex_test.go
とfutex_test.go
という2つの新しいテストファイルが追加されています。
export_futex_test.go
:runtime
パッケージの内部関数であるfutexsleep
とfutexwakeup
をテストのためにエクスポートするためのファイルです。Goのテストフレームワークは通常、エクスポートされた関数のみをテストできますが、ランタイムの低レベル関数をテストするためにこのようなメカニズムが用いられます。futex_test.go
:TestFutexsleep
というテスト関数が含まれています。このテストは、futexsleep
に意図的に非常に大きなタイムアウト値(1<<31+100)*1e9
ナノ秒(約2^31秒 + 100ナノ秒)を渡しています。この値は、修正前のコードであればtv_sec
がオーバーフローするような値です。 テストのロジックは以下の通りです。futexsleep
を別のゴルーチンで呼び出し、非常に長いタイムアウトを設定します。- メインゴルーチンでは、1秒後に
futexwakeup
を呼び出して、スリープ中のゴルーチンを強制的に起こします。 - もし
futexsleep
が1秒以内に(オーバーフローによって)早期に終了した場合、テストは失敗します。 このテストは、tv_sec
のオーバーフローが修正され、futexsleep
が正しく長い時間待機できるようになったことを検証します。
コアとなるコードの変更箇所
変更は、各OS固有のランタイムファイル内のスリープ関連関数(例: runtime·mach_semacquire
, runtime·futexsleep
, runtime·semasleep
)におけるタイムアウト値の計算部分です。
例として、src/pkg/runtime/os_linux.c
のruntime·futexsleep
関数の変更を見てみましょう。
変更前:
void
runtime·futexsleep(uint32 *addr, uint32 val, int64 ns)
{
Timespec ts, *tsp;
if(ns < 0)
tsp = nil;
else {
ts.tv_sec = ns/1000000000LL;
ts.tv_nsec = ns%1000000000LL;
// Avoid overflow
if(ts.tv_sec > 1<<30)
ts.tv_sec = 1<<30;
tsp = &ts;
}
// ... futexシステムコール呼び出し ...
}
変更後:
void
runtime·futexsleep(uint32 *addr, uint32 val, int64 ns)
{
Timespec ts, *tsp;
int64 secs; // 新しく導入された変数
if(ns < 0)
tsp = nil;
else {
secs = ns/1000000000LL; // まずsecsに計算
// Avoid overflow
if(secs > 1LL<<30) // secsをクランプ
secs = 1LL<<30;
ts.tv_sec = secs; // クランプされたsecsをtv_secに代入
ts.tv_nsec = ns%1000000000LL;
tsp = &ts;
}
// ... futexシステムコール呼び出し ...
}
同様の変更が、他のOS固有のファイルにも適用されています。
コアとなるコードの解説
変更の目的は、ns
(ナノ秒)から秒数を計算する際に、その結果が32ビット整数で表現できる範囲に収まるようにすることです。
-
int64 secs;
ns
がint64
であるため、秒数を計算した結果も一時的にint64
で保持するためにsecs
変数が導入されました。これにより、ns / 1000000000LL
の計算結果がint64
の範囲で正確に保持されます。 -
secs = ns / 1000000000LL;
ナノ秒を秒に変換します。LL
サフィックスは、リテラル1000000000
がlong long
型であることを示し、int64
同士の除算を保証します。 -
if(secs > 1LL<<30)
計算された秒数secs
が1LL<<30
(2の30乗秒)よりも大きいかどうかをチェックします。1LL<<30
は、32ビット符号付き整数で安全に表現できる最大秒数に近い値です。 -
secs = 1LL<<30;
もしsecs
が1LL<<30
を超えていた場合、secs
の値を1LL<<30
に制限します。これにより、ts.tv_sec
に代入される値が32ビット整数のオーバーフローを引き起こすことがなくなります。 -
ts.tv_sec = secs;
クランプされたsecs
の値をTimespec
構造体のtv_sec
フィールドに代入します。
この修正により、Goランタイムは、OSが32ビットのtime_t
を使用している場合でも、非常に長いスリープ時間を安全に処理できるようになりました。
関連リンク
- Go Issue #5063: https://github.com/golang/go/issues/5063
- Go CL 7876043: https://golang.org/cl/7876043 (これは古いGerritのリンクであり、現在はGitHubのコミットページにリダイレクトされるか、参照情報として残っています)
参考にした情報源リンク
struct timespec
に関する一般的な情報:futex
に関する一般的な情報:- Go言語のランタイムに関する情報(一般的な理解のため):
- Goの公式ドキュメントやソースコード
- Goの内部動作に関するブログ記事や技術解説