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

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

このコミットは、Go言語のfmtパッケージにおけるゼロパディングの不具合を修正するものです。特に、非常に大きなパディングが指定された場合にバッファオーバーフローによるクラッシュが発生する問題と、浮動小数点数のゼロパディングが正しく機能せず、符号が数値の途中に挿入されてしまう問題を解決しています。

コミット

commit f59064de80eed5e7a84d20f1a889859cfa8259f7
Author: Rob Pike <r@golang.org>
Date:   Wed Aug 7 08:38:46 2013 +1000

    fmt: fix up zero padding
    If the padding is huge, we crashed by blowing the buffer. That's easy: make sure
    we have a big enough buffer by allocating in problematic cases.
    Zero padding floats was just wrong in general: the space would appear in the
    middle.
    
    Fixes #6044.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12498043

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

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

元コミット内容

fmt: fix up zero padding

パディングが非常に大きい場合、バッファを使い果たしてクラッシュしていました。これは簡単です。問題のあるケースでは、十分な大きさのバッファを確保するようにします。 浮動小数点数のゼロパディングは、一般的に間違っていました。スペースが途中に現れていました。

Fixes #6044.

R=golang-dev, rsc CC=golang-dev https://golang.org/cl/12498043

変更の背景

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおける2つの主要なバグを修正するために行われました。

  1. 巨大なパディングによるクラッシュ: fmtパッケージの書式設定機能において、非常に大きな幅(パディング)を指定して数値をフォーマットしようとすると、内部で使用される固定サイズのバッファがオーバーフローし、プログラムがクラッシュするという問題がありました。これは、特に予期せぬ大きな入力や、悪意のある入力によってサービス拒否(DoS)攻撃につながる可能性も秘めていました。コミットメッセージにある「blowing the buffer」とは、このバッファオーバーフローを指します。

  2. 浮動小数点数のゼロパディングの誤動作: 浮動小数点数をゼロパディング(例えば%010fのように指定)してフォーマットする際に、期待される結果が得られないという問題がありました。具体的には、負の数の場合、符号(-)が数値の先頭ではなく、ゼロパディングの途中に挿入されてしまうという不具合がありました。これにより、出力の可読性が損なわれ、期待される書式と異なる結果となっていました。

これらの問題は、GoのIssueトラッカーでIssue #6044として報告されていました。このコミットは、これらの報告された問題を解決し、fmtパッケージの堅牢性と正確性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と書式設定の基本を理解しておく必要があります。

  • fmtパッケージ: Go言語の標準ライブラリの一つで、Goのプログラムにおける入力と出力の書式設定を扱います。C言語のprintfscanfに似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータ型を解析したりするのに使われます。
  • 書式動詞 (Format Verbs): fmtパッケージでは、値をどのように文字列に変換するかを制御するために、%d(整数)、%f(浮動小数点数)、%s(文字列)などの書式動詞を使用します。
  • フラグ (Flags): 書式動詞に加えて、出力の挙動をさらに細かく制御するためにフラグを使用できます。
    • 0フラグ: ゼロパディングを指定します。指定された幅に満たない場合、先頭にゼロを埋めます。
    • -フラグ: 左寄せを指定します。デフォルトは右寄せです。
    • +フラグ: 常に符号(正の数には+、負の数には-)を出力します。
    • (スペース)フラグ: 正の数には符号の代わりにスペースを出力します。
  • 幅 (Width): 出力されるフィールドの最小幅を指定します。例えば、%10dは整数を少なくとも10文字幅で出力し、足りない場合はスペースでパディングします。
  • 精度 (Precision): 浮動小数点数や文字列に対して、小数点以下の桁数や出力する文字数を指定します。例えば、%.2fは浮動小数点数を小数点以下2桁で出力します。
  • バッファリング: プログラムがデータを処理する際に、一時的にデータを保持するためのメモリ領域です。効率的なI/O操作のために使用されます。このコミットでは、書式設定の際に使用される内部バッファのサイズが問題となっていました。
  • make関数: Go言語でスライス、マップ、チャネルなどの組み込み型を初期化するために使用される関数です。スライスの場合、容量を指定してメモリを事前に確保することができます。

技術的詳細

