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

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

このコミットは、Go言語の標準ライブラリ time パッケージにおける ParseDuration 関数が、非常に大きな時間文字列をパースする際に発生する int64 オーバーフローの問題を修正するものです。具体的には、パースされた期間が time.Duration 型(基底型は int64)で表現できる範囲を超えた場合に、不正な値(ランダムな値や負の値)が返される可能性があった問題を解決します。

コミット

commit 4ca6e588e4bac8bffa56dfe42526d7a12e7cb69c
Author: Adam Langley <agl@golang.org>
Date:   Mon Mar 10 12:33:45 2014 -0400

    time: handle int64 overflow in ParseDuration.
    
    Previously, passing a long duration to ParseDuration could result in
    random, even negative, values.
    
    LGTM=r
    R=golang-codereviews, bradfitz, r
    CC=golang-codereviews
    https://golang.org/cl/72120043

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

https://github.com/golang/go/commit/4ca6e588e4bac8bffa56dfe42526d7a12e7cb69c

元コミット内容

time: handle int64 overflow in ParseDuration.

Previously, passing a long duration to ParseDuration could result in
random, even negative, values.

LGTM=r
R=golang-codereviews, bradfitz, r
CC=golang-codereviews
https://golang.org/cl/72120043

変更の背景

Go言語の time パッケージには、期間を表す Duration 型があります。この型は int64 を基底としており、ナノ秒単位で時間を表現します。ParseDuration 関数は、"1h30m" のような文字列から Duration 型の値を生成するために使用されます。

このコミットが導入される前は、ParseDuration 関数が非常に大きな期間(例えば、数百万時間)を表す文字列を処理する際に、内部的な計算で float64 を使用していました。float64int64 よりも広い範囲の値を表現できますが、精度には限界があります。問題は、float64 で計算された結果が int64 の最大値または最小値を超えた場合、その float64 の値を int64 に変換する際にオーバーフローが発生し、予期せぬ結果(例えば、非常に大きな正の期間が負の値として解釈される、またはランダムな値になる)が生じていたことです。

このようなオーバーフローは、プログラムの予期せぬ動作やクラッシュにつながる可能性があり、特に時間計算が重要なシステムでは深刻なバグとなり得ます。このコミットは、この潜在的なデータ破損を防ぎ、ParseDuration 関数が int64 の範囲を超える期間を検出した場合に明示的にエラーを返すようにすることで、堅牢性を向上させることを目的としています。

前提知識の解説

Go言語の time.Duration

Go言語の time パッケージには、時間の期間を表す Duration 型が定義されています。

type Duration int64

この定義からわかるように、Durationint64 のエイリアスであり、ナノ秒単位で期間を保持します。int64 の最大値は約 9.22 × 10^18 です。これは約 292 年に相当します。したがって、292年を超える期間は time.Duration で正確に表現できません。

int64 のオーバーフロー

コンピュータの数値型には、表現できる値の範囲に限りがあります。int64 は64ビットの符号付き整数型であり、その範囲は -2^63 から 2^63 - 1 までです。

  • 最小値: -9,223,372,036,854,775,808 (約 -9.22 × 10^18)
  • 最大値: 9,223,372,036,854,775,807 (約 9.22 × 10^18)

数値計算の結果がこの範囲を超えた場合、オーバーフロー(またはアンダーフロー)が発生します。Go言語では、整数型のオーバーフローはデフォルトでラップアラウンド(例えば、最大値に1を加えると最小値に戻る)しますが、浮動小数点数から整数への変換では、値が範囲外の場合に予期せぬ結果(例えば、最大値または最小値にクリップされる、あるいは実装依存の動作)になることがあります。

float64int64 の変換

float64 は倍精度浮動小数点数であり、int64 よりもはるかに広い範囲の値を表現できますが、その精度は有限です。非常に大きな整数を float64 で表現しようとすると、精度が失われる可能性があります。また、float64 から int64 への変換では、float64 の値が int64 の範囲外である場合、Go言語の仕様では結果が未定義となるか、あるいは特定のプラットフォームでは最大値や最小値にクリップされることがあります。このコミットの背景にある問題は、まさにこの変換時の挙動の不確実性とその結果としての不正な値の生成でした。

技術的詳細

