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

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

このコミットは、Go言語の標準ライブラリtimeパッケージにおけるSubメソッドの整数オーバーフロー問題を修正するものです。具体的には、time.SubDuration型(int64)で表現できる範囲を超える結果を生成した場合に、適切な最小値または最大値を返すように変更されています。

コミット

commit fc0b5ef0fdc3bc90d93759b068f9be486f4252da
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date:   Fri Jun 21 18:07:57 2013 -0700

    time: handle integer overflow in Sub
    
    If time.Sub results in a value that won't fit in a Duration (int64),
    return either the min or max int64 value as appropriate.
    
    Fixes #5011.
    
    R=golang-dev, bradfitz, r, rsc
    CC=golang-dev
    https://golang.org/cl/10328043

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

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

元コミット内容

time: handle integer overflow in Sub

If time.Sub results in a value that won't fit in a Duration (int64),
return either the min or max int64 value as appropriate.

Fixes #5011.

R=golang-dev, bradfitz, r, rsc
CC=golang-dev
https://golang.org/cl/10328043

変更の背景

このコミットは、Go言語のtimeパッケージにおけるTime.Subメソッドが、2つのTime値の差を計算する際に発生しうる整数オーバーフローの問題に対処するために行われました。time.Duration型はint64として定義されており、これは約290年間の期間を表現できます。しかし、非常に遠い過去または未来のTime値同士の差を計算しようとすると、このint64の範囲を超えてしまう可能性がありました。

元のSubメソッドの実装では、単純に秒とナノ秒の差を計算してDurationにキャストしていました。この計算結果がint64の最大値(maxDuration)や最小値(minDuration)を超えた場合、予期せぬ値(オーバーフローやアンダーフローによる符号反転など)が返される可能性がありました。これは、プログラムのロジックに誤った時間差が伝播し、バグを引き起こす原因となります。

Go言語のIssue #5011(コミットメッセージに記載されていますが、Web検索では直接的なIssue番号のヒットは確認できませんでした。これは内部的なトラッキング番号であるか、または関連する議論が別のIssueに統合された可能性があります)は、このようなDurationのオーバーフローが問題となるケースを指摘していたと考えられます。特に、time.Durationint64であるため、非常に大きな時間差を表現しようとすると、その限界に達してしまうという根本的な制約があります。このコミットは、この制約内で最も安全かつ予測可能な挙動を提供することを目指しています。

前提知識の解説

  • time.Time: Go言語で特定の日時を表す構造体です。内部的には、エポック(1970年1月1日UTC)からの経過秒数とナノ秒数を保持しています。
  • time.Duration: Go言語で時間の長さを表す型で、int64のエイリアスです。ナノ秒単位で時間を表現し、約290年間の期間を表現できます。
    • int64の最大値は 2^63 - 1 (約 9.22 x 10^18) です。
    • int64の最小値は -2^63 (約 -9.22 x 10^18) です。
    • 1秒は10^9ナノ秒なので、maxDurationは約 9.22 x 10^9 秒、つまり約 292 年に相当します。
  • 整数オーバーフロー/アンダーフロー: 整数型で表現できる最大値を超えた場合に、値が最小値に戻ってしまう現象をオーバーフローと呼びます。逆に、最小値を下回った場合に最大値に戻ってしまう現象をアンダーフローと呼びます。Go言語の整数型は、デフォルトでラップアラウンド(循環)挙動を示します。
  • Time.Sub(u Time) Durationメソッド: tuの間の時間差をDurationとして返します。計算式は t - u です。
  • Time.Add(d Duration) Timeメソッド: tdの期間を加算した新しいTime値を返します。
  • Time.Equal(u Time) boolメソッド: 2つのTime値が等しいかどうかを判定します。
  • Time.Before(u Time) boolメソッド: tuより前の時刻であるかどうかを判定します。

技術的詳細

このコミットの主要な目的は、Time.SubメソッドがDurationint64範囲を超える結果を生成した場合に、オーバーフローやアンダーフローによる不正な値を返すのではなく、Durationの最大値または最小値を返すようにすることです。

