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

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

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおけるバグ修正に関するものです。具体的には、fmtパッケージが提供する書式設定機能において、非常に大きな負のint64値をバイナリ形式(%b)で出力しようとした際に発生するクラッシュを修正しています。

影響を受けるファイルは以下の通りです。

  • src/pkg/fmt/fmt_test.go: fmtパッケージのテストファイル。このコミットでは、バグを再現し、修正を検証するための新しいテストケースが追加されています。
  • src/pkg/fmt/format.go: fmtパッケージの書式設定ロジックを実装している主要なファイル。このコミットでは、バッファサイズの定義が修正されています。

コミット

  • コミットハッシュ: a662d3d9a757c0556f27d650a9dfe3bf0f2db1bf
  • Author: Rob Pike r@golang.org
  • Date: Fri Apr 13 09:28:37 2012 +1000

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

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

元コミット内容

fmt: fix crash of %b on huge negative int64
The buffer had 64 bytes but needs one more for the sign.

Fixes #3510.

R=golang-dev, dave, dsymonds
CC=golang-dev
https://golang.org/cl/6011057

変更の背景

このコミットの背景には、Go言語のfmtパッケージが、非常に大きな負のint64整数をバイナリ形式(%b)で文字列に変換する際に、プログラムがクラッシュするという深刻なバグが存在していました。

具体的には、int64型は64ビットの符号付き整数であり、その最小値は-2^63です。この値をバイナリで表現すると、符号ビットを含めて64桁の2進数となります。しかし、fmtパッケージ内部で使用されていたバッファのサイズが、この64桁のバイナリ表現に加えて、負の符号(-)を格納するためのスペースを考慮していなかったため、バッファオーバーフローが発生し、結果としてプログラムがクラッシュしていました。

この問題は、Go言語のIssueトラッカーで#3510として報告されていました。コミットメッセージにあるFixes #3510は、このコミットがその特定のバグ報告に対応するものであることを示しています。

前提知識の解説

fmtパッケージ

Go言語のfmtパッケージは、C言語のprintfscanfに似た、書式設定されたI/O(入出力)機能を提供します。これにより、様々なデータ型を文字列に変換したり、文字列からデータを解析したりすることができます。

  • Sprintf関数: fmt.Sprintfは、指定された書式文字列と引数を使用して文字列を生成し、その結果の文字列を返します。ファイルや標準出力に直接書き込むのではなく、文字列として結果を取得したい場合に利用されます。
  • 書式動詞(Verb): fmtパッケージでは、%d(10進数)、%x(16進数)、%s(文字列)など、様々な書式動詞が用意されています。このコミットで問題となっているのは、整数をバイナリ形式で出力するための%bです。

整数表現とint64

  • int64: Go言語におけるint64型は、64ビット幅の符号付き整数型です。これは、約-9 * 10^18から9 * 10^18までの範囲の整数値を表現できます。
  • 負の数の表現(2の補数): コンピュータ内部では、負の数は通常「2の補数」形式で表現されます。64ビットの2の補数表現では、最上位ビット(MSB)が符号ビットとして機能し、0であれば正、1であれば負を示します。
  • バイナリ表現: 整数を2進数で表現したものです。例えば、10進数の5は2進数で101、10進数の-5は2の補数表現で...11111011のように表現されます。int64の最小値である-2^63は、2進数で1000...000(1と63個の0)と表現されます。

バッファ

プログラミングにおいて「バッファ」とは、データを一時的に格納するためのメモリ領域のことです。このコミットの文脈では、fmtパッケージが数値を文字列に変換する際に、その文字列を一時的に保持するために使用するメモリ領域を指します。バッファのサイズが不足すると、書き込もうとするデータがバッファの境界を超えてしまい、メモリ破壊やプログラムのクラッシュを引き起こす可能性があります(バッファオーバーフロー)。

技術的詳細

このバグの根本原因は、fmtパッケージがint64型の数値をバイナリ形式(%b)で書式設定する際に、内部で使用するバッファのサイズ見積もりが不十分だったことにあります。

int64の最小値は-2^63です。この値をバイナリで表現すると、1の後に63個の0が続く形になります(1000000000000000000000000000000000000000000000000000000000000000)。これは64ビット(64桁)のバイナリ表現です。