このコミットの技術的な核心は、time.ParseDuration 関数が文字列からパースした期間の値を time.Duration 型(int64)に変換する前に、その値が int64 の表現範囲内に収まっているかを明示的にチェックする点にあります。

ParseDuration 関数は、入力された期間文字列(例: "1h30m", "5s")を解析し、最終的にナノ秒単位の float64f を生成します。この ftime.Duration 型の基底である int64 に変換される直前に、以下のチェックが追加されました。

if f < float64(-1<<63) || f > float64(1<<63-1) {
    return 0, errors.New("time: overflow parsing duration")
}

このコードスニペットは、fint64 の最小値 (-1<<63) より小さいか、または int64 の最大値 (1<<63-1) より大きいかを評価します。

  • 1<<63 は、int64 の最大値 2^63 - 1 の次の値(符号なしの場合の 2^63)を int64 として表現しようとするとオーバーフローし、結果として int64 の最小値 -2^63 となります。したがって、float64(-1<<63)int64 の最小値を float64 で表現したものです。
  • float64(1<<63-1)int64 の最大値を float64 で表現したものです。

もし f がこの int64 の範囲外であれば、errors.New("time: overflow parsing duration") を返して、パースが失敗したことを明示的に通知します。これにより、呼び出し元はオーバーフローを検出し、適切にエラーハンドリングを行うことができます。

また、この変更には、オーバーフローケースをテストするための新しいテストケースが src/pkg/time/time_test.go に追加されています。これは、"3000000h"(300万時間)という非常に大きな期間文字列を ParseDuration に渡した場合に、エラーが返されることを確認するものです。300万時間は約342年に相当し、これは int64 で表現できる約292年という期間を明らかに超えるため、オーバーフローが発生するはずです。

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

src/pkg/time/format.go

--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -1240,5 +1240,8 @@ func ParseDuration(s string) (Duration, error) {
 	if neg {
 		f = -f
 	}
+	if f < float64(-1<<63) || f > float64(1<<63-1) {
+		return 0, errors.New("time: overflow parsing duration")
+	}
 	return Duration(f), nil
 }

src/pkg/time/time_test.go

--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -842,6 +842,7 @@ var parseDurationTests = []struct {
 	{"-.", false, 0},
 	{".s", false, 0},
 	{"+.s", false, 0},
+	{"3000000h", false, 0}, // overflow
 }
 
 func TestParseDuration(t *testing.T) {

コアとなるコードの解説

src/pkg/time/format.go の変更

ParseDuration 関数内で、期間を表す float64 型の変数 f が最終的な Duration 型(int64)に変換される直前に、オーバーフローチェックが追加されました。

if f < float64(-1<<63) || f > float64(1<<63-1) {
    return 0, errors.New("time: overflow parsing duration")
}
  • float64(-1<<63): これは int64 の最小値を float64 にキャストしたものです。1<<63int64 の範囲外の値を生成しようとしますが、Goの整数オーバーフローのルールにより、int64 の最小値(-9223372036854775808)になります。
  • float64(1<<63-1): これは int64 の最大値(9223372036854775807)を float64 にキャストしたものです。

この if 文は、パースされた期間 fint64 で表現できる範囲(int64 の最小値から最大値まで)を超えているかどうかをチェックします。もし範囲外であれば、0 と共に新しいエラー errors.New("time: overflow parsing duration") を返します。これにより、呼び出し元は期間が大きすぎて表現できない場合に、明確なエラーを受け取ることができます。

src/pkg/time/time_test.go の変更

parseDurationTests というテストケースのスライスに、新しいエントリが追加されました。

{"3000000h", false, 0}, // overflow
  • "3000000h": これはテスト対象の期間文字列です。300万時間という非常に大きな値であり、int64Duration 型では表現できないオーバーフローを引き起こすことを意図しています。
  • false: この値は、ParseDuration がこの入力に対してエラーを返すことを期待していることを示します。
  • 0: 期待される Duration の値ですが、このテストケースの主な目的はエラーが返されることの確認であるため、ここではあまり重要ではありません。

このテストケースの追加により、ParseDuration がオーバーフローを正しく検出し、エラーを返すという新しい動作が保証されます。

関連リンク

参考にした情報源リンク