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

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

このコミットは、Go言語の標準ライブラリ image/gif パッケージにおける重要なセキュリティ修正と堅牢性向上に関するものです。具体的には、GIF画像のデコード時に、ピクセルデータが現在のパレットの範囲外のインデックスを参照している場合にエラーを発生させるよう変更が加えられました。これにより、不正なGIFファイルがパレット外のメモリ領域にアクセスしようとする潜在的な脆弱性や、予期せぬ動作を引き起こす可能性が排除されます。

コミット

commit 8192017e14a0902293717ef3c847672a9b9a0da4
Author: Jeff R. Allen <jra@nella.org>
Date:   Mon Jul 1 14:11:45 2013 +1000

    image/gif: do not allow pixels outside the current palette
    
    After loading a frame of a GIF, check that each pixel
    is inside the frame's palette.
    
    Fixes #5401.
    
    R=nigeltao, r
    CC=golang-dev
    https://golang.org/cl/10597043

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

https://github.com/golang/go/commit/8192017e14a0902293717ef3c847672a9b9a0da4

元コミット内容

image/gif: do not allow pixels outside the current palette

After loading a frame of a GIF, check that each pixel
is inside the frame's palette.

Fixes #5401.

R=nigeltao, r
CC=golang-dev
https://golang.org/cl/10597043

変更の背景

この変更は、Go言語の image/gif パッケージが、不正に作成されたGIFファイルを処理する際に発生する可能性のある問題を修正するために導入されました。GIF画像は、ピクセルデータを直接色情報として持つのではなく、パレット(カラーマップ)と呼ばれる色のリストへのインデックスとして持ちます。デコーダは、このインデックスを使ってパレットから実際の色を取得し、画像をレンダリングします。

しかし、悪意のある、あるいは破損したGIFファイルは、パレットのサイズを超えるインデックスをピクセルデータとして含んでいる可能性があります。このような不正なインデックスが使用されると、デコーダはパレット配列の範囲外のメモリにアクセスしようとします。これは、プログラムのクラッシュ(パニック)、未定義の動作、あるいは最悪の場合、攻撃者による任意のコード実行や情報漏洩につながる可能性のあるセキュリティ脆弱性(Out-of-bounds read)を引き起こす可能性があります。

このコミットは、GoのIssue #5401("image/gif: panic on malformed GIF")で報告された問題に対応するものです。この問題は、特定の不正なGIFファイルが image/gif パッケージによって処理されるとパニックを引き起こすことを示していました。この修正は、このようなパニックを防ぎ、デコーダの堅牢性とセキュリティを向上させることを目的としています。

前提知識の解説

GIF (Graphics Interchange Format)

GIFは、1987年にCompuServeによって導入されたビットマップ画像フォーマットです。主にウェブ上でアニメーションや透過画像を表現するために広く使用されています。GIFの主要な特徴は以下の通りです。

  • インデックスカラー: GIFは最大256色(2^8色)のインデックスカラーをサポートします。画像内の各ピクセルは、直接色情報を持つのではなく、グローバルカラーテーブル(GCT)またはローカルカラーテーブル(LCT)と呼ばれるパレット内の色のインデックスとして格納されます。
  • パレット (Color Table): パレットは、画像で使用される色のRGB値を定義するリストです。各エントリには0から255までのインデックスが割り当てられます。
  • LZW圧縮: GIFはLZW (Lempel-Ziv-Welch) 圧縮アルゴリズムを使用して画像データを圧縮します。これは可逆圧縮であり、画質を損なわずにファイルサイズを削減します。
  • アニメーション: 複数の画像を連続して表示することでアニメーションを表現できます。各フレームは独立した画像として扱われ、それぞれ独自のパレットを持つことも可能です。
  • 透過性: 1つの色を透過色として指定できます。

GIFファイルの構造(関連部分)

GIFファイルは、ヘッダ、論理画面記述子、グローバルカラーテーブル(オプション)、アプリケーション拡張、コメント拡張、グラフィック制御拡張、画像記述子、ローカルカラーテーブル(オプション)、画像データ、トレーラなどのブロックで構成されます。

  • 画像記述子 (Image Descriptor): 各画像(フレーム)のサイズ、位置、およびローカルカラーテーブルの有無などの情報を含みます。
  • ローカルカラーテーブル (Local Color Table - LCT): 特定の画像フレームにのみ適用されるパレットです。これが存在しない場合、グローバルカラーテーブルが使用されます。
  • 画像データ (Image Data): LZW圧縮されたピクセルインデックスのシーケンスです。デコーダはこれを解凍し、得られたインデックスを対応するパレットに適用して実際のピクセル色を決定します。

Go言語の image/gif パッケージ