しかし、負の数を文字列として出力する際には、この64桁のバイナリ表現の前に、負の符号を示すハイフン(-)が追加されます。つまり、int64の最小値をバイナリで出力する場合、合計で1 (符号) + 64 (バイナリ桁数) = 65文字が必要になります。

元のコードでは、このバッファサイズを64バイトとして定義していました(nByte = 64)。このサイズでは、符号文字のための1バイトが不足していました。そのため、int64の最小値のような「巨大な負の数」を%bで書式設定しようとすると、fmtパッケージは65バイト目のデータを64バイトのバッファに書き込もうとし、結果としてバッファオーバーフローが発生し、プログラムがクラッシュしていました。

このコミットでは、このバッファサイズを65バイトに増やすことで、符号文字のためのスペースを確保し、バッファオーバーフローを防いでいます。

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

このコミットによるコードの変更は、主に以下の2つのファイルで行われています。

  1. src/pkg/fmt/fmt_test.go:

    --- a/src/pkg/fmt/fmt_test.go
    +++ b/src/pkg/fmt/fmt_test.go
    @@ -461,6 +461,9 @@ var fmttests = []struct {
      	// zero reflect.Value, which formats as <nil>.\n\
      	// This test is just to check that it shows the two NaNs at all.\n\
      	{\"%v\", map[float64]int{math.NaN(): 1, math.NaN(): 2}, \"map[NaN:<nil> NaN:<nil>]\"},\n\
    +\n\
    +\t// Used to crash because nByte didn't allow for a sign.\n\
    +\t{\"%b\", int64(-1 << 63), \"-1000000000000000000000000000000000000000000000000000000000000000\"},\n\
     }\n\
     \n\
     func TestSprintf(t *testing.T) {
    

    この変更では、fmttestsというテストケースのスライスに新しいエントリが追加されています。

    • {"%b", int64(-1 << 63), "-1000000000000000000000000000000000000000000000000000000000000000"} このテストケースは、%b書式動詞を使用してint64の最小値(-1 << 63-2^63と同じ)をフォーマットし、期待される出力文字列が正しいことを検証します。このテストケースは、修正前のバージョンではクラッシュを引き起こすはずでした。
  2. src/pkg/fmt/format.go:

    --- a/src/pkg/fmt/format.go
    +++ b/src/pkg/fmt/format.go
    @@ -10,7 +10,7 @@ import (\n\
     )\n\
      \n\
     const (\n\
    -\tnByte = 64\n\
    +\tnByte = 65 // %b of an int64, plus a sign.\n\
      \n\
     \tldigits = \"0123456789abcdef\"\n\
     \tudigits = \"0123456789ABCDEF\"\n
    

    この変更は、nByteという定数の値を64から65に変更しています。コメントも追加され、int64のバイナリ表現に加えて符号のためのスペースが必要であることが明記されています。

コアとなるコードの解説

src/pkg/fmt/fmt_test.goの変更

追加されたテストケースは、このバグの再現と修正の検証に不可欠です。 int64(-1 << 63)は、Go言語でint64型の最小値を表現する慣用的な方法です。1 << 632^63を意味し、それに-1を掛けることで-2^63、つまりint64の最小値が得られます。 このテストケースが追加されたことで、将来的に同様のバグが再発した場合でも、自動テストによって早期に検出できるようになります。

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

nByte定数は、fmtパッケージが数値を文字列に変換する際に使用する内部バッファの最大サイズを定義していました。 元の値64は、64ビットの整数が最大で64桁のバイナリ表現になることを想定していましたが、負の数、特にint64の最小値の場合、そのバイナリ表現の前に負の符号(-)が付くため、合計で65文字が必要となります。 nByte = 65への変更は、この不足していた1バイトのスペースを確保し、バッファオーバーフローを防ぐための直接的な修正です。この変更により、fmtパッケージはint64の最小値を%bで正しく書式設定できるようになりました。

関連リンク

参考にした情報源リンク

  • コミットメッセージ
  • Go言語のfmtパッケージに関する一般的な知識
  • Go言語における整数型とビット演算に関する知識
  • 2の補数表現に関する一般的な知識
  • Web検索: "golang issue 3510" (ただし、この検索結果は、このコミットが参照する2012年のIssue 3510とは異なる、より新しいgoplsのクラッシュレポートに関するものであり、直接的な関連性は見られませんでした。)