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

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

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおける、ゼロパディングされた大きな16進数(hexadecimal)の出力に関するバグ修正を目的としています。具体的には、内部バッファから割り当てられたバッファへの切り替えを計算する際に、「0x」の幅が考慮されていなかった問題に対処しています。

コミット

commit fc908a0298f574948ebf4eab62cf319319e77020
Author: Rob Pike <r@golang.org>
Date:   Thu Jan 16 09:48:23 2014 -0800

    fmt: fix bug printing large zero-padded hexadecimal
    We forgot to include the width of "0x" when computing the crossover
    from internal buffer to allocated buffer.
    Also add a helper function to the test for formatting large zero-padded
    test strings.
    
    Fixes #6777.
    
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/50820043

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

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

元コミット内容

Go言語のfmtパッケージにおいて、大きなゼロパディングされた16進数を表示する際のバグを修正します。このバグは、内部バッファから動的に割り当てられるバッファへ切り替える際の計算で、「0x」プレフィックスの幅が考慮されていなかったことに起因します。また、大きなゼロパディングされたテスト文字列をフォーマットするためのヘルパー関数をテストに追加します。

このコミットはIssue #6777を修正します。

変更の背景

Go言語のfmtパッケージは、様々なデータ型をフォーマットして文字列として出力するための機能を提供します。この機能には、数値のゼロパディング(指定された幅になるまで先行するゼロで埋めること)や、16進数形式での出力が含まれます。

コミットメッセージによると、このバグは、特に大きな数値をゼロパディングして16進数形式で出力する際に発生していました。fmtパッケージは、効率のために小さな出力に対しては内部の固定サイズバッファを使用し、より大きな出力が必要な場合には動的にメモリを割り当ててバッファを拡張する仕組みを持っています。

問題は、16進数出力で#フラグ(%#xなど)を使用した場合に付加される「0x」プレフィックスの扱いにありました。この「0x」は出力文字列の幅に影響を与えますが、内部バッファから動的バッファへの切り替えを判断する際の幅の計算において、この2文字分が考慮されていませんでした。その結果、必要なバッファサイズが過小評価され、バッファオーバーフローや、予期せぬ出力の切り捨て、あるいはパニック(プログラムの異常終了)といった問題を引き起こす可能性がありました。

この修正は、このようなエッジケースにおけるfmtパッケージの堅牢性と正確性を向上させることを目的としています。

前提知識の解説

Go言語のfmtパッケージ

fmtパッケージは、Go言語におけるI/Oフォーマット機能を提供します。C言語のprintfscanfに似た機能を提供し、様々なデータ型を整形して出力したり、文字列からデータを読み取ったりすることができます。

  • フォーマット動詞: %d (10進数), %x (16進数), %b (2進数), %f (浮動小数点数) など、出力形式を指定する文字です。
  • フラグ: フォーマット動詞と組み合わせて、出力の挙動を制御します。
    • # (%#x): 16進数や8進数で出力する際に、それぞれ0x0のプレフィックスを付加します。
    • 0 (%0w): 指定された幅wになるまで、先行するゼロでパディングします。
  • 幅 (Width): 出力される文字列の最小幅を指定します。例えば、%10dは10文字幅で10進数を出力し、必要に応じてスペースでパディングします。
  • 精度 (Precision): 浮動小数点数や文字列の出力において、小数点以下の桁数や文字列の最大長を指定します。
  • 内部バッファと動的バッファ: fmtパッケージのようなフォーマットライブラリでは、パフォーマンスのために、ある程度のサイズの出力まではスタック上に確保された固定サイズの内部バッファ(このコミットではf.intbufがこれに該当)を使用します。しかし、出力がそのサイズを超えると、ヒープ上に動的に大きなバッファを割り当てて処理を続行します。この切り替えのロジックが正確でないと、バッファオーバーフローやデータ破損の原因となります。

16進数表現と「0x」プレフィックス

16進数(Hexadecimal)は、基数16の数値表現です。0-9とA-F(またはa-f)の16種類の記号を使用します。Go言語のfmtパッケージで%xフォーマット動詞を使用すると16進数で出力されます。%#xのように#フラグを付けると、出力される16進数の前に0xというプレフィックスが付加され、これが16進数であることを明示します。この「0x」は2文字分の幅を持ちます。

バッファ管理とオーバーフロー