このコミットは、src/pkg/fmt/format.go内のfmt構造体のメソッド、特にintegerformatFloatに焦点を当てて修正を行っています。

1. 巨大なパディングによるバッファオーバーフローの修正 (integerメソッド)

integerメソッドは、整数値を書式設定する際に使用されます。元の実装では、内部バッファとしてf.intbufという固定サイズの配列(intbuf [68]byte)を使用していました。この配列のサイズは、64ビット整数の最大値(約20桁)と符号、基数変換などを考慮して設計されていましたが、ユーザーが指定するパディング幅(f.wid)がこの固定バッファのサイズをはるかに超える場合、バッファオーバーフローが発生し、プログラムがクラッシュする可能性がありました。

修正では、以下のロジックが追加されました。

	var buf []byte = f.intbuf[0:]
	if f.widPresent && f.wid > nByte {
		// We're going to need a bigger boat.
		buf = make([]byte, f.wid)
	}

ここで、nByteは数値の桁数(符号を含む)を表します。もしユーザーが指定した幅f.widが、数値自体の桁数nByteよりも大きく、かつf.widPresent(幅が指定されていることを示すフラグ)が真である場合、つまり固定バッファでは足りない可能性がある場合に、make([]byte, f.wid)を使って必要な幅に応じたサイズの新しいスライスを動的に確保するように変更されました。これにより、パディング幅がどれだけ大きくても、適切なサイズのバッファが確保され、バッファオーバーフローが防止されます。

また、バッファのインデックス計算もlen(f.intbuf)からlen(buf)に変更され、動的に確保されたバッファのサイズが正しく考慮されるようになりました。

2. 浮動小数点数のゼロパディングの誤動作修正 (formatFloatメソッド)

formatFloatメソッドは、浮動小数点数を書式設定する際に使用されます。元の実装では、負の浮動小数点数をゼロパディングする場合、符号(-)が数値の途中に挿入されるという問題がありました。これは、fmtパッケージが内部的に数値を文字列に変換し、その後にパディングや符号の処理を行う際に、符号の位置を誤って判断していたためです。

修正では、以下のロジックがformatFloatメソッドの符号処理部分に追加されました。

	// The formatted number starts at slice[1].
	switch slice[1] {
	case '-', '+':
		// If we're zero padding, want the sign before the leading zeros.
		// Achieve this by writing the sign out and padding the postive number.
		if f.zero && f.widPresent && f.wid > len(slice) {
			f.buf.WriteByte(slice[1])
			f.wid--
			f.pad(slice[2:])
			return
		}
		// We're set; drop the leading space.
		slice = slice[1:]
	default:
		// ... (既存のロジック)
	}

このコードブロックは、書式設定された数値の文字列表現(slice)の先頭が符号(-または+)である場合に実行されます。

  • f.zeroが真(ゼロパディングが指定されている)
  • f.widPresentが真(幅が指定されている)
  • f.wid > len(slice)(指定された幅が数値の文字列長よりも大きい、つまりパディングが必要)

これらの条件がすべて満たされる場合、つまりゼロパディングが必要な負の浮動小数点数である場合に、以下の処理が行われます。

  1. f.buf.WriteByte(slice[1]): まず、符号(slice[1]、つまり-または+)を直接出力バッファに書き込みます。
  2. f.wid--: 符号を出力した分、残りのパディング幅を1減らします。
  3. f.pad(slice[2:]): 残りの数値部分(符号を除いたslice[2:])に対してパディング処理を行います。この際、既に符号が出力されているため、残りのパディングは数値の前にゼロを埋める形で行われます。

これにより、符号が常に数値の先頭に配置され、その後にゼロパディングが続くという正しい動作が保証されます。

テストケースの追加

