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

[インデックス 15894] ファイルの概要

このコミットは、Go言語の image/gif パッケージにおいて、GIF画像のデコード処理における堅牢性を向上させるための変更です。具体的には、GIFファイルのフレーム境界が論理スクリーン(画像全体のキャンバス)の境界を超えていないか、また、画像データ量が画像サイズに対して過剰でないかを検証するロジックが追加されました。これにより、不正な形式のGIFファイルや悪意を持って作成されたGIFファイルによる潜在的な問題を防止し、デコーダの安定性とセキュリティを強化しています。

コミット

commit 87700cf75d660597e70d2eb7e3760f12232562ff
Author: Jeff R. Allen <jra@nella.org>
Date:   Fri Mar 22 09:30:31 2013 -0700

    image/gif: reject a GIF image if frame bounds larger than image bounds
    
    The GIF89a spec says: "Each image must fit within the
    boundaries of the Logical Screen, as defined in the
    Logical Screen Descriptor." Also, do not accept
    GIFs which have too much data for the image size.
    
    R=nigeltao, jra, r
    CC=bradfitz, golang-dev
    https://golang.org/cl/7602045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/87700cf75d660597e70d2eb7e3760f12232562ff

元コミット内容

このコミットの元々の意図は、GIF画像のデコード時に、フレームの境界が画像全体の論理スクリーン(キャンバス)の境界を逸脱している場合に、そのGIF画像を拒否することです。また、画像サイズに対してデータが過剰に含まれているGIFも受け入れないようにする変更も含まれています。これは、GIF89a仕様に厳密に従い、不正なGIFファイルによる潜在的な脆弱性やデコードエラーを防ぐためのものです。

変更の背景

GIF (Graphics Interchange Format) は、ウェブ上で広く利用されている画像フォーマットですが、その仕様は複雑であり、過去には不正な形式のファイルがデコーダのクラッシュやメモリ破壊を引き起こす脆弱性の原因となることがありました。

この変更の背景には、主に以下の点が挙げられます。

  1. GIF89a仕様への準拠: GIF89a仕様のセクション20(Image Descriptor)には、「各画像は、論理スクリーン記述子で定義された論理スクリーンの境界内に収まらなければならない」と明記されています。このコミットは、この仕様に厳密に準拠し、仕様違反のGIFファイルを適切に処理することを目的としています。仕様に準拠しないファイルを拒否することで、デコーダの予測不能な動作を防ぎます。
  2. セキュリティと堅牢性の向上: フレームの境界が論理スクリーンを逸脱しているような不正なGIFファイルは、デコーダが予期しないメモリ領域にアクセスしようとしたり、バッファオーバーフローを引き起こしたりする可能性があります。これは、サービス拒否(DoS)攻撃や、場合によっては任意のコード実行につながるセキュリティ脆弱性となる可能性があります。この変更により、このような潜在的な攻撃ベクトルを排除し、デコーダの堅牢性を高めます。
  3. データ整合性の確保: 画像サイズに対して過剰なデータが含まれているGIFファイルも、デコード処理において問題を引き起こす可能性があります。これは、ファイルが破損しているか、意図的に不正なデータが挿入されている可能性を示唆します。過剰なデータを拒否することで、デコーダが不必要な処理を行ったり、不正な状態に陥ることを防ぎます。

これらの背景から、Go言語の image/gif パッケージは、より安全で堅牢なGIFデコーダを提供するために、この検証ロジックを導入しました。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

1. GIF (Graphics Interchange Format)

GIFは、1987年にCompuServeによって導入されたビットマップ画像フォーマットです。主にウェブ上でアニメーションや透過画像を表現するために使用されます。GIFファイルは、複数の画像を連続して表示することでアニメーションを実現できます。

