[インデックス 15889] ファイルの概要
このコミットは、Go言語の標準ライブラリである image/gif
パッケージにおけるGIF画像のデコード処理の堅牢性を向上させるものです。具体的には、GIF画像のピクセルデータ量と画像境界(幅と高さ)が一致しない場合に、より厳密なチェックを行うように変更が加えられています。これにより、不正な形式のGIFファイルや、悪意を持って作成されたGIFファイルに対する耐性が向上し、デコーダが予期せぬ動作をしたり、クラッシュしたりするのを防ぎます。
コミット
commit f308efd869af3cd7cbf74af8ef6558cf4245048b
Author: Nigel Tao <nigeltao@golang.org>
Date: Fri Mar 22 14:42:02 2013 +1100
image/gif: tighten the checks for when the amount of an image's pixel
data does not agree with its bounds.
R=r, jeff.allen
CC=golang-dev
https://golang.org/cl/7938043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f308efd869af3cd7cbf74af8ef6558cf4245048b
元コミット内容
image/gif: tighten the checks for when the amount of an image's pixel data does not agree with its bounds.
このコミットは、GIF画像のピクセルデータ量がその画像が持つべき境界(幅と高さから計算される総ピクセル数)と一致しない場合に、チェックを厳格化することを目的としています。
変更の背景
GIF(Graphics Interchange Format)は、ウェブ上で広く利用されている画像フォーマットの一つです。GIFファイルは、画像データだけでなく、その画像の論理的な画面記述子(Logical Screen Descriptor)や画像記述子(Image Descriptor)といったメタデータを含んでいます。これらの記述子には、画像の幅、高さ、カラーマップなどの情報が含まれており、デコーダはこれらの情報に基づいて画像データを正しく解釈する必要があります。
しかし、不正なGIFファイルや、意図的に破損させられたGIFファイルの場合、画像記述子に示される画像のサイズと、実際にファイルに含まれるピクセルデータの量が一致しないことがあります。このような状況は、デコーダが予期せぬ動作をしたり、メモリを過剰に消費したり、最悪の場合、セキュリティ上の脆弱性(例: サービス拒否攻撃)を引き起こす可能性があります。
このコミットの背景には、このような不正なGIFファイルに対するデコーダの堅牢性を高め、より安全で安定した画像処理を実現するという目的があります。特に、ピクセルデータの不足(errNotEnough
)や過剰(errTooMuch
)といった具体的なエラーを明確にすることで、デコード処理の信頼性を向上させています。
前提知識の解説
GIF (Graphics Interchange Format)
GIFは、1987年にCompuServeによって導入されたビットマップ画像フォーマットです。主にウェブ上でアニメーション画像や透過画像を表現するために使用されます。
- LZW (Lempel-Ziv-Welch) 圧縮: GIFはLZWという可逆圧縮アルゴリズムを使用しています。これにより、画質を損なうことなくファイルサイズを削減できます。LZW圧縮は、繰り返し現れるパターンを辞書に登録し、その辞書のインデックスで置き換えることでデータを圧縮します。
- ブロック構造: GIFファイルは、データブロックの連続として構成されています。画像データは、LZW圧縮されたバイト列がさらに255バイト以下のサブブロックに分割されて格納されます。各サブブロックの先頭にはそのサブブロックの長さを示すバイトがあり、0x00バイトのサブブロックがデータの終端を示します。
- Logical Screen Descriptor (論理画面記述子): GIFファイルの全体的なキャンバスサイズ、背景色、グローバルカラーマップの有無などの情報を含みます。
- Image Descriptor (画像記述子): 各画像ブロックの開始位置、幅、高さ、ローカルカラーマップの有無などの情報を含みます。
LZW (Lempel-Ziv-Welch) 圧縮/伸長
LZWは、データ圧縮に用いられる辞書ベースのアルゴリズムです。GIFにおいては、画像データを圧縮するために使用されます。
- エンコード: 入力データから繰り返し現れるシーケンスを検出し、それらを辞書に登録します。そして、そのシーケンスを辞書のエントリのインデックスに置き換えて出力します。
- デコード: 圧縮されたデータ(辞書インデックス)を読み込み、辞書を参照して元のシーケンスを再構築します。GIFのLZWデコードでは、通常、初期コードワード長(Initial Code Size)が指定され、それに基づいて辞書が初期化されます。
Go言語の image
パッケージと image/gif
パッケージ
Go言語の標準ライブラリには、画像処理のための image
パッケージとそのサブパッケージ群が含まれています。
image
パッケージ: 画像の基本的なインターフェース(image.Image
)や、色モデル(color.Color
、color.RGBA
など)、矩形(image.Rectangle
)などを定義しています。image.Paletted
は、GIFのようにパレット(カラーマップ)を使用する画像を表現するための構造体です。image/gif
パッケージ: GIFフォーマットのエンコードとデコード機能を提供します。Decode
関数はio.Reader
からGIFデータを読み込み、image.Image
インターフェースを実装する構造体(通常はimage.Paletted
)を返します。
技術的詳細
このコミットの主要な変更点は、GIFデコード処理におけるピクセルデータの整合性チェックの強化です。
-
新しいエラーの導入:
errNotEnough = errors.New("gif: not enough image data")
errTooMuch = errors.New("gif: too much image data")
これらのエラーは、それぞれピクセルデータが不足している場合と過剰な場合に返されるようになります。これにより、デコード失敗の原因がより明確になります。
-
blockReader
の改善:blockReader
は、GIFのLZW圧縮データが格納されているサブブロックを読み込むためのヘルパー構造体です。- 以前は
io.EOF
を直接返していましたが、b.err
フィールドを導入し、エラー状態を内部で保持するように変更されました。これにより、Read
メソッドがより堅牢になり、エラー伝播が改善されます。 io.ReadFull
のエラーチェックもb.err
を介して行われるようになりました。
- 以前は
-
decoder.decode
のロジック変更:decoder.decode
関数は、GIFファイルの主要なブロック(拡張ブロック、画像記述子、トレーラーなど)を解析し、画像データをデコードする中心的なロジックを含んでいます。- エラーハンドリングの統一: 以前は
Loop
ラベルとbreak Loop
を使用してエラー時にループを抜けていましたが、新しいコードではif err != nil { return err }
の形式で即座にエラーを返すように変更され、エラー伝播がより直接的になりました。 - LZWデコード後のデータ検証: 最も重要な変更点です。
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
でLZWデコーダが作成されます。ここでbr
はblockReader
のインスタンスです。io.ReadFull(lzwr, m.Pix)
でピクセルデータを読み込みます。ここでm.Pix
はimage.Paletted
のピクセルデータ配列です。- この
io.ReadFull
がio.ErrUnexpectedEOF
を返した場合、それはピクセルデータが不足していることを意味するため、errNotEnough
を返すように変更されました。 - LZWデコーダと
blockReader
の両方の枯渇チェック: LZWデコードが完了した後、LZWデコーダ (lzwr
) とその基盤となるblockReader
(br
) の両方が完全にデータを消費し尽くしているか(つまり、Read
を呼び出すと(0, io.EOF)
を返すか)を厳密にチェックするようになりました。if n, err := lzwr.Read(d.tmp[:1]); n != 0 || err != io.EOF { ... }
if n, err := br.Read(d.tmp[:1]); n != 0 || err != io.EOF { ... }
これらのチェックで(0, io.EOF)
以外の結果が返された場合、それはピクセルデータが過剰であることを意味するため、errTooMuch
を返すようになりました。
- トレーラーブロックの処理:
sTrailer
(0x3b) が読み込まれた際の処理も変更されました。以前は単にbreak Loop
でしたが、if len(d.image) == 0 { return io.ErrUnexpectedEOF }
というチェックが追加され、画像が一つもデコードされていない状態でトレーラーが来た場合はエラーを返すようになりました。
- エラーハンドリングの統一: 以前は
-
テストケースの追加 (
reader_test.go
): 新しいテストファイルsrc/pkg/image/gif/reader_test.go
が追加され、これらの新しいエラー条件(errNotEnough
とerrTooMuch
)が適切に発生するかどうかを検証するテストケースが記述されています。testCases
構造体でnPix
(ピクセル数) とextra
(余分なブロックの有無) を変化させ、期待されるエラー (wantErr
) を確認しています。lzwEncode
ヘルパー関数を使って、指定されたピクセル数のLZW圧縮データを作成しています。- 不正なピクセル数や余分なデータを含むGIFストリームを意図的に作成し、
Decode
関数が期待通りのエラーを返すか、または正しくデコードできるかを検証しています。
これらの変更により、GoのGIFデコーダは、GIFファイルのピクセルデータが画像の論理的な境界と一致しない場合に、より正確かつ早期にエラーを検出できるようになり、デコーダの堅牢性とセキュリティが向上しました。
コアとなるコードの変更箇所
src/pkg/image/gif/reader.go
// 新しいエラー変数の定義
var (
errNotEnough = errors.New("gif: not enough image data")
errTooMuch = errors.New("gif: too much image data")
)
// blockReader 構造体に err フィールドを追加
type blockReader struct {
r reader
slice []byte
err error // エラー状態を保持
tmp [256]byte
}
// blockReader.Read メソッドの変更
func (b *blockReader) Read(p []byte) (int, error) {
if b.err != nil { // 以前のエラーがあればそれを返す
return 0, b.err
}
// ... (既存のロジック) ...
// ReadByte や io.ReadFull のエラーを b.err に代入
blockLen, b.err = b.r.ReadByte()
// ...
if _, b.err = io.ReadFull(b.r, b.slice); b.err != nil {
return 0, b.err
}
// ...
}
// decoder.decode メソッドの変更
func (d *decoder) decode(r io.Reader, configOnly bool) error {
// ... (既存のロジック) ...
for { // ループ構造の変更 (Loop: for ... break Loop から for { ... return err } へ)
c, err := d.r.ReadByte()
if err != nil {
return err // エラー時に即座にリターン
}
switch c {
case sExtension:
if err = d.readExtension(); err != nil {
return err
}
case sImageDescriptor:
// ... (既存のロジック) ...
br := &blockReader{r: d.r} // blockReader のインスタンス化
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
if _, err = io.ReadFull(lzwr, m.Pix); err != nil {
if err != io.ErrUnexpectedEOF {
return err
}
return errNotEnough // ピクセルデータ不足の場合
}
// LZWデコーダと blockReader の両方が枯渇しているかチェック
if n, err := lzwr.Read(d.tmp[:1]); n != 0 || err != io.EOF {
if err != nil {
return err
}
return errTooMuch // ピクセルデータ過剰の場合
}
if n, err := br.Read(d.tmp[:1]); n != 0 || err != io.EOF {
if err != nil {
return err
}
return errTooMuch // ピクセルデータ過剰の場合
}
// ... (既存のロジック) ...
case sTrailer:
if len(d.image) == 0 { // 画像が一つもデコードされていない場合のチェック
return io.ErrUnexpectedEOF
}
return nil // 正常終了
default:
return fmt.Errorf("gif: unknown block type: 0x%.2x", c) // 不明なブロックタイプの場合
}
}
}
src/pkg/image/gif/reader_test.go
(新規追加)
package gif
import (
"bytes"
"compress/lzw"
"image"
"image/color"
"reflect"
"testing"
)
func TestDecode(t *testing.T) {
// テスト用のGIFヘッダとトレーラ
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"
)
// LZWエンコードヘルパー関数
lzwEncode := func(n int) []byte {
b := &bytes.Buffer{}
w := lzw.NewWriter(b, lzw.LSB, 2)
w.Write(make([]byte, n))
w.Close()
return b.Bytes()
}
// テストケースの定義
testCases := []struct {
nPix int // 画像データ内のピクセル数
extra bool // LZWエンコードデータ後に余分なブロックを書き込むか
wantErr error // 期待されるエラー
}{
{0, false, errNotEnough}, // ピクセルデータ不足
{1, false, errNotEnough}, // ピクセルデータ不足
{2, false, nil}, // 正常 (2x1画像なので2ピクセルが正しい)
{2, true, errTooMuch}, // ピクセルデータ過剰 (余分なブロックがある)
{3, false, errTooMuch}, // ピクセルデータ過剰 (3ピクセルある)
}
for _, tc := range testCases {
b := &bytes.Buffer{}
b.WriteString(header)
// 画像記述子とLZWデータ、終端ブロック、トレーラを書き込む
b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02") // 2x1画像
if tc.nPix > 0 {
enc := lzwEncode(tc.nPix)
b.WriteByte(byte(len(enc))) // LZWデータの長さ
b.Write(enc) // LZWデータ
}
if tc.extra {
b.WriteString("\x01\x02") // 余分なブロック
}
b.WriteByte(0x00) // 空のブロック (画像データの終端)
b.WriteString(trailer)
// Decode を実行し、エラーと結果を検証
got, err := Decode(b)
if err != tc.wantErr {
t.Errorf("nPix=%d, extra=%t\ngot %v\nwant %v", tc.nPix, tc.extra, err, tc.wantErr)
}
if tc.wantErr != nil {
continue
}
// 正常な場合の画像内容の検証
want := &image.Paletted{
Pix: []uint8{0, 0},
Stride: 2,
Rect: image.Rect(0, 0, 2, 1),
Palette: color.Palette{
color.RGBA{0x10, 0x20, 0x30, 0xff},
color.RGBA{0x40, 0x50, 0x60, 0xff},
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("nPix=%d, extra=%t\ngot %v\nwant %v", tc.nPix, tc.extra, got, want)
}
}
}
コアとなるコードの解説
このコミットの核心は、GIF画像のデコード時に、画像データがその宣言されたサイズと正確に一致するかどうかを厳密に検証する点にあります。
-
エラーの明確化:
errNotEnough
とerrTooMuch
という具体的なエラーを導入することで、デコード失敗の原因が「ピクセルデータが足りない」のか「ピクセルデータが多すぎる」のかを明確に区別できるようになりました。これは、デバッグやエラーハンドリングの際に非常に役立ちます。 -
blockReader
の堅牢化:blockReader
はLZWデコーダに生のバイトデータを提供する役割を担っています。このコミットでは、blockReader
自身がエラー状態を保持するb.err
フィールドを持つようになりました。これにより、Read
メソッド内で発生したエラーが適切に伝播され、デコード処理全体の一貫性が向上します。特に、io.ReadFull
の呼び出しでエラーが発生した場合に、そのエラーがb.err
に格納され、後続のRead
呼び出しで即座に返されるようになります。 -
decoder.decode
のロジック強化:- LZWデコード後の厳密なチェック: GIFの画像データはLZW圧縮されており、その後に0x00バイトの終端ブロックが続きます。このコミットでは、
io.ReadFull(lzwr, m.Pix)
でピクセルデータを読み込んだ後、LZWデコーダ (lzwr
) とその基盤となるblockReader
(br
) の両方が、その時点で完全にデータを消費し尽くしていることを確認します。lzwr.Read(d.tmp[:1])
とbr.Read(d.tmp[:1])
を呼び出し、戻り値が(0, io.EOF)
であることを期待します。- もし
n != 0
(読み込んだバイト数が0でない) またはerr != io.EOF
(エラーがEOFでない) であれば、それは予期せぬデータが残っている、つまりピクセルデータが過剰であることを意味し、errTooMuch
が返されます。 - 逆に、
io.ReadFull
がio.ErrUnexpectedEOF
を返した場合(これは、LZWデコーダが期待する量のデータを読み込む前に基盤となるリーダーがEOFに達したことを意味します)、errNotEnough
が返されます。
- トレーラーブロックの検証: GIFファイルの終端を示すトレーラーブロック (
sTrailer
) が現れた際に、それまでに少なくとも1つの画像がデコードされていることを確認するようになりました。これにより、空のGIFファイルや不正な構造のファイルに対するエラーハンドリングが改善されます。
- LZWデコード後の厳密なチェック: GIFの画像データはLZW圧縮されており、その後に0x00バイトの終端ブロックが続きます。このコミットでは、
-
包括的なテストの追加:
reader_test.go
に追加されたテストケースは、これらの新しいエラー条件を網羅的に検証しています。特に、意図的にピクセルデータを不足させたり、過剰にしたり、余分なブロックを追加したりすることで、デコーダが期待通りのエラーを返すことを確認しています。これにより、変更が正しく機能し、将来のリグレッションを防ぐための安全網が提供されます。
これらの変更は、GIFデコーダの堅牢性を大幅に向上させ、不正なGIFファイルに対する耐性を高めることで、Go言語の画像処理ライブラリ全体の信頼性を強化しています。
関連リンク
- Go言語の
image
パッケージドキュメント: https://pkg.go.dev/image - Go言語の
image/gif
パッケージドキュメント: https://pkg.go.dev/image/gif - GIF89a仕様 (英語): https://www.w3.org/Graphics/GIF/spec-gif89a.txt
- LZW圧縮アルゴリズム (Wikipedia): https://ja.wikipedia.org/wiki/LZW%E5%9C%A7%E7%B8%AE
参考にした情報源リンク
- Go言語の公式ドキュメント
- GIF89a仕様書
- LZW圧縮に関する一般的な情報源
- Go言語の
image
およびimage/gif
パッケージのソースコード