Go言語の標準ライブラリ image/gif パッケージは、GIF画像をエンコードおよびデコードするための機能を提供します。このパッケージは、image パッケージのインターフェース(image.Imageimage.Paletted など)を実装しており、Goプログラム内でGIF画像を簡単に操作できるように設計されています。

デコード処理では、image/gif パッケージはGIFファイルのバイトストリームを読み込み、その構造を解析し、ピクセルデータを抽出します。抽出されたピクセルインデックスは、適切なパレット(グローバルまたはローカル)を使用して image.Paletted 型の画像に変換されます。

技術的詳細

このコミットの技術的詳細は、GIFデコードプロセスにおけるピクセルインデックスの検証に焦点を当てています。

GIFの画像データは、LZW圧縮されたバイト列として格納されています。このバイト列を解凍すると、0から255までの範囲のピクセルインデックスのシーケンスが得られます。これらのインデックスは、そのフレームで使用されるパレット(image.Paletted 構造体の Palette フィールド)内の色を指します。

デコーダが画像データを処理する際、各ピクセルインデックスがパレットの有効な範囲内にあることを確認する必要があります。パレットのサイズは、GIFのヘッダや画像記述子で指定され、最大256エントリです。例えば、パレットが256色未満(例:16色)しか定義されていない場合、ピクセルインデックスが15を超える値(例:16や200)を持つことは不正です。

このコミット以前は、image/gif パッケージはLZW解凍後に得られたピクセルインデックスがパレットの有効な範囲内にあるかどうかを厳密にチェックしていませんでした。そのため、不正なインデックスが m.Pix に格納された場合、その後の処理で m.Palette[pixel] のようにアクセスしようとすると、Goランタイムがパニック(runtime error: index out of range)を引き起こす可能性がありました。

この修正では、decode 関数内で画像データがデコードされ、m.Pix に格納された後、以下の検証ロジックが追加されました。

  1. パレットサイズのチェック: if len(m.Palette) < 256 という条件が追加されています。これは、パレットが最大サイズ(256色)でない場合にのみ、ピクセル値の検証を行うことを意味します。なぜなら、パレットが256色である場合、ピクセルインデックスは常に0-255の範囲内であり、これはバイト型 (byte) の取りうる値の範囲と一致するため、パレットの範囲外になることはありません。しかし、パレットが256色未満の場合、ピクセルインデックスがパレットの有効なエントリ数を超えてしまう可能性があります。
  2. ピクセル値の検証: for _, pixel := range m.Pix ループで、デコードされた各ピクセルインデックス (pixel) が反復処理されます。
  3. 範囲外チェック: if int(pixel) >= len(m.Palette) という条件で、現在のピクセルインデックスがパレットの長さ以上であるかどうかがチェックされます。pixelbyte 型ですが、len(m.Palette) と比較するために int にキャストされます。
  4. エラーの返却: もしピクセルインデックスがパレットの範囲外であれば、新しく定義されたエラー errBadPixel ("gif: invalid pixel value") が返され、デコード処理が中断されます。

この変更により、image/gif パッケージは、不正なピクセルインデックスを持つGIFファイルに対しても堅牢になり、パニックを回避して明確なエラーを返すようになりました。これは、セキュリティと信頼性の両面で重要な改善です。

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

このコミットで変更された主要なファイルは以下の2つです。

  1. src/pkg/image/gif/reader.go: GIFデコードロジックが実装されているファイル。
  2. src/pkg/image/gif/reader_test.go: reader.go のテストファイル。

src/pkg/image/gif/reader.go の変更