2. GIFファイル構造の主要要素

  • ヘッダ (Header): GIFのバージョン(例: GIF87a, GIF89a)を識別します。
  • 論理スクリーン記述子 (Logical Screen Descriptor): GIF画像のキャンバス全体のサイズ(幅と高さ)、カラー解像度、背景色インデックス、グローバルカラーテーブルの有無とそのサイズなどの情報を含みます。これは、GIFファイルが描画される仮想的なディスプレイ領域を定義します。
  • グローバルカラーテーブル (Global Color Table): 論理スクリーン全体で使用される色のパレットを定義します。
  • 画像記述子 (Image Descriptor): 各画像ブロックの開始位置、幅、高さ、ローカルカラーテーブルの有無とそのサイズなどの情報を含みます。GIFアニメーションの場合、各フレームがこの画像記述子を持ちます。
  • 画像データ (Image Data): LZW圧縮されたピクセルデータが含まれます。
  • グラフィックコントロール拡張 (Graphic Control Extension): アニメーションの遅延時間、透過色インデックス、ユーザー入力の有無、破棄方法などの情報を含みます。
  • アプリケーション拡張 (Application Extension): アプリケーション固有のデータ(例: Netscape 2.0のループ回数)を含みます。
  • コメント拡張 (Comment Extension): テキストコメントを含みます。
  • トレーラー (Trailer): ファイルの終わりを示す単一のバイト (0x3B) です。

3. image.Rectimage.Paletted (Go言語の image パッケージ)

Go言語の標準ライブラリ image パッケージは、画像処理のための基本的な型と関数を提供します。

  • image.Rect(x0, y0, x1, y1): 長方形の領域を定義する image.Rectangle 型の値を生成します。x0, y0 は左上の点の座標、x1, y1 は右下の点の座標(ただし、x1 は含まれず x0 から x1-1 まで、y1 は含まれず y0 から y1-1 まで)。
  • image.Paletted: パレット化された画像を表現する型です。各ピクセルはカラーパレットへのインデックスとして格納されます。GIF画像は通常パレット化された画像であるため、この型が使用されます。
  • bounds.Intersect(otherRect): image.Rectangle 型のメソッドで、2つの長方形の共通部分(交差する領域)を返します。もし共通部分がない場合、または一方の長方形がもう一方を完全に含んでいる場合でも、適切な長方形が返されます。このメソッドは、ある長方形が別の長方形の内部に完全に収まっているかを効率的にチェックするために使用できます。もし bounds.Intersect(image.Rect(0, 0, d.width, d.height)) の結果が元の bounds と等しい場合、それは boundsimage.Rect(0, 0, d.width, d.height) の内部に完全に収まっていることを意味します。

4. LZW圧縮 (Lempel-Ziv-Welch)

GIF画像データはLZWアルゴリズムで圧縮されています。これは可逆圧縮アルゴリズムの一種で、繰り返し現れるパターンを短いコードに置き換えることでデータサイズを削減します。

技術的詳細

このコミットの技術的詳細は、主に src/pkg/image/gif/reader.gonewImageFromDescriptor 関数における変更と、それに対応するテストケースの追加にあります。

1. newImageFromDescriptor 関数の変更

この関数は、GIFファイルの画像記述子(Image Descriptor)から新しい image.Paletted オブジェクトを作成する役割を担っています。変更前は、画像記述子から読み取った left, top, width, height を基に単純に image.NewPaletted(image.Rect(left, top, left+width, top+height), nil) を呼び出していました。

変更後、以下の検証ロジックが追加されました。

	// The GIF89a spec, Section 20 (Image Descriptor) says:
	// "Each image must fit within the boundaries of the Logical
	// Screen, as defined in the Logical Screen Descriptor."
	bounds := image.Rect(left, top, left+width, top+height)
	if bounds != bounds.Intersect(image.Rect(0, 0, d.width, d.height)) {
		return nil, errors.New("gif: frame bounds larger than image bounds")
	}
	return image.NewPaletted(bounds, nil), nil
  • bounds := image.Rect(left, top, left+width, top+height): まず、現在のフレームの境界を image.Rectangle オブジェクトとして作成します。lefttop はフレームの左上隅の座標、widthheight はフレームの幅と高さです。
  • image.Rect(0, 0, d.width, d.height): これは、GIFファイルの論理スクリーン全体の境界を表します。d.widthd.height は、デコーダのコンテキストで保持されている論理スクリーンの幅と高さです。
  • bounds.Intersect(image.Rect(0, 0, d.width, d.height)): この部分が変更の核心です。現在のフレームの境界 (bounds) と論理スクリーン全体の境界の共通部分を計算します。
  • if bounds != bounds.Intersect(...): この条件式は、現在のフレームの境界が論理スクリーンの境界内に完全に収まっているかをチェックします。
    • もし bounds が論理スクリーンの境界内に完全に収まっている場合、bounds とその共通部分 (bounds.Intersect(...)) は等しくなります。
    • もし bounds が論理スクリーンの境界を少しでもはみ出している場合、共通部分は元の bounds よりも小さくなるため、両者は等しくなりません。
  • return nil, errors.New("gif: frame bounds larger than image bounds"): フレームの境界が論理スクリーンをはみ出している場合、エラーを返してデコードを中断します。