プログラムがデータを処理する際、一時的にデータを格納するためにバッファを使用します。バッファには固定サイズのものと、必要に応じてサイズを拡張できる動的なものがあります。バッファオーバーフローは、プログラムがバッファにその容量を超えるデータを書き込もうとしたときに発生します。これはセキュリティ上の脆弱性やプログラムのクラッシュにつながる可能性があります。このコミットでは、バッファの切り替え判断における幅の計算ミスが、潜在的なバッファオーバーフローの原因となっていたと考えられます。

技術的詳細

このコミットの技術的な核心は、fmtパッケージのformat.goファイル内のintegerメソッドにおけるバッファサイズ計算の修正にあります。

integerメソッドは、整数値を様々な基数(10進数、16進数など)でフォーマットする役割を担っています。このメソッド内で、出力文字列の幅が内部バッファnByte(65バイト)を超える場合に、より大きな動的バッファを割り当てるロジックが存在します。

修正前のコードでは、動的バッファを割り当てるかどうかの判断基準となるf.wid(指定された最小幅)が、%#xのように0xプレフィックスが付加されるケースで、その2文字分の幅を考慮していませんでした。

修正後のコードでは、以下の変更が加えられました。

  1. format.gonByte定数のコメントが更新され、16進数で0xが追加される場合を特別に扱う必要があることが明記されました。
  2. func (f *fmt) integer(...) メソッド内で、f.widPresent(幅が指定されているか)がtrueの場合に、width変数を導入しました。
  3. base == 16 && f.sharp(基数が16進数で、#フラグが設定されている)という条件が追加されました。この条件が真の場合、widthに2(0xの幅)が加算されます。
  4. この調整されたwidthnByteを超える場合にのみ、新しい動的バッファがmake([]byte, width)によって割り当てられるようになりました。

これにより、0xプレフィックスが付加される場合でも、必要なバッファサイズが正確に計算され、適切なサイズのバッファが確保されるようになります。

また、fmt_test.goには、この修正を検証するための新しいテストケースとヘルパー関数zeroFillが追加されました。zeroFill関数は、指定されたプレフィックス、幅、サフィックスに基づいてゼロパディングされた文字列を生成し、テストの期待値を簡潔に記述できるようにします。これにより、非常に大きなゼロパディングされた数値の出力が正しく行われることを確認しています。特に、%#064x%#072xのような、0xプレフィックスと大きな幅を組み合わせたケースが追加され、バグが修正されたことを確認しています。

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

src/pkg/fmt/format.go

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -10,7 +10,9 @@ import (
 )
 
 const (
-	nByte = 65 // %b of an int64, plus a sign.
+	// %b of an int64, plus a sign.
+	// Hex can add 0x and we handle it specially.
+	nByte = 65
 
 	ldigits = "0123456789abcdef"
 	udigits = "0123456789ABCDEF"
@@ -160,9 +162,16 @@ func (f *fmt) integer(a int64, base uint64, signedness bool, digits string) {
 	}
 
 	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)
+	if f.widPresent {
+		width := f.wid
+		if base == 16 && f.sharp {
+			// Also adds "0x".
+			width += 2
+		}
+		if width > nByte {
+			// We're going to need a bigger boat.
+			buf = make([]byte, width)
+		}
 	}
 
 	negative := signedness == signed && a < 0

src/pkg/fmt/fmt_test.go

--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -518,6 +518,33 @@ var fmtTests = []struct {
 
 	// Incomplete format specification caused crash.
 	{"%.\", 3, "%!.(int=3)"},
+\n+\t// Used to panic with out-of-bounds for very large numeric representations.\n+\t// nByte is set to handle one bit per uint64 in %b format, with a negative number.\n+\t// See issue 6777.\n+\t{"%#064x", 1, zeroFill("0x", 64, "1")},\n+\t{"%#064x", -1, zeroFill("-0x", 63, "1")},\n+\t{"%#064b", 1, zeroFill("", 64, "1")},\n+\t{"%#064b", -1, zeroFill("-", 63, "1")},\n+\t{"%#064o", 1, zeroFill("", 64, "1")},\n+\t{"%#064o", -1, zeroFill("-", 63, "1")},\n+\t{"%#064d", 1, zeroFill("", 64, "1")},\n+\t{"%#064d", -1, zeroFill("-", 63, "1")},\n+\t// Test that we handle the crossover above the size of uint64\n+\t{"%#072x", 1, zeroFill("0x", 72, "1")},\n+\t{"%#072x", -1, zeroFill("-0x", 71, "1")},\n+\t{"%#072b", 1, zeroFill("", 72, "1")},\n+\t{"%#072b", -1, zeroFill("-", 71, "1")},\n+\t{"%#072o", 1, zeroFill("", 72, "1")},\n+\t{"%#072o", -1, zeroFill("-", 71, "1")},\n+\t{"%#072d", 1, zeroFill("", 72, "1")},\n+\t{"%#072d", -1, zeroFill("-", 71, "1")},\n+}\n+\n+// zeroFill generates zero-filled strings of the specified width. The length\n+// of the suffix (but not the prefix) is compensated for in the width calculation.\n+func zeroFill(prefix string, width int, suffix string) string {\n+\treturn prefix + strings.Repeat("0", width-len(suffix)) + suffix\n }\n \n func TestSprintf(t *testing.T) {

コアとなるコードの解説

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

  • nByte定数のコメント更新:

    const (
    	// %b of an int64, plus a sign.
    	// Hex can add 0x and we handle it specially.
    	nByte = 65
    

    この変更は、nByteが単にint64の2進数表現と符号だけでなく、16進数表現における0xプレフィックスの特殊な扱いも考慮する必要があることを開発者に明確に伝えます。これは、後続のコード変更の意図を補強するものです。

  • integerメソッド内のバッファサイズ計算ロジックの修正:

    	var buf []byte = f.intbuf[0:]
    	if f.widPresent { // 幅が指定されている場合
    		width := f.wid // 指定された幅を初期値とする
    		if base == 16 && f.sharp { // 16進数でかつ '#' フラグが指定されている場合
    			// Also adds "0x".
    			width += 2 // 幅に "0x" の2文字分を加算
    		}
    		if width > nByte { // 調整された幅が内部バッファサイズを超える場合
    			// We're going to need a bigger boat.
    			buf = make([]byte, width) // より大きな動的バッファを割り当てる
    		}
    	}
    

    これがこのコミットの最も重要な変更点です。

    • 以前はf.wid > nByteという単純な比較でしたが、f.widPresentのチェックの中に、width変数を導入し、base == 16 && f.sharpの条件でwidth2を加算するロジックが追加されました。
    • これにより、%#xのように0xプレフィックスが付加される場合に、その2文字分の幅がバッファサイズ計算に正しく反映されるようになりました。
    • 結果として、動的バッファが必要な場合に、適切なサイズのバッファが割り当てられるようになり、バッファオーバーフローや関連する問題が回避されます。

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

  • zeroFillヘルパー関数の追加:

    func zeroFill(prefix string, width int, suffix string) string {
    	return prefix + strings.Repeat("0", width-len(suffix)) + suffix
    }
    

    この関数は、テストケースで期待されるゼロパディングされた文字列を生成するためのユーティリティです。プレフィックス、指定された全体の幅、およびサフィックスを受け取り、その間の部分をゼロで埋めます。サフィックスの長さは全体の幅から差し引かれ、プレフィックスはそのまま追加されます。これにより、テストコードがより簡潔になり、期待される出力の構造が明確になります。

  • 新しいテストケースの追加:

    	// Used to panic with out-of-bounds for very large numeric representations.
    	// nByte is set to handle one bit per uint64 in %b format, with a negative number.
    	// See issue 6777.
    	{"%#064x", 1, zeroFill("0x", 64, "1")},
    	{"%#064x", -1, zeroFill("-0x", 63, "1")},
    	// ... (他のフォーマット動詞と幅の組み合わせ) ...
    	// Test that we handle the crossover above the size of uint64
    	{"%#072x", 1, zeroFill("0x", 72, "1")},
    	{"%#072x", -1, zeroFill("-0x", 71, "1")},
    	// ... (他のフォーマット動詞と幅の組み合わせ) ...
    

    これらの新しいテストケースは、特に大きな幅と#フラグを組み合わせた16進数、2進数、8進数、10進数のフォーマットを対象としています。

    • %#064x%#072xのようなケースは、0xプレフィックスとゼロパディングが組み合わされた際のバッファ計算の正確性を検証します。
    • nByteのサイズ(65)に近い幅(64)や、それを超える幅(72)のテストケースが含まれており、内部バッファから動的バッファへの切り替えが正しく行われることを確認しています。
    • これらのテストは、以前パニックを引き起こしていた可能性のあるシナリオを網羅し、修正が正しく機能していることを保証します。

関連リンク

参考にした情報源リンク