--- a/src/pkg/image/gif/reader.go
+++ b/src/pkg/image/gif/reader.go
@@ -20,6 +20,7 @@ import (
 var (
 	errNotEnough = errors.New("gif: not enough image data")
 	errTooMuch   = errors.New("gif: too much image data")
+	errBadPixel  = errors.New("gif: invalid pixel value")
 )
 
 // If the io.Reader does not also have ReadByte, then decode will introduce its own buffering.
@@ -210,6 +211,15 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error {
 				return errTooMuch
 			}
 
+			// Check that the color indexes are inside the palette.
+			if len(m.Palette) < 256 {
+				for _, pixel := range m.Pix {
+					if int(pixel) >= len(m.Palette) {
+						return errBadPixel
+					}
+				}
+			}
+
 			// Undo the interlacing if necessary.
 			if d.imageFields&ifInterlace != 0 {
 				uninterlace(m)

src/pkg/image/gif/reader_test.go の変更

--- a/src/pkg/image/gif/reader_test.go
+++ b/src/pkg/image/gif/reader_test.go
@@ -9,16 +9,16 @@ import (
 	"testing"
 )
 
-func TestDecode(t *testing.T) {
-	// header and trailer are parts of a valid 2x1 GIF image.
-	const (
-		header = "GIF89a" +
-			"\x02\x00\x01\x00" + // width=2, height=1
-			"\x80\x00\x00" + // headerFields=(a color map of 2 pixels), backgroundIndex, aspect
-			"\x10\x20\x30\x40\x50\x60" // the color map, also known as a palette
-		trailer = "\x3b"
-	)
+// header, palette and trailer are parts of a valid 2x1 GIF image.
+const (
+	header = "GIF89a" +
+		"\x02\x00\x01\x00" + // width=2, height=1
+		"\x80\x00\x00" // headerFields=(a color map of 2 pixels), backgroundIndex, aspect
+	palette = "\x10\x20\x30\x40\x50\x60" // the color map, also known as a palette
+	trailer = "\x3b"
+)
 
+func TestDecode(t *testing.T) {
 	// lzwEncode returns an LZW encoding (with 2-bit literals) of n zeroes.
 	lzwEncode := func(n int) []byte {
 		b := &bytes.Buffer{}
@@ -42,6 +42,7 @@ func TestDecode(t *testing.T) {
 	for _, tc := range testCases {
 		b := &bytes.Buffer{}
 		b.WriteString(header)
+		b.WriteString(palette)
 		// Write an image with bounds 2x1 but tc.nPix pixels. If tc.nPix != 2
 		// then this should result in an invalid GIF image. First, write a
 		// magic 0x2c (image descriptor) byte, bounds=(0,0)-(2,1), a flags
@@ -114,7 +115,7 @@ func try(t *testing.T, b []byte, want string) {
 }
 
 func TestBounds(t *testing.T) {
-	// make a local copy of testGIF
+	// Make a local copy of testGIF.
 	gif := make([]byte, len(testGIF))
 	copy(gif, testGIF)
 	// Make the bounds too big, just by one.
@@ -136,3 +137,61 @@ func TestBounds(t *testing.T) {
 	}\n \ttry(t, gif, want)\n }\n+\n+func TestNoPalette(t *testing.T) {\n+\tb := &bytes.Buffer{}\n+\n+\t// Manufacture a GIF with no palette, so any pixel at all\n+\t// will be invalid.\n+\tb.WriteString(header[:len(header)-3])\n+\tb.WriteString("\\x00\\x00\\x00") // No global palette.\n+\n+\t// Image descriptor: 2x1, no local palette.\n+\tb.WriteString("\\x2c\\x00\\x00\\x00\\x00\\x02\\x00\\x01\\x00\\x00\\x02")\n+\n+\t// Encode the pixels: neither is in range, because there is no palette.\n+\tpix := []byte{0, 128}\n+\tenc := &bytes.Buffer{}\n+\tw := lzw.NewWriter(enc, lzw.LSB, 2)\n+\tw.Write(pix)\n+\tw.Close()\n+\tb.WriteByte(byte(len(enc.Bytes())))\n+\tb.Write(enc.Bytes())\n+\tb.WriteByte(0x00) // An empty block signifies the end of the image data.\n+\n+\tb.WriteString(trailer)\n+\n+\ttry(t, b.Bytes(), "gif: invalid pixel value")\n+}\n+\n+func TestPixelOutsidePaletteRange(t *testing.T) {\n+\tfor _, pval := range []byte{0, 1, 2, 3, 255} {\n+\t\tb := &bytes.Buffer{}\n+\n+\t\t// Manufacture a GIF with a 2 color palette.\n+\t\tb.WriteString(header)\n+\t\tb.WriteString(palette)\n+\n+\t\t// Image descriptor: 2x1, no local palette.\n+\t\tb.WriteString("\\x2c\\x00\\x00\\x00\\x00\\x02\\x00\\x01\\x00\\x00\\x02")\n+\n+\t\t// Encode the pixels; some pvals trigger the expected error.\n+\t\tpix := []byte{pval, pval}\n+\t\tenc := &bytes.Buffer{}\n+\t\tw := lzw.NewWriter(enc, lzw.LSB, 2)\n+\t\tw.Write(pix)\n+\tw.Close()\n+\t\tb.WriteByte(byte(len(enc.Bytes())))\n+\tb.Write(enc.Bytes())\n+\t\tb.WriteByte(0x00) // An empty block signifies the end of the image data.\n+\n+\t\tb.WriteString(trailer)\n+\n+\t\t// No error expected, unless the pixels are beyond the 2 color palette.\n+\t\twant := ""\n+\t\tif pval >= 2 {\n+\t\t\twant = "gif: invalid pixel value"\n+\t\t}\n+\t\ttry(t, b.Bytes(), want)\n+\t}\n+}\n```

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

### `src/pkg/image/gif/reader.go`

1.  **`errBadPixel` の追加**:
    `var` ブロックに `errBadPixel = errors.New("gif: invalid pixel value")` が追加されました。これは、ピクセル値がパレットの範囲外である場合に返される新しいエラーです。これにより、デコードエラーの原因がより明確になります。

2.  **ピクセル値の検証ロジックの追加**:
    `func (d *decoder) decode(r io.Reader, configOnly bool) error` 関数内の、画像データがデコードされ `m.Pix` に格納された直後に、新しいコードブロックが挿入されました。
    *   `if len(m.Palette) < 256 { ... }`: この条件は、パレットが最大サイズ(256色)でない場合にのみ、以下のピクセル検証ループを実行することを示しています。パレットが256色の場合、`byte` 型のピクセル値は常に有効なインデックスとなるため、このチェックは不要です。
    *   `for _, pixel := range m.Pix { ... }`: デコードされたすべてのピクセルインデックス (`m.Pix` スライス内の各 `byte` 値) を反復処理します。
    *   `if int(pixel) >= len(m.Palette) { ... }`: 各 `pixel` の値が、現在のパレットの長さ (`len(m.Palette)`) 以上であるかをチェックします。`pixel` は `byte` 型なので、比較のために `int` にキャストされています。
    *   `return errBadPixel`: もしピクセル値がパレットの範囲外であれば、即座に `errBadPixel` を返してデコード処理を終了します。これにより、不正なインデックスによるパニックを防ぎます。

### `src/pkg/image/gif/reader_test.go`

テストファイルには、新しいエラーケースを検証するためのテストが追加されました。

1.  **`TestDecode` の変更**:
    既存の `TestDecode` 関数で、`palette` 定数が `header` 定数から分離され、明示的に `b.WriteString(palette)` で書き込まれるようになりました。これにより、テストコードの可読性が向上し、パレットの有無を制御しやすくなっています。

2.  **`TestNoPalette` の追加**:
    この新しいテストケースは、グローバルパレットが全く存在しないGIFファイルを意図的に作成し、デコードを試みます。
    *   `b.WriteString(header[:len(header)-3])` と `b.WriteString("\\x00\\x00\\x00")` を使用して、ヘッダからパレット情報を取り除き、グローバルパレットがないことを示します。
    *   `pix := []byte{0, 128}` のように、任意のピクセル値を設定します。パレットがないため、これらのピクセル値はすべて不正と見なされるべきです。
    *   `try(t, b.Bytes(), "gif: invalid pixel value")` を呼び出し、デコードが `errBadPixel` を返すことを検証します。

3.  **`TestPixelOutsidePaletteRange` の追加**:
    このテストケースは、パレットは存在するものの、ピクセル値がそのパレットの範囲外であるGIFファイルを検証します。
    *   `for _, pval := range []byte{0, 1, 2, 3, 255}` ループで、様々なピクセル値を試します。
    *   `b.WriteString(header)` と `b.WriteString(palette)` で、2色(`\x10\x20\x30\x40\x50\x60` は3バイトずつ2色を定義)のパレットを持つGIFを作成します。このパレットの有効なインデックスは0と1です。
    *   `pix := []byte{pval, pval}` で、テスト対象のピクセル値を設定します。
    *   `if pval >= 2 { want = "gif: invalid pixel value" }` というロジックで、ピクセル値が2以上の場合(つまり、2色パレットの範囲外の場合)に `errBadPixel` が期待されることを指定します。
    *   `try(t, b.Bytes(), want)` を呼び出し、期待されるエラーが返されることを検証します。

これらのテストの追加により、新しいピクセル検証ロジックが正しく機能し、不正なGIFファイルに対して期待通りのエラーを返すことが保証されます。

## 関連リンク

*   Go Issue #5401: [https://github.com/golang/go/issues/5401](https://github.com/golang/go/issues/5401)
*   Go CL 10597043: [https://golang.org/cl/10597043](https://golang.org/cl/10597043)

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

*   GIF (Graphics Interchange Format) - Wikipedia: [https://ja.wikipedia.org/wiki/Graphics_Interchange_Format](https://ja.wikipedia.org/wiki/Graphics_Interchange_Format)
*   LZW (Lempel-Ziv-Welch) - Wikipedia: [https://ja.wikipedia.org/wiki/LZW](https://ja.wikipedia.org/wiki/LZW)
*   Go言語 image/gif パッケージドキュメント: [https://pkg.go.dev/image/gif](https://pkg.go.dev/image/gif)
*   Go言語 image パッケージドキュメント: [https://pkg.go.dev/image](https://pkg.go.dev/image)
*   Out-of-bounds read - CWE: [https://cwe.mitre.org/data/definitions/125.html](https://cwe.mitre.org/data/definitions/125.html)
*   Go言語の標準ライブラリのソースコード (image/gif): [https://github.com/golang/go/tree/master/src/image/gif](https://github.com/golang/go/tree/master/src/image/gif)