このチェックにより、GIF89a仕様に違反する、フレームが論理スクリーンからはみ出すような不正なGIFファイルを早期に検出して拒否できるようになります。

2. reader_test.go のテストケース追加

src/pkg/image/gif/reader_test.goTestBounds 関数が追加され、上記の検証ロジックが正しく機能するかをテストしています。

  • testGIF というシンプルな1x1ピクセルのGIFバイト配列が定義されています。
  • try ヘルパー関数は、与えられたGIFバイト配列をデコードし、期待されるエラーメッセージと比較します。
  • TestBounds 内では、testGIF の画像記述子内の幅/高さを意図的に変更し、以下のシナリオをテストしています。
    • 境界が大きすぎる場合: testGIF[32] = 2 と設定することで、フレームの幅を論理スクリーン(1ピクセル)よりも大きくし、"gif: frame bounds larger than image bounds" エラーが返されることを確認します。
    • 境界が小さすぎる場合: testGIF[32] = 0 と設定することで、フレームの幅を0にし、この場合は境界チェックには引っかからないが、画像データが過剰になるため、"gif: too much image data" エラーが返されることを確認します。これは、このコミットのもう一つの目的である「画像サイズに対してデータが過剰なGIFを拒否する」という側面をテストしています。
    • 境界が非常に大きい場合: testGIF[32+i] = 0xff と設定することで、フレームの幅と高さを最大値に設定し、同様に "gif: frame bounds larger than image bounds" エラーが返されることを確認します。

これらのテストケースは、追加された境界チェックが期待通りに機能し、不正なGIFファイルを適切に拒否できることを保証します。

コアとなるコードの変更箇所

