[インデックス 16239] ファイルの概要
このコミットは、Go言語のfmtパッケージにおけるPrintf関数が、不完全なフォーマット指定子(例: %.)を受け取った際に発生するクラッシュを修正するものです。具体的には、Printf("%.", 3)のような呼び出しが原因で発生していたランタイムパニックを解消し、より堅牢なエラーハンドリングを導入しています。
コミット
commit b42c8294ebe5bde8e7368716f909385cf7be148d
Author: Rob Pike <r@golang.org>
Date: Mon Apr 29 13:52:07 2013 -0700
fmt: fix crash for Printf("%.", 3)
Fixes #5311
R=golang-dev, bradfitz, iant
CC=golang-dev
https://golang.org/cl/8961050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b42c8294ebe5bde8e7368716f909385cf7be148d
元コミット内容
fmt: fix crash for Printf("%.", 3)
このコミットは、fmtパッケージのPrintf関数が、Printf("%.", 3)のように不完全なフォーマット指定子(この場合は小数点のみ)と引数を受け取った場合に発生するクラッシュを修正します。
変更の背景
Go言語のfmtパッケージは、C言語のprintfに似た書式設定されたI/O機能を提供します。このパッケージは、様々なデータ型を文字列に変換し、指定されたフォーマットで出力するために広く利用されています。
このコミットが修正する問題は、Printf関数がフォーマット文字列を解析する際に、不完全なフォーマット指定子(例: %.)に遭遇した場合に発生していました。通常、%.の後には精度を示す数値やアスタリスクが続くことが期待されますが、このケースではフォーマット文字列の末尾に%.が来ており、その後に続く文字がない状態でした。
このような状況下で、fmtパッケージの内部処理がフォーマット文字列の範囲外のメモリにアクセスしようとし、結果としてランタイムパニック(クラッシュ)を引き起こしていました。これは、プログラムの予期せぬ終了を招く深刻なバグであり、堅牢なアプリケーション開発においては回避されるべき挙動です。
このバグはGoのIssueトラッカーで#5311として報告されており、このコミットはその報告された問題を解決するために作成されました。
前提知識の解説
Go言語のfmtパッケージ
fmtパッケージは、Go言語における基本的なフォーマットI/O機能を提供します。主な関数にはPrintf、Sprintf、Fprintfなどがあり、これらはフォーマット文字列と可変個の引数を受け取り、整形された文字列を生成または出力します。
フォーマット指定子
fmtパッケージのフォーマット指定子は、C言語のprintfと同様に、%で始まり、その後に続く文字(動詞と呼ばれる)によって引数の型と出力形式を指定します。例えば、%dは整数、%sは文字列、%fは浮動小数点数を表します。
精度指定子
フォーマット指定子の中には、出力の精度を制御するための「精度指定子」があります。これは通常、%と動詞の間に.(ピリオド)を挟んで指定されます。例えば、%.2fは浮動小数点数を小数点以下2桁まで表示することを意味します。
ランタイムパニック
Go言語におけるランタイムパニックは、プログラムの実行中に回復不可能なエラーが発生した場合に起こります。これは通常、nilポインタのデリファレンス、配列の範囲外アクセス、または他の深刻なプログラミングエラーによって引き起こされます。パニックが発生すると、プログラムは通常、スタックトレースを出力して終了します。
parsenum関数
fmtパッケージの内部で使われるparsenum関数は、フォーマット文字列から数値(幅や精度など)を解析するために使用されます。この関数は、フォーマット文字列の現在の位置と終了位置を受け取り、解析された数値と次の解析開始位置を返します。
技術的詳細
このバグは、fmtパッケージがフォーマット文字列を解析する際に、精度指定子(.)の後に続く文字を期待しているにもかかわらず、フォーマット文字列の末尾に到達してしまい、インデックスが範囲外になることで発生していました。
具体的には、src/pkg/fmt/print.go内のdoPrintf関数において、フォーマット文字列中の%の後に幅指定子を解析し、その次に精度指定子を解析するロジックがあります。
元のコードでは、精度指定子をチェックする条件が以下のようになっていました。
if i < end && format[i] == '.' {
この条件は、現在のインデックスiがフォーマット文字列の終端endより小さいこと、およびformat[i]が.であることを確認しています。しかし、この条件が真となった場合、次の行でformat[i+1]にアクセスしようとします。
if format[i+1] == '*' {
問題は、format[i]が.であり、かつiがend - 1(つまり、.がフォーマット文字列の最後の文字)である場合に発生します。このとき、i < endは真ですが、i+1はendと等しくなり、format[i+1]へのアクセスは文字列の範囲外アクセス(out-of-bounds access)となります。これがランタイムパニックの原因でした。
このコミットでは、この脆弱性を修正するために、条件式を以下のように変更しています。
if i+1 < end && format[i] == '.' {
この変更により、format[i+1]にアクセスする前に、i+1がフォーマット文字列の終端endより小さいことを確実にチェックするようになりました。これにより、.がフォーマット文字列の最後の文字である場合、i+1 < endの条件が偽となり、format[i+1]へのアクセスが試みられることはなくなります。結果として、範囲外アクセスによるクラッシュが防止されます。
また、fmt_test.goに新しいテストケースが追加され、この修正が正しく機能することを確認しています。{"%.", 3, "%!.(int=3)"}というテストケースは、Printf("%.", 3)がクラッシュする代わりに、%!.(int=3)というエラーメッセージを出力することを期待しています。これは、Goのfmtパッケージが、不正なフォーマット指定子に対しては、引数の型と値をデバッグ形式で出力するという堅牢なエラーハンドリングポリシーに従っていることを示しています。
コアとなるコードの変更箇所
src/pkg/fmt/fmt_test.go
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -497,6 +497,9 @@ var fmttests = []struct {
// causing +2+0i and +3+0i instead of 2+0i and 3+0i.
{"%v", []complex64{1, 2, 3}, "[(1+0i) (2+0i) (3+0i)]"},
{"%v", []complex128{1, 2, 3}, "[(1+0i) (2+0i) (3+0i)]"},
+
+ // Incomplete format specification caused crash.
+ {"%.\", 3, "%!.(int=3)"},
}
func TestSprintf(t *testing.T) {
src/pkg/fmt/print.go
--- a/src/pkg/fmt/print.go
+++ b/src/pkg/fmt/print.go
@@ -1072,7 +1072,7 @@ func (p *pp) doPrintf(format string, a []interface{}) {
// do we have precision?
- if i < end && format[i] == '.' {
+ if i+1 < end && format[i] == '.' {
if format[i+1] == '*' {
p.fmt.prec, p.fmt.precPresent, i, fieldnum = intFromArg(a, end, i+1, fieldnum)
if !p.fmt.precPresent {
コアとなるコードの解説
src/pkg/fmt/fmt_test.goの変更
新しいテストケース{"%.", 3, "%!.(int=3)"}がfmttestsスライスに追加されました。
- 最初の要素
"%."は、Printf`に渡されるフォーマット文字列です。これは、問題を引き起こしていた不完全な精度指定子です。 - 2番目の要素
3は、Printfに渡される引数です。 - 3番目の要素
"%!.(int=3)"は、この入力に対する期待される出力です。これは、fmtパッケージが不正なフォーマット指定子を検出した場合に生成するエラーメッセージの形式です。%!はエラーを示し、.は元のフォーマット指定子の一部、(int=3)は引数の型と値を示しています。このテストケースの追加により、修正が正しく機能し、クラッシュが回避されるだけでなく、適切なエラーメッセージが生成されることが保証されます。
src/pkg/fmt/print.goの変更
doPrintf関数内のif文の条件がi < end && format[i] == '.'からi+1 < end && format[i] == '.'に変更されました。
- 変更前:
i < end && format[i] == '.'- この条件は、現在のインデックス
iがフォーマット文字列の範囲内であり、かつformat[i]がピリオド(.)であることを確認します。 - しかし、もし
iがend - 1(つまり、ピリオドが文字列の最後の文字)である場合、この条件は真になります。その直後にformat[i+1]へのアクセスが試みられますが、i+1はendと等しくなり、文字列の範囲外アクセスが発生し、パニックを引き起こしていました。
- この条件は、現在のインデックス
- 変更後:
i+1 < end && format[i] == '.'- この新しい条件は、
format[i]がピリオドであることに加えて、i+1がフォーマット文字列の終端endよりも小さいことを確認します。 - これにより、
format[i+1]にアクセスする前に、そのインデックスが常に有効な範囲内にあることが保証されます。 - もしピリオドがフォーマット文字列の最後の文字である場合(
iがend - 1)、i+1 < endは偽となるため、ifブロック内のコード(format[i+1]へのアクセスを含む)は実行されません。これにより、範囲外アクセスが効果的に防止され、クラッシュが回避されます。
- この新しい条件は、
この小さな変更は、fmtパッケージの堅牢性を大幅に向上させ、予期せぬ入力に対する安定性を確保しています。
関連リンク
- Go Issue 5311: https://github.com/golang/go/issues/5311
- Go CL 8961050: https://golang.org/cl/8961050
参考にした情報源リンク
- Go言語公式ドキュメント
fmtパッケージ: https://pkg.go.dev/fmt - Go言語におけるパニックとリカバリー: https://go.dev/blog/defer-panic-and-recover (一般的な情報源として)
- Go言語のソースコード (GitHub): https://github.com/golang/go (一般的な情報源として)