[インデックス 19159] ファイルの概要
このコミットは、Go言語の image/png パッケージにおける、悪意を持って作成されたPNGファイルがデコード時にクラッシュする可能性があったバグを修正するものです。具体的には、PNGファイルが過剰なピクセルデータを含んでいると主張し、zlib.Reader がエラーを返さない場合に発生する問題に対処しています。
コミット
commit c47f08657a09aaabda3974b50b8a29e460f9927a
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Apr 16 12:18:57 2014 +1000
image/png: fix crash when an alleged PNG has too much pixel data,
so that the zlib.Reader returns nil error.
Fixes #7762.
LGTM=r
R=r
CC=golang-codereviews
https://golang.org/cl/86750044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c47f08657a09aaabda3974b50b8a29e460f9927a
元コミット内容
このコミットは、image/png パッケージが、不正な形式のPNGファイル、特にピクセルデータが過剰であると主張するファイルに対して、zlib.Reader が nil エラーを返し続ける場合に発生するクラッシュを修正します。これは、Go issue #7762 に対応するものです。
変更の背景
PNGファイルは、画像データを圧縮して保存するためにzlib圧縮を使用します。Goの image/png パッケージは、この圧縮されたデータを zlib.Reader を使用して読み込み、解凍します。
問題は、不正に作成されたPNGファイルにおいて、zlib.Reader がデータの終端(EOF)に達したにもかかわらず、または無効なデータに遭遇したにもかかわらず、Read メソッドが (0, nil)(読み込んだバイト数0、エラーなし)を返し続ける可能性があった点にあります。これは、zlib.Reader の内部状態が不正なデータによって予期せぬ状態になり、無限ループに陥る原因となっていました。
元のコードでは、r.Read(pr[:1]) の呼び出しが io.EOF 以外のエラーを返さない限り、ループを抜けることができませんでした。しかし、特定の条件下では (0, nil) が無限に返されるため、デコード処理が停止せず、リソースを消費し続けることでサービス拒否(DoS)攻撃につながる可能性や、プログラムがクラッシュする可能性がありました。
このコミットは、このような悪意のある入力に対する堅牢性を高め、デコード処理が無限ループに陥ることを防ぐために導入されました。
前提知識の解説
PNG (Portable Network Graphics)
PNGは、可逆圧縮を特徴とするラスターグラフィックファイル形式です。ウェブ上で広く使用されており、透明度(アルファチャンネル)をサポートします。PNGファイルは、複数の「チャンク」で構成されており、画像データは通常、IDATチャンクに格納されます。このIDATチャンク内のデータは、zlib圧縮アルゴリズムを使用して圧縮されています。
zlib圧縮
zlibは、データ圧縮のためのソフトウェアライブラリであり、Deflateアルゴリズムを実装しています。PNGファイルは、画像データを効率的に保存するためにzlib圧縮を利用しています。Go言語の compress/zlib パッケージは、zlib形式のデータを読み書きするための zlib.Reader および zlib.Writer を提供します。
io.Reader インターフェース
Go言語の io.Reader インターフェースは、データを読み込むための基本的な抽象化を提供します。このインターフェースは、単一のメソッド Read(p []byte) (n int, err error) を定義しています。
nは、読み込まれたバイト数を示します。errは、読み込み中に発生したエラーを示します。err == nilの場合、読み込みは成功し、nバイトが読み込まれました。err == io.EOFの場合、データの終端に達しました。この場合でもn > 0である可能性があります(終端に達する前に一部のデータを読み込んだ場合)。err != nilかつerr != io.EOFの場合、読み込み中にエラーが発生しました。
io.ErrNoProgress
io.ErrNoProgress は、Goの io パッケージで定義されているエラー定数です。これは、io.Reader の Read メソッドが複数回連続して呼び出されたにもかかわらず、常に (0, nil)(読み込んだバイト数0、エラーなし)を返し、データの読み込みが進まない状態を示します。これは通常、基となる io.Reader の実装に問題があるか、またはデータストリームが予期せぬ方法で「スタック」していることを示唆します。このエラーは、無限ループを防ぎ、Reader が進行しない状況を検出するために使用されます。
技術的詳細
このコミットの核心は、image/png パッケージの decode メソッドにおける、zlib圧縮データの読み込みロジックの変更です。
元のコードでは、zlibストリームの終端(zlibチェックサムの検証のため)を確認するために、r.Read(pr[:1]) を一度だけ呼び出していました。ここで r は zlib.Reader のインスタンスです。期待される動作は、有効なデータがすべて読み込まれた後に io.EOF を返すか、または不正なデータに対してエラーを返すことでした。
しかし、特定の不正なPNGファイルでは、zlib.Reader が io.EOF や他のエラーを返さずに、n=0, err=nil を返し続ける状況が発生しました。これは、zlib.Reader が内部的に「進行していない」状態に陥っていることを意味します。
修正では、この r.Read(pr[:1]) の呼び出しをループ内に配置し、最大100回まで試行するように変更しました。
n := 0とerrはループの外部で初期化されます。for i := 0; n == 0 && err == nil; i++ループは、nが0でerrがnilの間、つまりReadがバイトを読み込まず、エラーも発生しない間、繰り返されます。- ループ内で
iが100に達した場合、これはzlib.Readerが100回連続して進行していないことを意味します。この場合、io.ErrNoProgressを返してデコード処理を中断します。これにより、無限ループを防ぎ、リソースの枯渇を防ぎます。 - ループを抜けた後、
err != nil && err != io.EOFの条件でエラーチェックを行います。これは、io.EOF以外のエラーが発生した場合にFormatErrorを返すためのものです。
この変更により、zlib.Reader が不正な入力に対して (0, nil) を返し続けるような状況でも、デコード処理がタイムアウトし、適切なエラー(io.ErrNoProgress)を返すことで、プログラムの安定性と堅牢性が向上します。
コアとなるコードの変更箇所
変更は src/pkg/image/png/reader.go ファイルの decode メソッド内で行われました。
--- a/src/pkg/image/png/reader.go
+++ b/src/pkg/image/png/reader.go
@@ -505,8 +505,14 @@ func (d *decoder) decode() (image.Image, error) {
}
// Check for EOF, to verify the zlib checksum.
- n, err := r.Read(pr[:1])
- if err != io.EOF {
+ n := 0
+ for i := 0; n == 0 && err == nil; i++ {
+ if i == 100 {
+ return nil, io.ErrNoProgress
+ }
+ n, err = r.Read(pr[:1])
+ }
+ if err != nil && err != io.EOF {
return nil, FormatError(err.Error())\n \t}\n \tif n != 0 || d.idatLength != 0 {
コアとなるコードの解説
変更されたコードブロックは、PNGのIDATチャンクからすべてのピクセルデータを読み込んだ後、zlibストリームの終端とチェックサムを検証するために、zlib.Reader (r) からさらに1バイトを読み込もうとする部分です。
n := 0: 読み込まれたバイト数を格納する変数nを0で初期化します。これは、ループの条件で使用されます。for i := 0; n == 0 && err == nil; i++:i := 0: ループの試行回数をカウントするカウンタiを初期化します。n == 0 && err == nil: このループは、r.Readがバイトを読み込まず(n=0)、かつエラーも発生しない(err=nil)場合に継続します。これは、zlib.Readerが進行していない状態を示します。i++: 各ループの反復でカウンタをインクリメントします。
if i == 100:- ループが100回繰り返され、その間ずっと
zlib.Readerが進行しなかった場合、この条件が真になります。 return nil, io.ErrNoProgress: この時点で、zlib.Readerがスタックしていると判断し、io.ErrNoProgressエラーを返してデコード処理を中断します。これにより、無限ループを防ぎ、不正な入力に対する堅牢性を確保します。
- ループが100回繰り返され、その間ずっと
n, err = r.Read(pr[:1]):zlib.Reader(r) から1バイト (pr[:1]) を読み込もうとします。- 読み込み結果は
n(読み込まれたバイト数)とerr(発生したエラー)に格納されます。
if err != nil && err != io.EOF:- ループを抜けた後、最終的なエラーチェックを行います。
errがnilではなく、かつio.EOFでもない場合(つまり、予期せぬエラーが発生した場合)、FormatErrorを返します。io.EOFは正常な終端を示すため、ここではエラーとして扱われません。
この変更により、zlib.Reader が不正な入力に対して無限に (0, nil) を返し続ける状況を検出し、早期にエラーを返すことで、プログラムの安定性を向上させています。
関連リンク
- Go issue #7762: https://github.com/golang/go/issues/7762
- Go CL 86750044: https://golang.org/cl/86750044
参考にした情報源リンク
- Go言語の
ioパッケージドキュメント: https://pkg.go.dev/io - Go言語の
compress/zlibパッケージドキュメント: https://pkg.go.dev/compress/zlib - PNG (Portable Network Graphics) 公式ウェブサイト: http://www.libpng.org/pub/png/
- zlib 公式ウェブサイト: https://www.zlib.net/
io.ErrNoProgressに関するGoのドキュメントや議論 (Web検索結果より)