[インデックス 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検索結果より)