--- a/src/pkg/image/gif/reader.go
+++ b/src/pkg/image/gif/reader.go
@@ -348,7 +348,15 @@ func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) {
 	width := int(d.tmp[4]) + int(d.tmp[5])<<8
 	height := int(d.tmp[6]) + int(d.tmp[7])<<8
 	d.imageFields = d.tmp[8]
-	return image.NewPaletted(image.Rect(left, top, left+width, top+height), nil), nil
+
+	// The GIF89a spec, Section 20 (Image Descriptor) says:
+	// "Each image must fit within the boundaries of the Logical
+	// Screen, as defined in the Logical Screen Descriptor."
+	bounds := image.Rect(left, top, left+width, top+height)
+	if bounds != bounds.Intersect(image.Rect(0, 0, d.width, d.height)) {
+		return nil, errors.New("gif: frame bounds larger than image bounds")
+	}
+	return image.NewPaletted(bounds, nil), nil
 }
 
 func (d *decoder) readBlock() (int, error) {
--- a/src/pkg/image/gif/reader_test.go
+++ b/src/pkg/image/gif/reader_test.go
@@ -84,3 +84,52 @@ func TestDecode(t *testing.T) {
 		}
 	}\n}\n+\n+// testGIF is a simple GIF that we can modify to test different scenarios.\n+var testGIF = []byte{\n+\t'G', 'I', 'F', '8', '9', 'a',\n+\t1, 0, 1, 0, // w=1, h=1 (6)\n+\t128, 0, 0, // headerFields, bg, aspect (10)\n+\t0, 0, 0, 1, 1, 1, // color map and graphics control (13)\n+\t0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, // (19)\n+\t// frame 1 (0,0 - 1,1)\n+\t0x2c,\n+\t0x00, 0x00, 0x00, 0x00,\n+\t0x01, 0x00, 0x01, 0x00, // (32)\n+\t0x00,\n+\t0x02, 0x02, 0x4c, 0x01, 0x00, // lzw pixels\n+\t// trailer\n+\t0x3b,\n+}\n+\n+func try(t *testing.T, b []byte, want string) {\n+\t_, err := DecodeAll(bytes.NewReader(b))\n+\tvar got string\n+\tif err != nil {\n+\t\tgot = err.Error()\n+\t}\n+\tif got != want {\n+\t\tt.Fatalf("got %v, want %v", got, want)\n+\t}\n+}\n+\n+func TestBounds(t *testing.T) {\n+\t// Make the bounds too big, just by one.\n+\ttestGIF[32] = 2\n+\twant := "gif: frame bounds larger than image bounds"\n+\ttry(t, testGIF, want)\n+\n+\t// Make the bounds too small; does not trigger bounds\n+\t// check, but now there's too much data.\n+\ttestGIF[32] = 0\n+\twant = "gif: too much image data"\n+\ttry(t, testGIF, want)\n+\ttestGIF[32] = 1\n+\n+\t// Make the bounds really big, expect an error.\n+\twant = "gif: frame bounds larger than image bounds"\n+\tfor i := 0; i < 4; i++ {\n+\t\ttestGIF[32+i] = 0xff\n+\t}\n+\ttry(t, testGIF, want)\n+}\n```

## コアとなるコードの解説

### `src/pkg/image/gif/reader.go` の変更点

`newImageFromDescriptor` 関数は、GIFの画像記述子から読み取った情報に基づいて、新しい画像フレーム(`image.Paletted` 型)を生成します。

変更前は、単に `left`, `top`, `width`, `height` を使って `image.Rect` を作成し、それに基づいて `image.NewPaletted` を呼び出していました。これでは、画像フレームが論理スクリーンの範囲外に定義されていても、エラーなく画像オブジェクトが作成されてしまいます。

変更後、以下の重要な検証が追加されました。

1.  **フレーム境界の取得**:
    ```go
    bounds := image.Rect(left, top, left+width, top+height)
    ```
    これは、現在の画像フレームが占めるべき領域を `image.Rectangle` 型で表現しています。`left` と `top` はフレームの左上隅の座標、`width` と `height` はフレームの寸法です。

2.  **論理スクリーン境界との比較**:
    ```go
    if bounds != bounds.Intersect(image.Rect(0, 0, d.width, d.height)) {
        return nil, errors.New("gif: frame bounds larger than image bounds")
    }
    ```
    *   `image.Rect(0, 0, d.width, d.height)` は、GIFファイルの論理スクリーン全体の領域を表します。`d.width` と `d.height` は、GIFファイルのヘッダから読み取られた論理スクリーンの幅と高さです。
    *   `bounds.Intersect(...)` は、現在のフレームの境界 (`bounds`) と論理スクリーン全体の境界の共通部分を計算します。
    *   この `if` 文の条件 `bounds != bounds.Intersect(...)` は、現在のフレームの境界が論理スクリーンの境界内に完全に収まっているかをチェックします。
        *   もしフレームが論理スクリーン内に完全に収まっていれば、`bounds` とその共通部分 (`bounds.Intersect(...)`) は全く同じ長方形になります。
        *   もしフレームが論理スクリーンからはみ出している場合、共通部分は元の `bounds` よりも小さい(または異なる位置の)長方形になるため、両者は等しくなりません。
    *   この条件が真(つまり、フレームが論理スクリーンからはみ出している)の場合、`"gif: frame bounds larger than image bounds"` というエラーを返してデコード処理を中断します。これにより、仕様違反のGIFファイルが処理されるのを防ぎ、デコーダの堅牢性を高めます。

### `src/pkg/image/gif/reader_test.go` の変更点