変更は主に以下の点で行われています。

  1. minDurationmaxDuration定数の導入: timeパッケージ内に、Duration型の最小値と最大値を表す定数minDurationmaxDurationが導入されました。

    const (
        minDuration Duration = -1 << 63 // int64の最小値
        maxDuration Duration = 1<<63 - 1 // int64の最大値
    )
    

    これらの定数は、int64のビット表現を利用して、それぞれ最小値と最大値を正確に定義しています。

  2. Time.Subメソッドのロジック変更: Time.Subメソッドの内部ロジックが変更され、計算されたDurationint64の範囲内に収まっているかを検証するようになりました。 元の実装は単純に Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec) でした。 新しい実装では、まずdとして通常の計算結果を求めます。

    d := Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec)
    

    次に、このdが正しい値であるかを検証するために、u.Add(d).Equal(t)というチェックを行います。

    • u.Add(d)は、時刻uに計算された期間dを加算した時刻を返します。
    • もしdが正しい期間であれば、u.Add(d)の結果は元の時刻tと等しくなるはずです。
    • このチェックがtrueであれば、dDurationの範囲内に収まっており、正しい値であると判断し、そのままdを返します。

    このチェックがfalseの場合、dはオーバーフローまたはアンダーフローを起こしている可能性が高いと判断し、以下の条件で適切なminDurationまたはmaxDurationを返します。

    • t.Before(u)trueの場合(つまりtuより前の時刻で、差が負になるべき場合)、計算結果がアンダーフローを起こして正の値になったか、または負の範囲外になったと判断し、minDurationを返します。
    • それ以外の場合(つまりtuより後の時刻で、差が正になるべき場合)、計算結果がオーバーフローを起こして負の値になったか、または正の範囲外になったと判断し、maxDurationを返します。

このロジックにより、Subメソッドは常にDurationの有効な範囲内の値を返すことが保証されます。

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

src/pkg/time/time.go

--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -424,6 +424,11 @@ func (t Time) YearDay() int {
 // largest representable duration to approximately 290 years.
 type Duration int64
 
+const (
+	minDuration Duration = -1 << 63
+	maxDuration Duration = 1<<63 - 1
+)
+
 // Common durations.  There is no definition for units of Day or larger
 // to avoid confusion across daylight savings time zone transitions.
 //
@@ -611,10 +616,21 @@ func (t Time) Add(d Duration) Time {
 	return t
 }
 
-// Sub returns the duration t-u.
+// Sub returns the duration t-u. If the result exceeds the maximum (or minimum)
+// value that can be stored in a Duration, the maximum (or minimum) duration
+// will be returned.
 // To compute t-d for a duration d, use t.Add(-d).
 func (t Time) Sub(u Time) Duration {
-\treturn Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec)
+\td := Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec)
+\t// Check for overflow or underflow.
+\tswitch {
+\tcase u.Add(d).Equal(t):
+\t\treturn d // d is correct
+\tcase t.Before(u):
+\t\treturn minDuration // t - u is negative out of range
+\tdefault:
+\t\treturn maxDuration // t - u is positive out of range
+\t}
 }
 
 // Since returns the time elapsed since t.

src/pkg/time/time_test.go

--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -1327,6 +1327,40 @@ func TestLoadFixed(t *testing.T) {
 	}
 }
 