src/pkg/fmt/fmt_test.goには、これらの修正を検証するための新しいテストケースが追加されています。

  • 巨大なパディングのテスト:

    • {"%0100d", 1, ...}: 整数1を100桁のゼロパディングでフォーマットするテスト。
    • {"%0100d", -1, ...}: 整数-1を100桁のゼロパディングでフォーマットするテスト。 これらのテストは、バッファオーバーフローが修正され、正しくゼロパディングされることを確認します。
  • 浮動小数点数のゼロパディングのテスト:

    • {"%0.100f", 1.0, ...}: 浮動小数点数1.0を小数点以下100桁の精度でフォーマットするテスト。
    • {"%0.100f", -1.0, ...}: 浮動小数点数-1.0を小数点以下100桁の精度でフォーマットするテスト。
    • {"%020f", -1.0, "-000000000001.000000"}: 浮動小数点数-1.0を20桁のゼロパディングでフォーマットするテスト。このテストは、符号が数値の先頭に正しく配置されることを特に検証します。元の不具合では、このケースで符号が途中に現れていました。

これらのテストケースは、修正が意図した通りに機能し、以前のバグが再発しないことを保証します。

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

src/pkg/fmt/fmt_test.go

--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -493,6 +493,17 @@ var fmtTests = []struct {
 	// Used to crash because nByte didn't allow for a sign.
 	{"%b", int64(-1 << 63), "-1000000000000000000000000000000000000000000000000000000000000000"},
 
+	// Used to panic.
+	{"%0100d", 1, "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"},
+	{"%0100d", -1, "-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"},
+	{"%0.100f", 1.0, "1.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},
+	{"%0.100f", -1.0, "-1.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},
+
+	// Zero padding floats used to put the minus sign in the middle.
+	{"%020f", -1.0, "-000000000001.000000"},
+	{"%20f", -1.0, "           -1.000000"},
+	{"%0100f", -1.0, "-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001.000000"},
+
 	// Complex fmt used to leave the plus flag set for future entries in the array
 	// causing +2+0i and +3+0i instead of 2+0i and 3+0i.
 	{"%v", []complex64{1, 2, 3}, "[(1+0i) (2+0i) (3+0i)]"},

src/pkg/fmt/format.go

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -160,6 +160,11 @@ func (f *fmt) integer(a int64, base uint64, signedness bool, digits string) {
 	}\n
 	var buf []byte = f.intbuf[0:]
+	if f.widPresent && f.wid > nByte {
+		// We're going to need a bigger boat.
+		buf = make([]byte, f.wid)
+	}
+
 	negative := signedness == signed && a < 0
 	if negative {
 		a = -a
@@ -182,7 +187,7 @@ func (f *fmt) integer(a int64, base uint64, signedness bool, digits string) {
 	// a is made into unsigned ua.  we could make things
 	// marginally faster by splitting the 32-bit case out into a separate
 	// block but it's not worth the duplication, so ua has 64 bits.
-	i := len(f.intbuf)
+	i := len(buf)
 	ua := uint64(a)
 	for ua >= base {
 		i--
@@ -191,7 +196,7 @@ func (f *fmt) integer(a int64, base uint64, signedness bool, digits string) {
 	}\n
 	i--
 	buf[i] = digits[ua]
-	for i > 0 && prec > nByte-i {\n
+	for i > 0 && prec > len(buf)-i {
 		i--
 		buf[i] = '0'
 	}
@@ -354,6 +359,14 @@ func (f *fmt) formatFloat(v float64, verb byte, prec, n int) {
 	// The formatted number starts at slice[1].
 	switch slice[1] {
 	case '-', '+':
+		// If we're zero padding, want the sign before the leading zeros.
+		// Achieve this by writing the sign out and padding the postive number.
+		if f.zero && f.widPresent && f.wid > len(slice) {
+			f.buf.WriteByte(slice[1])
+			f.wid--
+			f.pad(slice[2:])
+			return
+		}
 		// We're set; drop the leading space.
 		slice = slice[1:]
 	default:

コアとなるコードの解説

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

  1. func (f *fmt) integer(...) メソッド内:

    • バッファの動的確保:
      	var buf []byte = f.intbuf[0:]
      	if f.widPresent && f.wid > nByte {
      		// We're going to need a bigger boat.
      		buf = make([]byte, f.wid)
      	}
      
      このコードは、整数をフォーマットする際に使用するバッファのサイズを決定します。以前はf.intbufという固定サイズの配列を直接使用していましたが、この変更により、指定された幅(f.wid)が数値自体の桁数(nByte)よりも大きく、かつ幅が明示的に指定されている場合(f.widPresentがtrue)、f.widのサイズを持つ新しいバイトスライスbufを動的に作成します。これにより、非常に大きなパディング幅が指定されてもバッファオーバーフローが発生しなくなります。コメントの「We're going to need a bigger boat.」は、映画「ジョーズ」の有名なセリフで、より大きなものが必要であることをユーモラスに表現しています。
    • バッファインデックスの修正:
      -	i := len(f.intbuf)
      +	i := len(buf)
      
      この変更は、数値の桁をバッファに書き込む際の開始インデックスiの計算を修正しています。以前は固定バッファf.intbufの長さを基準にしていましたが、動的に確保されたbufスライスの長さを基準にするように変更されました。これにより、正しい位置に数値が書き込まれるようになります。
    • パディングループ条件の修正:
      -	for i > 0 && prec > nByte-i {
      +	for i > 0 && prec > len(buf)-i {
      
      このループは、精度(prec)に基づいてゼロを埋め込む処理を行っています。条件式がlen(buf)-iを使用するように変更されたことで、動的に確保されたbufの現在のサイズと、既に書き込まれた桁数に基づいて、残りのパディングが必要かどうかを正確に判断できるようになりました。
  2. func (f *fmt) formatFloat(...) メソッド内:

    • 浮動小数点数のゼロパディング修正:
      	switch slice[1] {
      	case '-', '+':
      		// If we're zero padding, want the sign before the leading zeros.
      		// Achieve this by writing the sign out and padding the postive number.
      		if f.zero && f.widPresent && f.wid > len(slice) {
      			f.buf.WriteByte(slice[1])
      			f.wid--
      			f.pad(slice[2:])
      			return
      		}
      		// We're set; drop the leading space.
      		slice = slice[1:]
      	default:
      		// ...
      	}
      
      このコードブロックは、フォーマットされた浮動小数点数の文字列表現(slice)の2番目の文字(インデックス1)が符号(-または+)である場合に実行されます。これは、strconv.AppendFloatが生成する文字列が、符号、数値、小数点、指数部という順序で構成されるためです。
      • if f.zero && f.widPresent && f.wid > len(slice): この条件は、ゼロパディングが要求されており(f.zero)、幅が指定されており(f.widPresent)、かつ指定された幅がフォーマットされた数値の文字列長よりも大きい(つまりパディングが必要)場合に真となります。
      • f.buf.WriteByte(slice[1]): 条件が真の場合、まず符号(slice[1])を直接出力バッファf.bufに書き込みます。これにより、符号が常に数値の先頭に配置されることが保証されます。
      • f.wid--: 符号を1文字分出力したため、残りのパディング幅を1減らします。
      • f.pad(slice[2:]): 最後に、符号を除いた残りの数値部分(slice[2:])に対してパディング処理を行います。このf.padメソッドは、残りの幅に応じてゼロまたはスペースを埋め込みます。これにより、符号の後にゼロパディングが続くという正しい動作が実現されます。
      • return: このケースが処理されたら、それ以上の処理は不要なので関数を終了します。
      • slice = slice[1:]: 上記のifブロックに入らなかった場合(例えば、ゼロパディングが不要な場合や、幅が足りない場合など)、元のロジックに従って、sliceから先頭のスペース(もしあれば)を削除し、数値部分のみを処理するようにします。

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

  • 新しいテストケースの追加:
    • {"%0100d", 1, ...}{"%0100d", -1, ...}: 巨大なゼロパディングが整数に適用された場合のテスト。以前のバッファオーバーフローの問題を検証します。
    • {"%0.100f", 1.0, ...}{"%0.100f", -1.0, ...}: 浮動小数点数に高い精度とゼロパディングが適用された場合のテスト。
    • {"%020f", -1.0, "-000000000001.000000"}: 負の浮動小数点数にゼロパディングが適用された場合の、符号の位置に関するテスト。これが最も重要なテストケースで、符号が数値の途中に現れるという以前のバグが修正されたことを確認します。

これらのテストケースは、修正が正しく機能し、以前のバグが再発しないことを保証するための重要な役割を果たしています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのIssueトラッカー
  • Go言語のソースコード
  • Go言語のコードレビューシステム (Gerrit)
  • Go言語のfmtパッケージの内部実装に関する一般的な知識