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

[インデックス 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<<302^30であり、これは2^31 - 1よりも小さい値です。この値にクランプすることで、tv_secが32ビット整数であるシステムでもオーバーフローすることなく、表現可能な最大値に近い、しかし安全な範囲に収めることができます。 1LL<<30秒は約34年です。これは、Goランタイムが通常扱うタイムアウトとしては十分に長い時間であり、これを超えるような非常に長いタイムアウトは、実用上は無限大に近いものとして扱っても問題ないという判断があったと考えられます。また、一部のシステムコール実装では、非常に大きなタイムアウト値に対して特別な振る舞いをすることがあるため、このような上限設定が安定性向上に寄与する可能性もあります。

新しいテストケース

このコミットでは、export_futex_test.gofutex_test.goという2つの新しいテストファイルが追加されています。

  • export_futex_test.go: runtimeパッケージの内部関数であるfutexsleepfutexwakeupをテストのためにエクスポートするためのファイルです。Goのテストフレームワークは通常、エクスポートされた関数のみをテストできますが、ランタイムの低レベル関数をテストするためにこのようなメカニズムが用いられます。
  • futex_test.go: TestFutexsleepというテスト関数が含まれています。このテストは、futexsleepに意図的に非常に大きなタイムアウト値 (1<<31+100)*1e9 ナノ秒(約2^31秒 + 100ナノ秒)を渡しています。この値は、修正前のコードであればtv_secがオーバーフローするような値です。 テストのロジックは以下の通りです。
    1. futexsleepを別のゴルーチンで呼び出し、非常に長いタイムアウトを設定します。
    2. メインゴルーチンでは、1秒後にfutexwakeupを呼び出して、スリープ中のゴルーチンを強制的に起こします。
    3. もしfutexsleepが1秒以内に(オーバーフローによって)早期に終了した場合、テストは失敗します。 このテストは、tv_secのオーバーフローが修正され、futexsleepが正しく長い時間待機できるようになったことを検証します。

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

変更は、各OS固有のランタイムファイル内のスリープ関連関数(例: runtime·mach_semacquire, runtime·futexsleep, runtime·semasleep)におけるタイムアウト値の計算部分です。

例として、src/pkg/runtime/os_linux.cruntime·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ビット整数で表現できる範囲に収まるようにすることです。

  1. int64 secs; nsint64であるため、秒数を計算した結果も一時的にint64で保持するためにsecs変数が導入されました。これにより、ns / 1000000000LLの計算結果がint64の範囲で正確に保持されます。

  2. secs = ns / 1000000000LL; ナノ秒を秒に変換します。LLサフィックスは、リテラル1000000000long long型であることを示し、int64同士の除算を保証します。

  3. if(secs > 1LL<<30) 計算された秒数secs1LL<<30(2の30乗秒)よりも大きいかどうかをチェックします。1LL<<30は、32ビット符号付き整数で安全に表現できる最大秒数に近い値です。

  4. secs = 1LL<<30; もしsecs1LL<<30を超えていた場合、secsの値を1LL<<30に制限します。これにより、ts.tv_secに代入される値が32ビット整数のオーバーフローを引き起こすことがなくなります。

  5. ts.tv_sec = secs; クランプされたsecsの値をTimespec構造体のtv_secフィールドに代入します。

この修正により、Goランタイムは、OSが32ビットのtime_tを使用している場合でも、非常に長いスリープ時間を安全に処理できるようになりました。

関連リンク

参考にした情報源リンク