+const (
+	minDuration Duration = -1 << 63
+	maxDuration Duration = 1<<63 - 1
+)
+
+var subTests = []struct {
+	t Time
+	u Time
+	d Duration
+}{
+	{Time{}, Time{}, Duration(0)},
+	{Date(2009, 11, 23, 0, 0, 0, 1, UTC), Date(2009, 11, 23, 0, 0, 0, 0, UTC), Duration(1)},
+	{Date(2009, 11, 23, 0, 0, 0, 0, UTC), Date(2009, 11, 24, 0, 0, 0, 0, UTC), -24 * Hour},
+	{Date(2009, 11, 24, 0, 0, 0, 0, UTC), Date(2009, 11, 23, 0, 0, 0, 0, UTC), 24 * Hour},
+	{Date(-2009, 11, 24, 0, 0, 0, 0, UTC), Date(-2009, 11, 23, 0, 0, 0, 0, UTC), 24 * Hour},
+	{Time{}, Date(2109, 11, 23, 0, 0, 0, 0, UTC), Duration(minDuration)},
+	{Date(2109, 11, 23, 0, 0, 0, 0, UTC), Time{}, Duration(maxDuration)},
+	{Time{}, Date(-2109, 11, 23, 0, 0, 0, 0, UTC), Duration(maxDuration)},
+	{Date(-2109, 11, 23, 0, 0, 0, 0, UTC), Time{}, Duration(minDuration)},
+	{Date(2290, 1, 1, 0, 0, 0, 0, UTC), Date(2000, 1, 1, 0, 0, 0, 0, UTC), 290*365*24*Hour + 71*24*Hour},
+	{Date(2300, 1, 1, 0, 0, 0, 0, UTC), Date(2000, 1, 1, 0, 0, 0, 0, UTC), Duration(maxDuration)},
+	{Date(2000, 1, 1, 0, 0, 0, 0, UTC), Date(2290, 1, 1, 0, 0, 0, 0, UTC), -290*365*24*Hour - 71*24*Hour},
+	{Date(2000, 1, 1, 0, 0, 0, 0, UTC), Date(2300, 1, 1, 0, 0, 0, 0, UTC), Duration(minDuration)},
+}
+
+func TestSub(t *testing.T) {
+	for i, st := range subTests {
+		got := st.t.Sub(st.u)
+		if got != st.d {
+			t.Errorf("#%d: Sub(%v, %v): got %v; want %v", i, st.t, st.u, got, st.d)
+		}
+	}
+}
+
 func BenchmarkNow(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		t = Now()

コアとなるコードの解説

src/pkg/time/time.go

  • minDurationmaxDuration定数: Duration型が取りうる最小値と最大値を明示的に定義しています。これにより、オーバーフロー/アンダーフロー時の戻り値が明確になります。
  • Subメソッドの変更:
    • d := Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec): まず、従来の計算方法でDurationを計算します。このdは、まだオーバーフロー/アンダーフローを起こしている可能性があります。
    • switch文によるオーバーフロー/アンダーフローのチェック:
      • case u.Add(d).Equal(t): return d: この条件が最も重要です。計算されたduに加算し、その結果が元のtと等しい場合、dDurationの範囲内に収まっており、正しい値であると判断されます。このチェックは、int64のラップアラウンド特性を利用して、オーバーフロー/アンダーフローが発生したかどうかを効率的に検出します。もしオーバーフロー/アンダーフローが発生していれば、u.Add(d)の結果はtと一致しないはずです。
      • case t.Before(u): return minDuration: tuより前の時刻であるにもかかわらず、u.Add(d).Equal(t)falseだった場合、これはdがアンダーフローを起こして負の範囲外になったことを意味します。この場合、minDurationを返します。
      • default: return maxDuration: 上記のいずれでもない場合、tuより後の時刻であるにもかかわらず、u.Add(d).Equal(t)falseだったことを意味します。これはdがオーバーフローを起こして正の範囲外になったことを意味します。この場合、maxDurationを返します。

この新しいロジックにより、Subメソッドは常にDurationの有効な範囲内の値を返し、予期せぬ動作を防ぎます。

src/pkg/time/time_test.go

  • minDurationmaxDuration定数: テストファイルでもこれらの定数を再定義しています。これは、テストケース内でこれらの定数を使用するためです。
  • subTests変数の導入: Subメソッドのテストケースを構造体のスライスとして定義しています。これにより、様々なシナリオ(通常の差分、負の差分、オーバーフロー/アンダーフローの境界値)を網羅的にテストできます。
    • 特に注目すべきは、Time{}(ゼロ値の時刻)と非常に遠い未来/過去の時刻との差を計算し、minDurationmaxDurationが返されることを期待するテストケースです。例えば、{Time{}, Date(2109, 11, 23, 0, 0, 0, 0, UTC), Duration(minDuration)}は、ゼロ時刻から約96年後の時刻を引くと、Durationの最小値が返されることを確認しています。
  • TestSub関数の追加: subTestsに定義された各テストケースをループで実行し、Subメソッドの戻り値が期待されるDurationと一致するかどうかを検証しています。これにより、Subメソッドの新しいオーバーフロー/アンダーフロー処理が正しく機能していることが保証されます。

関連リンク

  • Go言語のtimeパッケージのドキュメント: https://pkg.go.dev/time
  • Go言語のDuration型に関する議論(関連する可能性のあるIssue):
    • time.Durationのオーバーフローに関する一般的な議論: https://github.com/golang/go/issues/32501 (このコミットのIssue #5011とは異なるが、Durationの限界に関する理解を深めるのに役立つ)

参考にした情報源リンク

  • コミット情報: /home/orange/Project/comemo/commit_data/16617.txt
  • Go言語のtimeパッケージのソースコード (コミット時点のバージョン): https://github.com/golang/go/tree/fc0b5ef0fdc3bc90d93759b068f9be486f4252da/src/pkg/time
  • Web検索結果: "golang issue 5011 time.Sub overflow" (直接的なIssueは見つからなかったが、time.Subの挙動やDurationの限界に関する一般的な情報が得られた)