[インデックス 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言語のprintf
やscanf
に似た機能を提供し、様々なデータ型を整形して出力したり、文字列からデータを読み取ったりすることができます。
- フォーマット動詞:
%d
(10進数),%x
(16進数),%b
(2進数),%f
(浮動小数点数) など、出力形式を指定する文字です。 - フラグ: フォーマット動詞と組み合わせて、出力の挙動を制御します。
#
(%#x
): 16進数や8進数で出力する際に、それぞれ0x
や0
のプレフィックスを付加します。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文字分の幅を考慮していませんでした。
修正後のコードでは、以下の変更が加えられました。
format.go
のnByte
定数のコメントが更新され、16進数で0x
が追加される場合を特別に扱う必要があることが明記されました。func (f *fmt) integer(...)
メソッド内で、f.widPresent
(幅が指定されているか)がtrue
の場合に、width
変数を導入しました。base == 16 && f.sharp
(基数が16進数で、#
フラグが設定されている)という条件が追加されました。この条件が真の場合、width
に2(0x
の幅)が加算されます。- この調整された
width
がnByte
を超える場合にのみ、新しい動的バッファが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
の条件でwidth
に2
を加算するロジックが追加されました。 - これにより、
%#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)のテストケースが含まれており、内部バッファから動的バッファへの切り替えが正しく行われることを確認しています。- これらのテストは、以前パニックを引き起こしていた可能性のあるシナリオを網羅し、修正が正しく機能していることを保証します。
関連リンク
- Go言語の
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語のソースコードリポジトリ: https://github.com/golang/go
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/50820043
は、このGerritシステムへのリンクです。) - Go言語のIssue Tracker: https://github.com/golang/go/issues (ただし、Issue #6777の具体的な内容は、ウェブ検索では見つけることができませんでした。コミットメッセージ自体が問題の概要を説明しています。)
- Go言語の
fmt
パッケージのソースコード: