Pillow: TiffDecode.cのタイルおよびRGBAパスにおける、境界外アクセス(OOB)によるメモリリークが、ピクセルデータにヒープメモリを書き込む。
```
============================================================================
TiffDecode.cのタイルおよびRGBAパスにおける、境界外アクセス(OOB)によるメモリリークが、ピクセルデータにヒープメモリを書き込む。
場所: src/libImaging/TiffDecode.c:483 in _decodeTile()
コミット: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
```
Pillowは、特定の不正な圧縮TIFFファイルをデコードする際に、ヒープバッファの終端を超えてデータを読み込み、読み込んだバイトデータを、返される画像のピクセルにコピーします。 攻撃者が、アプリケーションに画像ファイルを開放・読み込ませる(例えば、サムネイル、プレビュー、または変換された画像を返すサービスにアップロードするなど)ことで、そのプロセスのヒープメモリの一部、およびポインタ値を取得することができます。 より大きなファイルの場合、プロセスがクラッシュします。 特別な設定は不要で、境界外への書き込みも発生しません。
このバグは、`src/libImaging/TiffDecode.c`の`_decodeTile()`および`_decodeAsRGBA()`に存在します。 両関数とも、libtiffが認識するピクセルレイアウトに基づいてデコードバッファのサイズを決定します(`TIFFTileSize()`/`TIFFTileRowSize()`、または`img.width * 4`)。 その後、`TiffImagePlugin._setup()`がPillow自身のディレクトリ解析に基づいて選択したアンパッカーを使用して、各行をアンパックしますが、アンパッカーへの入力行が、libtiffが生成した行内に収まっているかどうかを確認する処理は行われていません。 `_decodeStrip()`には、OSS-Fuzzの`crash-0da0`に対応するために8.2.0で追加された、まさにそのチェックが存在しますが、タイルおよびRGBAパスには適用されていません。
2つのパーサーは、重複した`PhotometricInterpretation`または`BitsPerSample`タグによって異なる結果を返す可能性があります。これは、libtiffが最初の出現箇所を保持するのに対し、Pillowが最後の出現箇所を保持するためです。Pillowは、libtiffが1~4バイトで埋めた行から、1ピクセルあたり4~8バイトを読み出しますが、その結果、各タイルの最後の行がバッファの範囲を超えて読み取られることがあります。攻撃者は、公開されるメモリ領域(バッファの少し先にあるタイル行)と、タイルの幅を選択します。このメモリ量は、タイルの数が増えても増加しません。なぜなら、バッファはすべてのタイルに対して再利用されるからです。
タイル分割機能は、1c295bf43で導入されました(8.2.0で初めてリリース)。RGBA分割機能は、eb8c1206dで導入され、CVE-2020-35654の修正(8.1.0)に含まれています。
添付の再現スクリプトは、変更されていないビルドに対して実行されます。このスクリプトは、エラーなく読み込まれる3つの、おおよそ400バイトのファイルを生成します。しかし、各ファイルの最後のピクセル行は、glibcのmallocによるメモリ領域の初期化パターンによって変化し、ヒープポインタのバイトが含まれています。また、65536x16のタイルを持つ4番目のファイルは、`copy4` ← `_decodeTile` (`TiffDecode.c:483`)でセグメンテーション違反を引き起こします。これは、バッファの後にマッピングされている内容によって、実行結果が異なる場合があります。
## 詳細
`src/libImaging/TiffDecode.c` 内の `_decodeTile()` と `_decodeAsRGBA()` 関数では、`state->buffer` のサイズを、libtiff が生成すると予測するサイズ(`TIFFTileSize()` または `img.width * 4`)に基づいて設定しますが、各行を、Pillow が IFD を解析した結果で選択されたシャッフリング処理を用いて展開します。これらの関数には、展開処理の入力行のサイズ(`width * state->bits / 8` バイト)が、libtiff が書き込んだ行のサイズに収まるかどうかを確認する処理は含まれていません。`_decodeStrip()` 関数にはそのチェックがありますが、他の2つの関数にはありません。IFD パーサーがピクセル配置について異なる解釈をする場合(libtiff が無視するが Pillow が受け入れる、誤ったカウントや種類のタグ、または重複した `PhotometricInterpretation`/`BitsPerSample`/`SamplesPerPixel` エントリ(libtiff は最初のものを保持し、`ImageFileDirectory_v2.load()` は最後のものを保持する)など)が発生すると、展開処理がヒープバッファの末尾を超えて読み込みを行い、読み込まれたバイトが返される画像のピクセルとして扱われます。libtiff の実際のサンプル配置よりも広い Pillow の rawmode(`RGBX`, `RGB;16N`, `RGBA;16N`, `I;32`, `F;32F`, …)はすべて動作します。`RGBA;16N`(1ピクセルあたり64ビット)と、1ビットの libtiff データとの組み合わせは、最も大きな差があり、各タイル内の最初の行以降の行は、ほとんどがバッファの範囲外から読み込まれます。
リプロダサーによって生成された、約400バイトのTIFFファイル `tile_count2.tif` — 1つの16x16タイル、`BitsPerSample 8`、`SamplesPerPixel 1`、`Compression 8` (Adobe Deflate)、および `PhotometricInterpretation` が SHORT型で **count 2** (`6, 6`) と記述されている — が、`Image.open()` によって `RGB` モードで開かれ、libtiffのrawmode `RGBX` で読み込まれます。標準ビルドでは `load()` が成功します。結果の13行目から15行目には、256バイトのタイルバッファから16バイト、32バイト、48バイトの位置にあるデータが含まれています。`MALLOC_PERTURB_=85` および `=170` の下で同じファイルを2回デコードすると、0行目から12行目は同じですが、13行目から15行目のデータは異なります (`aa` と `55`、つまりglibcのフィルパターン)。拡張機能のASanビルドでは、同じ `load()` が `copy4` (`src/libImaging/Unpack.c:1290`) で `heap-buffer-overflow READ of size 64` を報告します。これは `src/libImaging/TiffDecode.c:483` から呼び出され、`TiffDecode.c:448` で割り当てられた256バイトの領域に対して発生します。同様の構成で、単一の65536x16タイル(4.5 KBのファイル)を使用すると、標準ビルドでSIGSEGVが発生し、ほとんどの場合、マッピングされていないアドレスが読み取られます(エラーが発生するかどうかは、1 MiBバッファの後に何がマッピングされているかによって異なります。より大きなタイルを使用すると、この問題が再現しやすくなります)。
### なぜ2つのパーサーが異なる結果になるのか
libtiff 4.5 は、countが1でない `PhotometricInterpretation` エントリを拒否します (`TIFFFetchNormalTag: Incorrect count for "PhotometricInterpretation"; tag ignored`)。そのため、このファイルでは `TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, …)` が0を返し、libtiffはデータを1ピクセルあたり8ビットのサンプルとして扱います。`TIFFTileSize() = 256`、`TIFFTileRowSize() = 16` となります。
Pillowは最初の値を保持し、処理を続行します。
```python
# src/PIL/TiffImagePlugin.py:762-770 (ImageFileDirectory_v2._setitem, load()から呼び出される)
try:
(dest[tag],) = values
except ValueError:
# We've got a builtin tag with 1 expected entry
warnings.warn(f"Metadata Warning, tag {tag} had too many entries: ...")
dest[tag] = values[0]
```
したがって、`_setup()`関数では`photo == 6`という値が設定されます。`OPEN_INFO[(II, 6, (1,), 1, (8,), ())]`の値は`("L", "L")`であり、ファイルが圧縮されているため、libtiffの処理がrawmodeを上書きします。
```python
# src/PIL/TiffImagePlugin.py:1578, 1600-1607
self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw"
…
if photo == 6:
self._mode = "RGB"
if self._compression in "jpeg" and self._planar_configuration == 1:
rawmode = "RGB"
else:
rawmode = "RGBX"
```
`RGBX`は32ビットのrawmodeであり、`get_unpacker()`関数(`src/decode.c:301-309`)が`state->bits = 32`と`state->shuffle = copy4`を設定します。`photo == 6`という条件による上書きの意図は、libtiffが`TIFFRGBAImage`を通じてRGBAを返すようにすることですが、その判断はC言語で、*libtiff*がタグを読み取る際に下されます。この場合、「YCbCrではない」という情報が読み込まれます。
```c
// src/libImaging/TiffDecode.c:737-764
TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); // unset -> stays 0
…
readAsRGBA = photometric == PHOTOMETRIC_YCBCR; // 0
…
if (TIFFIsTiled(tiff)) {
_decodeTile(im, state, tiff, planes, unpackers); // unpackers[0] = copy4
```
### 欠けているチェック
`_decodeTile()`関数は、libtiffのタイルが、アンパッカーが期待するサイズよりも*大きい*場合にのみ処理を行い、正確に`TIFFTileSize()`バイトを割り当て、`row_byte_size`の間隔でデータをシャッフラーに渡します。
```c
// src/libImaging/TiffDecode.c:403-487 (_decodeTile)
tile_bytes_size = TIFFTileSize(tiff); // 256
…
row_byte_size = TIFFTileRowSize(tiff); // 16
…
if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) {
// 256 > 1024 は偽 -> エラーは発生しない
…
}
state->bytes = tile_bytes_size;
new_data = realloc(state->buffer, state->bytes); // 256バイトのバッファ (448行目)
…
for (tile_y = 0; tile_y < current_tile_length; tile_y++) {
shuffler( // copy4: current_tile_width * 4 = 64バイトを読み込む
(UINT8 *)im->image[tile_y + y] + x * im->pixelsize,
state->buffer + tile_y * row_byte_size, // オフセットは16 * tile_y
current_tile_width
);
```
`tile_y`行目は、`[16·tile_y, 16·tile_y + 64)` の範囲から読み込まれます。`tile_y` が 13, 14, 15 の場合、256バイトの割り当てを超えて16、32、48バイト読み込まれます。一般的に、各タイルの最後の行では、最大で `(state->bits / libtiff_bits_per_pixel − 1) × row_byte_size` バイトまで読み込みが行われ、`row_byte_size` は攻撃者が選択する `TileWidth` に依存して変化します。
また、434行にある既存の片方向チェックでは、ファイルで制御される`tile_width`/`tile_length`(`UINT32`型で、428行で`INT_MAX`まで許容)が、32ビットの符号なし演算で`state->bits`と乗算されます。そのため、`tile_width * state->bits`は、`tile_width ≥ 2^32 / bits`の場合にオーバーフローします(64ビットのrawmodesでは2^26、`RGBX`/`I;32`/`F;32F`では2^27)。8 KBのファイルで、2,097,152×1の画像を`TILEWIDTH = 2^26+16`、`TILELENGTH = 1`、libtiffでは1ビットデータ、Pillowでは`RGBA;16N`で記述すると、libtiffは〜8 MBのタイルを割り当てますが、`unpackRGBA16L`が16 MBを読み込み、標準ビルドで`unpackRGBA16L ← _decodeTile (TiffDecode.c:483)`でSIGSEGVが発生します。434行と同じ32ビット演算で記述された行サイズ比較では、その行を128バイトと評価し、それでも受け入れてしまうため、新しいチェックは64ビットの型で行う必要があります。
stripパスでは、この状況を拒否します(8.2.0でOSS-Fuzzの`crash-0da0`に対応するために追加):
```c
// src/libImaging/TiffDecode.c:528-548 (_decodeStrip)
unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8;
…
row_byte_size = TIFFScanlineSize(tiff);
// unpackerが計算した行サイズが、行のバイトサイズよりも大きい場合、少なくともstripの最後の行で、
// 読み込みバッファのオーバーフローが発生します。
if (row_byte_size == 0 || unpacker_row_byte_size > row_byte_size) {
state->errcode = IMAGING_CODEC_BROKEN;
return -1;
}
```
同じファイルで、1つのタイルではなく1つのstripで記述すると、このチェックにより`load()`が`OSError: decoder error -2`で失敗します。
### `_decodeAsRGBA()`でも同様の問題が発生
`_decodeAsRGBA()`関数は、常に`img.width * 4`バイトのデータを各行に対して生成します(`TiffDecode.c:325`、バッファは339行で割り当てられます)。そして、`state->shuffle`を使用してデータを展開しますが(373行)、`state->bits`の値は考慮されません。これは、*libtiff*がYCbCrを認識しているのに対し、Pillowが`RGBX`を強制していない場合に発生します。libtiffは、後に出現する同じタグを無視します(最初の出現が優先されます)。一方、Pillowの`ImageFileDirectory_v2.load()`関数は、タグを上書きします(`TiffImagePlugin.py:945`、最後の値が優先されます)。再現に使用された`ycbcr_dup.tif`ファイルは、16x16のDeflateストリップで、`PhotometricInterpretation`が2回(`6`、次に`2`)および`BitsPerSample`が2回(`8,8,8`、次に`16,16,16`)記述されています。libtiffは、8ビットのYCbCrを1024バイトのRGBAラスターにデコードし、Pillowは`RGB;16N`を選択します(`state->bits = 48`)。そして、`unpackRGB16L`関数は、64バイトの行に対して96バイトを読み込みます。ASanは、`unpackRGB16L`関数(`Unpack.c:609`)で、`TiffDecode.c:373`から1024バイトの領域を超えたバッファオーバーフローを報告しています(`TiffDecode.c:339`)。
カウントが2つのエンコーディングは、唯一の問題ではありません。また、リリース版では、これは問題を引き起こしません。1サンプルで`photo == 6`というファイルは、未リリースのコミット155330d63(「YCbCrカラースペースの画像をlibtiffなしでRGBとして開かない(#9779)」(2026-08-26))以降、rawmodeとして`RGBX`のみが適用されます。リリース版では、重複したタグによってこの状態が発生します。3サンプルでタイル状に配置されたファイルで、`PhotometricInterpretation`が2つ(`1`または`2`、次に`6`)記述されている場合、同じ`copy4` ← `_decodeTile`による過読み込みが発生します。これは、libtiffが最初の値を保持し、1ピクセルあたり3バイトをデコードするのに対し、Pillowは`6`を保持し、4バイトを展開するためです(再現に使用された`tile_dup.tif`)。上記の`ycbcr_dup.tif`ファイルも、同様の方法で`_decodeAsRGBA()`関数に到達します。
## 影響
`Image.open()` と `load()` (または `thumbnail()`, `convert()`, `save()`, ...) を信頼できないファイルに対して呼び出すアプリケーションは、デフォルト設定ではこの問題に遭遇します。ファイルは TIFF として認識され、圧縮された TIFF は `libtiff` と `TiffDecode.c` を使用してデコードされますが、`READ_LIBTIFF` は設定されていません。読み込み済みのバイトが画像のピクセル行にコピーされるため、サムネイル、プレビュー、または変換された画像を返すサービスは、アップローダーに隣接するヒープメモリを返します。このヒープメモリには、ヒープポインタの値が含まれており、`RGBX`-8ビットを超える場合、最大で 3 つの `libtiff` のタイル行分、つまりバッファの先までです。タイル行の長さ (`TileWidth`) は攻撃者によって選択されます。`state->buffer` は一度割り当てられ、すべてのタイルで使用されるため、各タイルは同じ領域をそのバッファの先で再読み込みします。画像ごとに公開されるヒープの量は、約 `(unpacker_bpp / libtiff_bpp − 1)` のタイル行であり、タイル数が増加しても増加しません。タイルが広い場合、読み込みはマッピングの範囲外になり、プロセスがセグメンテーション違反を引き起こします (65536x16 の場合、多くの環境で標準ビルドがクラッシュします。これは、バッファの後にマッピングされている内容によって異なります)。これにより、リクエストの 1 つだけでなく、すべてのワーカープロセスが停止します。書き込みは、適切なサイズの `Imaging` 行に書き込まれるため、これはメモリ破壊ではなく、情報漏洩またはクラッシュを引き起こします。
これは、CVE-2020-35654 (Pillow と libtiff が `TiffDecode.c` で YCbCr IFD を異なる解釈で処理する) と同じ問題のファミリーです。その修正と、その後の `crash-0da0` の修正により、書き込みとストリップパスの読み込みは修正されましたが、タイルパスと RGBA パスは、依然として `libtiff` の行サイズよりも Pillow の rawmode を信頼しています。タイルパスのギャップは、1c295bf43c3a ("Check for dimensions and sizes to fit into int", 2021-01-25) で導入されました。このコミットでは、`_decodeTile()` が Pillow のアンパッカーによってサイズが決定されるバッファから、`TIFFTileSize()`/`TIFFTileRowSize()` に変更されました。これは Pillow 8.2.0 で初めて導入されました。`_decodeAsRGBA()` の部分は、eb8c1206d6b1 ("Fix CVE-2020-35654 - OOB Write in TiffDecode.c") で修正され、Pillow 8.1.0 で初めて導入されました。
## 再現手順
インストールされた、変更されていないPillowに対して、`repro`(単一のPython 3スクリプト)を実行します。サニタイザーのビルドは不要です。このスクリプトは、一時ディレクトリに4つのTIFFファイルを書き込み、各ファイルを子プロセスでデコードし、最後にそのディレクトリを削除します。
このスクリプトは、小さな従来のTIFFライターを使用して、TIFFファイルをバイトごとに書き込みます。各ファイルには、1つのIFDと、1つのDeflate圧縮ブロックが含まれています。Deflateは、ファイルがlibtiffと`TiffDecode.c`によってデコードされるようにするためだけに存在します。圧縮データ内のすべてのピクセルバイトが定義されているため、デコードされた画像は、プロセス内の他の要素に依存すべきではありません。
- **`tile_count2.tif`**: 16x16の、サンプル数が1つ、8ビットのタイルで、`PhotometricInterpretation`が「SHORT count 2」として記述されており、`(6, 6)`です。Pillowは`6`を保持し、`copy4`(1ピクセルあたり4バイトの`RGBX`)で展開します。libtiffは、このタグを無視し、1ピクセルあたり1バイトをデコードします。この問題には、155330d63(未リリース)が必要です。
- **`tile_dup.tif`**: 16x16の、サンプル数が3つ、8ビットのタイルで、`PhotometricInterpretation`が2回記述されており、最初に`2`、次に`6`です。libtiffは`2`(1ピクセルあたり3バイトのRGB)を保持し、Pillowは`6`(1ピクセルあたり4バイトの`RGBX`)を保持します。このバリエーションは、リリース版にも適用されます。
- **`ycbcr_dup.tif`**: 16x16のストリップで、`BitsPerSample`が最初に`8,8,8`、次に`16,16,16`、`PhotometricInterpretation`が最初に`6`、次に`2`です。libtiffは、`_decodeAsRGBA()`(1ピクセルあたり4バイト)を使用して8ビットのYCbCrをデコードし、Pillowは`unpackRGB16L`(1ピクセルあたり6バイト)を使用して`RGB;16N`を展開します。
- **`tile_big.tif`**: `tile_count2.tif`と同じレイアウトで、単一の65536x16のタイルです。
最初の3つのファイルについて、スクリプトは `Image.open()` + `load()` + `tobytes()` を2回実行します。1回は `MALLOC_PERTURB_=85` を、もう1回は `=170` を指定して実行します。もし2つの出力が異なる場合、デコードバッファを超えたデータがピクセルになっている可能性があります。その場合、スクリプトは最初に異なるオフセットと、その周辺のバイトを両方の実行から出力します。このテストセットでは、3つのファイルすべてで最後の行が異なっており、`aa`/`55` のフィルバイトや、ヒープポインタのバイトが表示されます。次に、スクリプトは `tile_big.tif` をデコードし、SIGSEGV(終了ステータス -11)が発生するかどうかを確認します。`gdb` が利用可能な場合、スクリプトは以下のフレームを出力します: `copy4` (`Unpack.c:1290`) ← `_decodeTile` (`TiffDecode.c:483`) ← `ImagingLibTiffDecode`。このクラッシュは ASLR に依存するため、まれに正常に実行されることがあります。
各ステップでは、使用した `_imaging` のバージョンと、Pillow がファイルをどのように開いたか(モード、サイズ、rawmode)を出力します。スクリプトは、1行の要約で終了します。最後の行は、いずれかのファイルでヒープのバイトがピクセルに漏れ出している場合、または大きなタイルがプロセスをクラッシュさせた場合は `BUG` となります。それ以外の場合は `OK` となります。
## 推奨される修正
添付のパッチは、`src/libImaging/TiffDecode.c` 内の2つの関連するデコーダで、`_decodeStrip()` の不変条件(アンパッカーの入力行は、libtiff の行内に収まる必要がある)を復元します。これは、各デコーダがバッファのサイズを決定する箇所です。
- `_decodeTile()` は、`unpacker_row_byte_size = ((tsize_t)tile_width * state->bits / planes + 7) / 8` を計算します(`tsize_t` へのキャストは、積が32ビットの演算範囲を超えないようにするためです。これにより、上記の `TILEWIDTH = 2^26+16` の場合でも、64ビット環境でのチェックを通過できます)。この値は、既存の `TIFFTileSize()` の整合性チェックで使用され、`_decodeStrip()` がストリップに対して行うのと同じです。このチェックの後、`realloc` の前に、`unpacker_row_byte_size > row_byte_size` の場合、`IMAGING_CODEC_BROKEN` エラーが発生します(`TIFFTileRowSize()`)。
- `_decodeAsRGBA()` は、`state->bits > 32` の場合、`IMAGING_CODEC_BROKEN` エラーが発生します。これは、`TIFFRGBAImage` の行は常に1ピクセルあたり32ビットであるためです。このチェックは、`TIFFRGBAImageBegin()` や、メモリの割り当てが行われる前に実行されます。
この修正により、タイルファイルとRGBAファイルで `load()` 関数が `OSError: decoder error -2` というエラーで失敗します。これは、既に同様のエラーが発生しているストリップファイルと同じ結果です。この修正には、`Tests/test_file_libtiff.py` に回帰テスト (`test_unpacker_row_wider_than_libtiff_row`) が追加されています。このテストでは、カウント2のタイルと、重複したタグを持つYCbCrストリップを、既存のファイル形式に従ってBase64で埋め込んでいます。この修正は、クリーンな状態のコードツリーに適用され、再構築されました。その結果、再現手順が動作しなくなり、`Tests/test_file_tiff.py`、`Tests/test_file_libtiff.py`、および `Tests/test_tiff_crashes.py` が正常に実行されるようになりました。
これらのチェックは、過剰な読み込みを回避しますが、2つのパーサー間で意見の相違が生じる可能性があります。また、`_setup()` 関数で、libtiffが異なる方法で読み取るタグから、libtiffの生のモードを推測するのを停止させることもできます。例えば、`PhotometricInterpretation`/`BitsPerSample`/`Compression` が単一の型付きの値ではない場合、「不明なピクセルモード」として扱うようにする(`ImageWidth`/`ImageLength` が `TiffImagePlugin.py:1473` で既に `int` であるのと同じように)、または `ImageFileDirectory_v2.load()` 関数で、libtiffと同様に、繰り返されるタグの最初の出現のみを保持する(これは、不正なIFDに対する意図的な動作変更であり、別個に行う必要があります)。しかし、これはC側の問題点を完全に解消するのではなく、問題が発生する可能性を狭めるだけです。`TiffImagePlugin` は、libtiff がどのように不正なディレクトリを解決するかを知ることができないため、デコーダー側の比較が実際に読み込みを制限しています。
## 修正内容
`_decodeTile()` および `_decodeAsRGBA()` 関数で、アンパッカーが行の幅が libtiff がデコードする行の幅よりも広い場合、TIFFファイルを拒否します。これは、`_decodeStrip()` 関数ですでに実装されている動作です。
この修正は、上記のコミットで `src/libImaging/TiffDecode.c` に適用されます。
```diff
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 3d804ea4a..c08dfe77c 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -1232,6 +1232,50 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(OSError, match="decoder error -9"):
im.load()
+ @pytest.mark.parametrize(
+ "data",
+ (
+ # 16x16の8ビットグレースケールタイル。PhotometricInterpretationタグが2つの値を持つ。
+ # PillowはこれをYCbCrとして読み取る(rawmode RGBX、1ピクセルあたり4バイト)
+ # 一方、libtiffはこれを無視して、1ピクセルあたり1バイトとしてデコードする。
+ (
+ b"SUkqAAgAAAAKAAABBAABAAAAEAAAAAEBBAABAAAAEAAAAAIBAwABAAAACAAA"
+ b"AAMBAwABAAAACAAAAAYBAwACAAAABgAGABUBAwABAAAAAQAAAEIBBAABAAAA"
+ b"EAAAAEMBBAABAAAAEAAAAEQBBAABAAAAhgAAAEUBBAABAAAACwEAAAAAAAB4"
+ b"nAEAAf/+AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUm"
+ b"JygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJT"
+ b"VFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+A"
+ b"gYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6yt"
+ b"rq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna"
+ b"29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+foAAQIDBJ9Bepo="
+ ),
+ # 16x16のストリップ。重複したBitsPerSampleとPhotometricInterpretation
+ # タグを持つ。libtiffは最初のものを保持する(8ビットYCbCr、1ピクセルあたり4バイトとして読み取る)
+ # 一方、Pillowは最後のものを保持する(16ビットRGB、rawmode RGB;16N、1ピクセルあたり6バイト)。
+ (
+ b"SUkqAAgAAAAMAAABAwABAAAAEAAAAAEBAwABAAAAEAAAAAIBAwADAAAAugEA"
+ b"AAIBAwADAAAAwAEAAAMBAwABAAAACAAAAAYBAwABAAAABgAAAAYBAwABAAAA"
+ b"AgAAABEBBAABAAAAngAAABUBAwABAAAAAwAAABYBAwABAAAAEAAAABcBBAAB"
+ b"AAAAHAEAABICAwACAAAAAQABAAAAAAB4nGNgZGJmYWVj5+Dk4ubh5eMXEBQS"
+
```
```
b"FhEVE5eQlJKWkZWTV1BUUlZRVVPX0NTS1tHV0zcwNDI2MTUzt7C0sraxtbN3"
+ b"cHRydnF1c/fw9PL28fXzDwgMCg4JDQuPiIyKjomNi09ITEpOSU1Lz8jMys7J"
+ b"zcsvKCwqLiktK6+orKquqa2rb2hsam5pbWvv6Ozq7unt658wcdLkKVOnTZ8x"
+ b"c9bsOXPnzV+wcNHiJUuXLV+xctXqNWvXrd+wcdPmLVu3bd+xc9fuPXv37T9w"
+ b"8NDhI0ePHT9x8tTpM2fPnb9w8dLlK1evXb9x89btO3fv3X/w8NHjJ0+fPX/x"
+ b"8tXrN2/fvf/w8dPnL1+/ff/x89fvP3///WcY9f+I9j8AoGJ+kAgACAAIABAA"
+ b"EAAQAA=="
+ ),
+ ),
+ )
+ @pytest.mark.filterwarnings("ignore:Metadata Warning")
+ def test_unpacker_row_wider_than_libtiff_row(self, data: bytes) -> None:
+ # Pillowが選択するアンパッカーは、libtiffでデコードされるよりも1行あたりに多くのバイトを読み込むため、
+ # デコーダーは、バッファの末尾を超えて読み込まずに、画像全体を拒否する必要があります。
+ with Image.open(io.BytesIO(base64.b64decode(data))) as im:
+ with pytest.raises(OSError, match="decoder error -2"):
+ im.load()
+
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256))
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 8a57c523d..2ee6c5f0a 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -304,6 +304,15 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) {
TRACE(("RowsPerBlock: %u \n", rows_per_block));
+ // TiffRGBAImagesは、1ピクセルあたり32ビットです。アンパッカーが、
+ // 1ピクセルあたり32ビットよりも多くのビットを読み込む場合、
+ // (少なくとも)ブロックの最後の行で、読み込みバッファのオーバーフローが発生します。
+ if (state->bits > 32) {
+ TRACE(("デコードエラー、アンパッカーのビット数 %d が 32 より大きい\n", state->bits));
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return -1;
+ }
+
if (!(TIFFRGBAImageOK(tiff, emsg) && TIFFRGBAImageBegin(&img, tiff, 0, emsg))) {
```
TRACE(("デコードエラー、メッセージ: %s", emsg));
state->errcode = IMAGING_CODEC_BROKEN;
@@ -397,7 +406,7 @@ _decodeTile(
) {
INT32 x, y, tile_y, current_tile_length, current_tile_width;
UINT32 tile_width, tile_length;
- tsize_t tile_bytes_size, row_byte_size;
+ tsize_t tile_bytes_size, row_byte_size, unpacker_row_byte_size;
UINT8 *new_data;
tile_bytes_size = TIFFTileSize(tiff);
@@ -431,7 +440,8 @@ _decodeTile(
return -1;
}
- if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) {
+ unpacker_row_byte_size = ((tsize_t)tile_width * state->bits / planes + 7) / 8;
+ if (tile_bytes_size > (unpacker_row_byte_size * tile_length)) {
// If the tile size as expected by LibTiff isn't what we're expecting, abort.
// man: TIFFTileSize returns the equivalent size for a tile of data as it
// would be returned in a call to TIFFReadTile ...
@@ -439,6 +449,13 @@ _decodeTile(
return -1;
}
+ // if the unpacker calculated row size is > row byte size, (at least) the last
+ // row of the tile will have a read buffer overflow.
+ if (unpacker_row_byte_size > row_byte_size) {
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return -1;
+ }
+
state->bytes = tile_bytes_size;
TRACE(("TIFFTileSize: %d\n", state->bytes));
```
## 再現手順
<details>
<summary>再現手順 — <code>repro</code>、4,178 bytes (クリックして開きます)</summary>
最後の行は、問題が発生すると `BUG`、発生しない場合は `OK` と表示されます。クリーンな状態のそのコミットで再実行すると、`BUG` と表示されました。
```python
#!/usr/bin/env python3
# _decodeTile()/_decodeAsRGBA() 関数におけるヒープオーバーリード (src/libImaging/TiffDecode.c)。標準ビルドで、
# サニタイザーは不要です。Pillow は、独自の IFD 解析から行のアンパッカーを選択し、libtiff は、
# デコードバッファのサイズを自身の解析から決定します。これらが一致しない場合、アンパッカーは state->buffer を超えて読み込み、
# そのバイトが画像に書き込まれます。証拠: (1) 完全な 16x16 ファイルのピクセルが、glibc の MALLOC_PERTURB_ フィルバイトによって変化する (バッファを超えたヒープのバイトがピクセルになる); (2) 65536x16 のタイルで、copy4 関数で SIGSEGV が発生する <- _decodeTile。数秒で実行されます。
import os, shutil, struct, subprocess, sys, tempfile, zlib
W = tempfile.mkdtemp()
S, L = 3, 4
def tiff(entries, data): # リトルエンディアンの標準 TIFF、1 つの IFD、タグは指定された順序
data_off = 8 + 2 + 12 * len(entries) + 4
extra_off = data_off + len(data)
ifd, extra = struct.pack("<H", len(entries)), b""
for tag, typ, vals in entries:
if vals is None:
typ, vals = L, [data_off]
raw = b"".join(struct.pack("<H" if typ == S else "<L", v) for v in vals)
if len(raw) > 4:
raw, extra = struct.pack("<L", extra_off + len(extra)), extra + raw
ifd += struct.pack("<HHL", tag, typ, len(vals)) + raw.ljust(4, b"\0")
return b"II*\0" + struct.pack("<L", 8) + ifd + b"\0" * 4 + data + extra
def tile(w, h, spp, photo): # 1 つの deflate タイル、8 ビット/サンプル
d = zlib.compress(bytes(i % 251 for i in range(w * h * spp)))
return tiff([(256, L, [w]), (257, L, [h]), (258, S, [8] * spp), (259, S, [8])] + photo +
[(277, S, [spp]), (322, L, [w]), (323, L, [h]), (324, L, None), (325, L, [len(d)])], d)
```
d = zlib.compress(bytes(range(256)) * 3)
files = {
# Photometric count 2 (6,6): libtiff はタグを無視して 1 B/px にする; Pillow は 6 を保持し、RGBX copy4 で 4 B/px (HEAD のみ)
"tile_count2.tif": tile(16, 16, 1, [(262, S, [6, 6])]),
# Photometric duplicated 2 then 6, 3 samples: libtiff は RGB で 3 B/px; Pillow は RGBX で 4 B/px (リリース版も)
"tile_dup.tif": tile(16, 16, 3, [(262, S, [2]), (262, S, [6])]),
# strip, libtiff は 8-bit YCbCr として認識される -> _decodeAsRGBA で 4 B/px; Pillow は RGB;16 として認識される -> 6 B/px
"ycbcr_dup.tif": tiff([(256, S, [16]), (257, S, [16]), (258, S, [8, 8, 8]), (258, S, [16, 16, 16]), (259, S, [8]),
(262, S, [6]), (262, S, [2]), (273, L, None), (277, S, [3]), (278, S, [16]), (279, L, [len(d)]),
(530, S, [1, 1])], d),
"tile_big.tif": tile(65536, 16, 1, [(262, S, [6, 6])]),
}
for n, b in files.items():
open(f"{W}/{n}", "wb").write(b)
LOAD = r"""
import sys, warnings; warnings.simplefilter("ignore")
from PIL import Image
im = Image.open(sys.argv[1]); print(Image.core.__file__, im.mode, im.size, im.tile[0].args[0], file=sys.stderr)
im.load(); sys.stdout.write(im.tobytes().hex())
"""
def run(name, perturb):
p = subprocess.run([sys.executable, "-c", LOAD, f"{W}/{name}"], capture_output=True, text=True,
env=dict(os.environ, MALLOC_PERTURB_=str(perturb)))
return p.returncode, p.stdout, p.stderr.strip().splitlines()[-1:] or [""]
leak = False
for n in ("tile_count2.tif", "tile_dup.tif", "ycbcr_dup.tif"):
r1, a, info = run(n, 85); r2, b, _ = run(n, 170)
print(f"== {n}: {info[0]} rc={r1},{r2}")
if r1 == 0 and r2 == 0 and a != b:
i = next(k for k in range(0, len(a), 2) if a[k:k+2] != b[k:k+2])
print(f" ピクセルデータのバイト列は、malloc のフィルパターンに依存し、バイト {i//2} の {len(a)//2} から:")
print(f" PERTURB=85 : ...{a[i-8:i+40]}\n PERTURB=170: ...{b[i-8:i+40]}")
leak = True
else:
print(" 同一の出力 / エラー -> 漏洩は確認されませんでした")
```
rc, _, info = run("tile_big.tif", 0)
print(f"== tile_big.tif: {info[0]} rc={rc}" + (" (SIGSEGV)" if rc == -11 else ""))
crash = rc == -11
if crash and shutil.which("gdb"):
bt = subprocess.run(["gdb", "-batch", "-ex", "run", "-ex", "bt 4", "--args", sys.executable, "-c", LOAD,
f"{W}/tile_big.tif"], capture_output=True, text=True).stdout
print("\n".join(l for l in bt.splitlines() if l.startswith("#")))
shutil.rmtree(W)
print(f"summary: heap bytes in pixels={leak} stock SIGSEGV={crash}")
print("BUG" if leak or crash else "OK")
```
---
影響を受けるパッケージ:
- pip (Python) Pillow: 影響を受けるバージョン >= 8.1.0, < 13.0.0、修正されたバージョン 13.0.0