このファイルには、追加された境界チェックのロジックが正しく機能するかを検証するための新しいテストケース `TestBounds` が追加されました。

1.  **`testGIF` 変数**:
    ```go
    var testGIF = []byte{
        'G', 'I', 'F', '8', '9', 'a',
        1, 0, 1, 0, // w=1, h=1 (6)
        // ... (以下略)
    }
    ```
    これは、テストのために特別に作成された、非常にシンプルな1x1ピクセルのGIFファイルのバイト配列です。この配列を直接操作することで、GIFファイルの特定のバイト(例えば、画像記述子の幅や高さ)を意図的に変更し、様々な不正なシナリオをシミュレートできます。

2.  **`try` ヘルパー関数**:
    ```go
    func try(t *testing.T, b []byte, want string) {
        _, err := DecodeAll(bytes.NewReader(b))
        var got string
        if err != nil {
            got = err.Error()
        }
        if got != want {
            t.Fatalf("got %v, want %v", got, want)
        }
    }
    ```
    この関数は、与えられたGIFバイト配列 `b` をデコードし、発生したエラーメッセージが期待される `want` 文字列と一致するかを検証します。これにより、テストコードの重複を減らし、可読性を向上させています。

3.  **`TestBounds` 関数**:
    *   **境界が大きすぎるケース**:
        ```go
        testGIF[32] = 2 // 画像記述子の幅を2に設定 (論理スクリーンは1x1)
        want := "gif: frame bounds larger than image bounds"
        try(t, testGIF, want)
        ```
        `testGIF` のバイト配列のインデックス32は、画像記述子の幅(2バイト目)に相当します。これを `2` に設定することで、フレームの幅が論理スクリーン(1ピクセル)よりも大きくなるように不正なGIFを作成し、`"gif: frame bounds larger than image bounds"` エラーが正しく検出されることを確認しています。

    *   **境界が小さすぎるがデータが過剰なケース**:
        ```go
        testGIF[32] = 0 // 画像記述子の幅を0に設定
        want = "gif: too much image data"
        try(t, testGIF, want)
        testGIF[32] = 1 // 元に戻す
        ```
        このケースでは、フレームの幅を `0` に設定します。この場合、境界チェックには引っかかりませんが、画像データがフレームサイズに対して過剰になるため、デコーダの別の部分で `"gif: too much image data"` エラーが発生することを確認しています。これは、このコミットのもう一つの目的である「画像サイズに対してデータが過剰なGIFを拒否する」という側面をテストしています。

    *   **境界が非常に大きいケース**:
        ```go
        want = "gif: frame bounds larger than image bounds"
        for i := 0; i < 4; i++ {
            testGIF[32+i] = 0xff // 画像記述子の幅と高さを最大値に設定
        }
        try(t, testGIF, want)
        ```
        画像記述子の幅と高さを示すバイトをすべて `0xff` (255) に設定することで、フレームのサイズが非常に大きくなるようにします。これにより、`"gif: frame bounds larger than image bounds"` エラーが確実に発生することを確認しています。

これらのテストは、`newImageFromDescriptor` 関数に追加された境界チェックが、様々な不正な入力に対して期待通りに機能し、適切なエラーを返すことを保証します。

## 関連リンク

*   Go CL 7602045: [https://golang.org/cl/7602045](https://golang.org/cl/7602045)

## 参考にした情報源リンク

*   GIF89a Specification: [https://www.w3.org/Graphics/GIF/spec-gif89a.txt](https://www.w3.org/Graphics/GIF/spec-gif89a.txt) (特に Section 20. Image Descriptor)
*   Go image package documentation: [https://pkg.go.dev/image](https://pkg.go.dev/image)
*   Go image/gif package documentation: [https://pkg.go.dev/image/gif](https://pkg.go.dev/image/gif)
*   LZW compression: [https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch)
*   GIF file format: [https://en.wikipedia.org/wiki/GIF](https://en.wikipedia.org/wiki/GIF)
*   Go errors package documentation: [https://pkg.go.dev/errors](https://pkg.go.dev/errors)