Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Readernil エラーを返し続ける場合に発生するクラッシュを修正します。これは、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.ReaderRead メソッドが複数回連続して呼び出されたにもかかわらず、常に (0, nil)(読み込んだバイト数0、エラーなし)を返し、データの読み込みが進まない状態を示します。これは通常、基となる io.Reader の実装に問題があるか、またはデータストリームが予期せぬ方法で「スタック」していることを示唆します。このエラーは、無限ループを防ぎ、Reader が進行しない状況を検出するために使用されます。

技術的詳細

このコミットの核心は、image/png パッケージの decode メソッドにおける、zlib圧縮データの読み込みロジックの変更です。

元のコードでは、zlibストリームの終端(zlibチェックサムの検証のため)を確認するために、r.Read(pr[:1]) を一度だけ呼び出していました。ここで rzlib.Reader のインスタンスです。期待される動作は、有効なデータがすべて読み込まれた後に io.EOF を返すか、または不正なデータに対してエラーを返すことでした。

しかし、特定の不正なPNGファイルでは、zlib.Readerio.EOF や他のエラーを返さずに、n=0, err=nil を返し続ける状況が発生しました。これは、zlib.Reader が内部的に「進行していない」状態に陥っていることを意味します。

修正では、この r.Read(pr[:1]) の呼び出しをループ内に配置し、最大100回まで試行するように変更しました。

  • n := 0err はループの外部で初期化されます。
  • for i := 0; n == 0 && err == nil; i++ ループは、n が0で errnil の間、つまり 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バイトを読み込もうとする部分です。

  1. n := 0: 読み込まれたバイト数を格納する変数 n を0で初期化します。これは、ループの条件で使用されます。
  2. for i := 0; n == 0 && err == nil; i++:
    • i := 0: ループの試行回数をカウントするカウンタ i を初期化します。
    • n == 0 && err == nil: このループは、r.Read がバイトを読み込まず(n=0)、かつエラーも発生しない(err=nil)場合に継続します。これは、zlib.Reader が進行していない状態を示します。
    • i++: 各ループの反復でカウンタをインクリメントします。
  3. if i == 100:
    • ループが100回繰り返され、その間ずっと zlib.Reader が進行しなかった場合、この条件が真になります。
    • return nil, io.ErrNoProgress: この時点で、zlib.Reader がスタックしていると判断し、io.ErrNoProgress エラーを返してデコード処理を中断します。これにより、無限ループを防ぎ、不正な入力に対する堅牢性を確保します。
  4. n, err = r.Read(pr[:1]):
    • zlib.Reader (r) から1バイト (pr[:1]) を読み込もうとします。
    • 読み込み結果は n(読み込まれたバイト数)と err(発生したエラー)に格納されます。
  5. if err != nil && err != io.EOF:
    • ループを抜けた後、最終的なエラーチェックを行います。
    • errnil ではなく、かつ io.EOF でもない場合(つまり、予期せぬエラーが発生した場合)、FormatError を返します。io.EOF は正常な終端を示すため、ここではエラーとして扱われません。

この変更により、zlib.Reader が不正な入力に対して無限に (0, nil) を返し続ける状況を検出し、早期にエラーを返すことで、プログラムの安定性を向上させています。

関連リンク

参考にした情報源リンク