[インデックス 12829] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/ascii85
パッケージにおける、特定の条件下で発生するパニック(実行時エラー)を修正するものです。具体的には、ASCII85エンコーディングの特殊なケースである「4つのゼロバイトが 'z' にエンコードされる」際に、ソーススライスの処理が正しく行われず、結果として「インデックスが範囲外」となるパニックが発生する問題を解決しています。
コミット
commit ac51c1384ab1a9a46247428d1d5d158d4cbc40b0
Author: Dmitry Chestnykh <dchest@gmail.com>
Date: Wed Apr 4 09:52:42 2012 -0400
encoding/ascii85: fix panic caused by special case
Special case for encoding 4 zeros as 'z' didn't
update source slice, causing 'index out of bounds'
panic in destination slice.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5970078
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ac51c1384ab1a9a46247428d1d5d158d4cbc40b0
元コミット内容
このコミットは、encoding/ascii85
パッケージの Encode
関数におけるバグ修正です。具体的には、入力データが4つのゼロバイト(\000\000\000\000
)である場合に、ASCII85の特殊な短縮表記である 'z'
を出力する処理において、ソーススライス src
のポインタが正しく進められていなかったことが問題でした。これにより、後続のエンコード処理で src
スライスへのアクセスが範囲外となり、パニックが発生していました。
変更の背景
ASCII85エンコーディングでは、連続する4つのゼロバイト(\000\000\000\000
)を短縮して1文字の 'z'
で表現するという特殊なルールがあります。これは、データ量を削減し、エンコードされた文字列の長さを短くするための最適化です。
Go言語の encoding/ascii85
パッケージの Encode
関数は、この 'z'
の特殊ケースを処理するロジックを含んでいました。しかし、この特殊ケースが適用された際に、エンコード対象のソースデータを示すスライス src
のポインタが、消費された4バイト分だけ正しく更新されていませんでした。
その結果、Encode
関数が次のエンコードブロックを処理しようとした際に、すでに処理済みであるはずの4バイトを再度参照しようとしたり、あるいはスライスの境界を越えてアクセスしようとしたりすることで、「インデックスが範囲外 (index out of bounds)」という実行時パニックが発生していました。このパニックは、特に大量のゼロバイトを含むデータをエンコードしようとした場合に顕在化する可能性がありました。
このコミットは、この特定のバグを修正し、encoding/ascii85
パッケージの堅牢性と信頼性を向上させることを目的としています。
前提知識の解説
ASCII85エンコーディング
ASCII85(またはBase85)は、バイナリデータをASCII文字列にエンコードするための方式の一つです。主にPostScriptやPDFファイルでバイナリデータを埋め込む際に使用されます。
- 基本的な仕組み: 4バイトのバイナリデータを5バイトのASCII文字に変換します。これにより、Base64エンコーディング(3バイトを4バイトに変換)よりも効率的にデータを表現できます。
- 文字セット: ASCII85は、
!
からu
までの85種類のASCII文字を使用します。 - 特殊な短縮表記 'z': 連続する4つのゼロバイト(
\000\000\000\000
)は、特別な短縮表記として1文字の'z'
にエンコードされます。これは、特にゼロが多いデータ(例: 圧縮されたデータ)において、エンコード後の文字列長をさらに短縮するための最適化です。 - 特殊な短縮表記 'y': 連続する4つのスペース文字(
'y'
にエンコードされます。このコミットでは直接関係ありませんが、ASCII85のもう一つの短縮表記として知られています。
Go言語のスライス (Slice)
Go言語のスライスは、配列のセグメント(部分)を参照するための軽量なデータ構造です。スライスは、基となる配列へのポインタ、長さ(len
)、容量(cap
)の3つの要素で構成されます。
- ポインタ: スライスが参照する基となる配列の開始位置を指します。
- 長さ (len): スライスに含まれる要素の数です。
- 容量 (cap): スライスの開始位置から基となる配列の終わりまでの要素の数です。
スライスを操作する際、slice = slice[low:high]
のように再スライスすることで、スライスが参照する範囲を変更できます。この操作は、基となる配列のデータをコピーするわけではなく、単にスライスのポインタ、長さ、容量を更新するだけです。
今回の問題は、src
スライスを再スライスする際に、'z'
の特殊ケースで消費された4バイト分だけポインタを正しく進めなかったために発生しました。これにより、src
スライスがまだ処理されていないデータを指していると誤解され、結果として範囲外アクセスが発生しました。
技術的詳細
encoding/ascii85
パッケージの Encode
関数は、入力バイトスライス src
を読み込み、ASCII85エンコードされたバイトスライス dst
に書き込みます。この関数はループ内で4バイトずつ src
を処理し、対応するASCII85文字を dst
に書き込んでいきます。
問題が発生したのは、以下のコードブロックです。
if v == 0 && len(src) >= 4 {
dst[0] = 'z'
dst = dst[1:]
// src = src[4:] <-- この行が不足していた
tn++
continue
}
ここで v == 0
は、現在の4バイトのブロックがすべてゼロであることを意味します。そして len(src) >= 4
は、少なくとも4バイトのデータが残っていることを確認しています。この条件が満たされると、dst[0] = 'z'
によって出力スライスの先頭に 'z'
が書き込まれ、dst = dst[1:]
によって dst
スライスが1バイト分進められます。
しかし、このブロックでは src
スライスが更新されていませんでした。つまり、4つのゼロバイトが 'z'
として処理されたにもかかわらず、src
スライスはまだその4つのゼロバイトを指したままでした。ループの次のイテレーションでは、src
は同じ4つのゼロバイトを再度処理しようとするか、あるいは src
の長さが不適切に評価され、後続の処理で src
の範囲外にアクセスしようとしてパニックを引き起こしました。
修正は、この if
ブロック内に src = src[4:]
を追加することです。これにより、4つのゼロバイトが 'z'
としてエンコードされた後、src
スライスが正しく4バイト分進められ、次の処理ブロックが正しい位置から開始されるようになります。
コアとなるコードの変更箇所
src/pkg/encoding/ascii85/ascii85.go
ファイルの Encode
関数内、'z'
の特殊ケースを処理する if
ブロックに1行追加されています。
--- a/src/pkg/encoding/ascii85/ascii85.go
+++ b/src/pkg/encoding/ascii85/ascii85.go
@@ -57,6 +57,7 @@ func Encode(dst, src []byte) int {
if v == 0 && len(src) >= 4 {
dst[0] = 'z'
dst = dst[1:]
+ src = src[4:] // 追加された行
tn++
continue
}
また、この修正を検証するために、src/pkg/encoding/ascii85/ascii85_test.go
に新しいテストケースが追加されています。
--- a/src/pkg/encoding/ascii85/ascii85_test.go
+++ b/src/pkg/encoding/ascii85/ascii85_test.go
@@ -28,6 +28,11 @@ var pairs = []testpair{
"l(DId<j@<?3r@:F%a+D58'ATD4$Bl@l3De:,-DJs`8ARoFb/0JMK@qB4^F!,R<AKZ&-DfTqBG%G\n" +\
">uD.RTpAKYo'+CT/5+Cei#DII?(E,9)oF*2M7/c\n",
},
+ // Special case when shortening !!!!! to z.
+ {
+ "\000\000\000\000",
+ "z",
+ },
}
var bigtest = pairs[len(pairs)-1]
コアとなるコードの解説
src/pkg/encoding/ascii85/ascii85.go
の変更
Encode
関数は、入力 src
スライスから4バイトのブロックを読み込み、それを v
という uint32
の値に変換します。
if v == 0 && len(src) >= 4 {
dst[0] = 'z'
dst = dst[1:]
src = src[4:] // この行が追加された
tn++
continue
}
if v == 0 && len(src) >= 4
: この条件は、現在の4バイトの入力ブロックがすべてゼロであり、かつエンコードに必要な4バイトがsrc
スライスに存在することを確認します。dst[0] = 'z'
: 条件が真の場合、出力スライスdst
の先頭にASCII85の短縮表記'z'
を書き込みます。dst = dst[1:]
:dst
スライスを1バイト分進めます。これは、'z'
が1バイトの出力文字であるためです。src = src[4:]
: このコミットで追加された重要な行です。src
スライスを4バイト分進めます。これにより、入力の4つのゼロバイトが正しく消費されたことをEncode
関数に伝え、次のループイテレーションが未処理のデータから開始されるようになります。この行がなかったために、src
スライスが正しく更新されず、パニックの原因となっていました。tn++
: 処理された入力バイト数(この場合は4バイト)をカウントする変数tn
をインクリメントします。continue
: 現在のループイテレーションを終了し、次のイテレーションに進みます。これにより、通常の4バイトから5文字への変換ロジックがスキップされます。
src/pkg/encoding/ascii85/ascii85_test.go
の変更
追加されたテストケースは、pairs
というテストデータのスライスに追加されています。
// Special case when shortening !!!!! to z.
{
"\000\000\000\000",
"z",
},
"\000\000\000\000"
: これは入力データで、4つのゼロバイトを表します。Go言語では、\000
はヌルバイト(値が0のバイト)のリテラルです。"z"
: これは期待される出力で、4つのゼロバイトがASCII85の短縮表記'z'
にエンコードされることを示します。
このテストケースは、修正が正しく機能し、4つのゼロバイトが期待通りに 'z'
にエンコードされ、かつパニックが発生しないことを検証します。
関連リンク
- Go言語の
encoding/ascii85
パッケージのドキュメント: https://pkg.go.dev/encoding/ascii85 (コミット当時のバージョンとは異なる可能性がありますが、現在のドキュメントも参考になります) - Go言語のコードレビューシステム (Gerrit) での変更リスト: https://golang.org/cl/5970078
参考にした情報源リンク
- ASCII85 (Wikipedia): https://en.wikipedia.org/wiki/Ascii85
- Go言語のスライス (Go言語公式ブログ): https://go.dev/blog/slices
- Go言語のパニックとリカバリ (Go言語公式ドキュメント): https://go.dev/blog/defer-panic-and-recover