Pillow: 巧妙に作成されたTIFFタグを介した、libtiffエンコーダにおけるメモリ情報漏洩とセグメンテーション違反

```
============================================================================
巧妙に作成されたTIFFタグを介した、libtiffエンコーダにおけるメモリ情報漏洩とセグメンテーション違反
場所: src/encode.c:1014 PyImaging_LibTiffEncoderNew()関数内
コミット: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
```

数百バイトの巧妙に作成されたTIFFファイルにより、Pillowは攻撃者が指定した量の自身のプロセスメモリを、書き戻すTIFFファイルにコピーするか、プロセス自体をクラッシュさせます。トリガーとなるのは、通常の`Image.open()`の後に、単純な`im.save(fp, "TIFF")`を実行することです。前提条件は、アプリケーションが画像をTIFF形式で再保存することのみです。アップロードされたファイルが圧縮されると、Pillowは自動的にlibtiffを介して保存処理を行い、アプリケーションからのオプションは一切使用されません。添付の再現コードは、356バイトのLZW TIFFファイルを使用しています。巧妙に作成されたタグの1つは、`_imaging.so`の読み取り専用データから取得した65536バイトを保持し、別の巧妙に作成されたタグは、実行ごとにワーカプロセスをSIGSEGVでクラッシュさせます。

このバグは、`PyImaging_LibTiffEncoderNew`関数(`src/encode.c`内)にあります。この関数は、Pythonの値(スカラー、タプル、またはバイト列)の形状に基づいて、variadicな`TIFFSetField()`関数に渡す引数を決定します。この関数は、`ImagingLibTiffMergeFieldInfo()`関数がlibtiffがそれらをどのように読み取るかを定義すると想定しています。しかし、`TIFFMergeFieldInfo()`関数は、libtiffの組み込みフィールドテーブルを上書きすることはありません。そのため、libtiffは、タグの独自の定義に従って引数を読み取ります。

- 例えば、DNGPrivateData(50740)のようなカウント付き配列タグの場合、ソースファイル内のカウント-1の整数が、バイト数として扱われます。その後、データポインタがPillowが書き込んだことのない引数スロットから読み取られ、そのバイト数分のデータが出力にコピーされます。
- ActiveArea(50829)のような固定サイズの配列タグの場合、整数自体がソースアドレスとして使用されます。

その未定義の領域が指す場所は、コンパイラによって異なります。テストに使用したclangのビルドでは、`_imaging.so`を指していました。また、より多くの要素を返す場合、libpython、libc、libtiffといったライブラリ内の数千のポインタが返され、これにより、他のバグを悪用しようとする攻撃者に対するアドレス空間のランダム化が無効になります。アプリケーションが`tiffinfo=im.tag_v2`でメタデータを保持している場合、64ビットのエントリがActiveAreaを、攻撃者が選択したアドレスから16バイトの読み込みに変換します。観測されたメモリのエラーはすべて読み込み操作であり、範囲外への書き込み操作は見つかりませんでした。

同じループの他の部分も同様の欠陥を抱えており、作成されたTIFFファイル、またはJPEG/PNG/WebPファイルを圧縮TIFFとして保存する際に含まれるEXIFデータから到達可能です。これらは、`bytes`、RATIONALタプル、および型付きタプルに関連する部分、そしてBYTE型のカラーマップに関連する部分です。ファイルからアクセス可能なフォワード参照は、`a3d45e9cefd0`(「カスタムのintとfloatのTIFFタグを追加」)で導入され、5.4.0で初めてリリースされました。

## 詳細

`PyImaging_LibTiffEncoderNew`(`src/encode.c:777-1060`)は、可変長引数関数`TIFFSetField()`に渡す引数リストを、各タグの*Python*の値(スカラー、タプル、`bytes`など)と、ソースファイルのIFDから自動的に検出されるかコピーされた型に基づいて選択します。そして、自身の`ImagingLibTiffMergeFieldInfo()`の登録が、libtiffが`va_list`をどのように読み込むかを定義していると想定しています。しかし、実際にはlibtiffは、既知のすべてのタグに対して、独自の定義を保持しており、コアタグは登録されません。したがって、libtiffは、Pillowが実際にプッシュした内容に関わらず、varargsを`(count, pointer)`、`(pointer)`、または3つのポインタとして、*その*フィールドテーブルに従って解釈します。エンコーダは、呼び出しの前に、そのテーブル(`TIFFFindField` / `TIFFFieldPassCount` / `TIFFFieldWriteCount` / `TIFFFieldDataType`)を参照することはありません。

最も深刻なケースは、添付の再現テストで示されているように、TIFFファイルがlibtiffライターを通じて再保存されるときです。このとき、Pillowが認識しないソースIFDのすべてのタグが、`TIFFSetField()`関数を使ってlibtiffに渡されます。カウント-1の整数値が、単一のスカラー引数として渡されます。libtiff自身のフィールドテーブルが、そのタグ番号を配列フィールドとして定義している場合、libtiffは引数を`(count, pointer)`または`(pointer)`として解釈します。その結果、攻撃者の整数値は、書き込まれていない引数スロットからコピーされるべきバイト数、またはソースアドレス自体になります。同様の不整合は、型付きタプル分岐(固定長の組み込みタグである`CFARepeatPatternDim`などについて、タプルの長さがポインタとして消費される)、`bytes`、`TIFF_RATIONAL`タプル、および同じループのコアタグ分岐からも到達可能です。これらの分岐は、以下で*同じ欠陥を持つ他の分岐*としてリストされており、同じ修正で対応する必要があります。

181バイトのTIFFファイル(16×16 RGB、LZWストリップ1つ、および追加のIFDエントリ1つ:`50740 / LONG / count 1 / value 4096` — DNGPrivateData)を`Image.open()`で開き、`im.save(buf, "TIFF")`で保存すると、4296バイトの出力ファイルが生成されます。この出力ファイルのタグ50740には、入力ファイルには存在しない4096バイトのデータが含まれています。`/proc/self/mem`を調べると、これらのデータは`_imaging.so`の読み取り専用データセグメントに存在します。追加のエントリを`50829 / LONG / 1 / 64`(ActiveArea)に変更すると、同じ2つの関数呼び出しで`memcpy(dst, src=0x40, n=16)`がセグメンテーション違反を引き起こします。これは、`ImagingLibTiffSetField` ← `PyImaging_LibTiffEncoderNew`の呼び出し中に発生します。拡張機能のASanビルドでは、最初の入力について、`memcpy`関数から呼び出された箇所で`READ of size 4096`というエラーが報告されています。

### コードパス

`_save`関数は、開かれた画像のすべてのタグをlibtiffの属性リストにコピーします。`LIBTIFF_CORE`にも`TiffTags.TAGS_V2`にも存在しないタグの場合、型が記録されないため、値は型付きで処理されません。

```python
# src/PIL/TiffImagePlugin.py:1782
supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
…
# :1959-1982
for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
if tag not in TiffTags.LIBTIFF_CORE:
…
elif isinstance(value, (int, float, str, bytes)) or …:
type = TiffTags.lookup(tag).type # 50740, 50829 などの場合は None
if type:
types[tag] = type
if tag not in atts and tag not in blocklist:
…
atts[tag] = value # plain int 4096
```

`compression` のデフォルト値は、元の画像の `im.info["compression"]` になります
(`TiffImagePlugin.py:1731-1746`)。そのため、圧縮された元の画像の場合、アプリケーションが明示的に要求しなくても、libtiff の書き込み機能が選択されます。

エンコーダでは、型付けされていない `int` が自動的に `TIFF_LONG` として検出されます
(`src/encode.c:805-815`)。そのタグは「スカラーカスタムフィールド」として登録され、その後、32ビットの可変引数で設定されます。

```c
// src/encode.c:844 (is_var_length = 0 → writecount 1, passcount 0)
if (ImagingLibTiffMergeFieldInfo(&encoder->state, type, key_int, is_var_length)) {
… continue;
}
…
// src/encode.c:1013-1016
} else if (type == TIFF_LONG || type == TIFF_IFD) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)
);
```

この登録は、呼び出し元が想定する動作とは異なります。`TIFFMergeFieldInfo()` は、libtiff がすでに認識していないタグに対してのみ定義を追加し、常に 0 (成功) を返します。そのため、libtiff の組み込みテーブルにあるタグの場合、組み込みの定義が有効なままです。Pillow のマージ呼び出しの直後に、リンクされた libtiff 4.5.0 に対してクエリを行った結果:

```
50740 DNGPrivateData type=BYTE passcount=1 writecount=-1 (変更なし)
50829 ActiveArea type=LONG passcount=0 writecount=4 (変更なし)
65000 CustomField type=LONG passcount=0 writecount=1 (Pillow の定義、タグは不明だった)
```

libtiffの`FIELD_CUSTOM`タグのsetterは、`va_list`を*その定義*に従って解釈します。`passcount=1`の場合、`va_arg(ap, int)`を要素数として読み込みます。攻撃者が指定した4096という値が要素数として扱われ、`count * size`バイトを割り当て、`memcpy`を使って、`va_arg(ap, void *)`からその分のバイト数をコピーします。この`va_arg(ap, void *)`が、`PyImaging_LibTiffEncoderNew`によって書き込まれるべきではない領域を指しています。x86-64アーキテクチャでは、この領域はレジスタ`rcx`に格納されており、このビルドでは`_imaging.so`の`.rodata`セクションへのポインタが格納されています。そのため、コピーは確実に成功します(2000000という値も成功し、2000200バイトの出力が得られます)。`TIFFWriteDirectory`は、コピーされたバイトを、タグの値として出力ファイルに書き込みます。固定長の配列フィールド(`passcount=0`、`writecount=4`、例えばActiveAreaの場合)では、要素数の引数が存在しないため、攻撃者が指定したスカラー値が`void *`のソースとして扱われ、16バイトがアドレス64から読み込まれます。これにより、SIGSEGVが発生します。

libtiff 4.5に組み込まれたテーブルで、Pillowが型情報を付与せずに、`(count, pointer)`の形式で受け取るタグには、DNGPrivateData (50740)、CFAPattern (33422)、CFAPlaneColor (50710)、LinearizationTable (50712)、BlackLevel (50714)、WhiteLevel (50717)、ColorMatrix1/2 (50721/50722)、AsShotNeutral (50728)、OriginalRawFileName/Data (50827/50828)、AsShotICCProfile (50831)、CurrentICCProfile (50833)などがあります。固定長のタグ(ポインタのみ → クラッシュ)には、ActiveArea (50829)、DNGVersion/DNGBackwardVersion (50706/50707)、DefaultScale/DefaultCropOrigin/DefaultCropSize (50718–50720)、AsShotWhiteXY (50729)、CFARepeatPatternDim (33421)、BlackLevelRepeatDim (50713)、ImageLayer (34732)、RawDataUniqueID (50781)などがあります。

アプリケーションが`tiffinfo=im.tag_v2`(メタデータを保持するための推奨される方法)で保存する場合、ファイルで宣言された型が尊重され、`LONG8`型の値が`src/encode.c:1049-1052`の` (uint64_t)PyLong_AsLongLong(value)`という分岐に到達します。ActiveAreaの場合、これは、攻撃者が指定した64ビットのアドレスから16バイトを読み込み、それを出力に書き込む処理です。テストでは、`50829 / LONG8 / 1 / <Pythonのバイトオブジェクトのアドレス>`というソースエントリから、`ActiveArea`の値として`(b'SECR', b'ET-T', b'OKEN', b'-012')`が出力されました。

`TiffImagePlugin.py`の1938行から1944行にある、`blocklist +=`の上のコメントには、すでに「誤った長さの配列タグは、セグメンテーション違反を引き起こす可能性がある」と記載されています。このブロックリストは、Pillow自体が認識しているタグの一部をカバーしているだけで、libtiffが認識しており、Pillowが認識していないタグは含まれていません。これらのタグの転送は、`a3d45e9cefd0`(「カスタムの整数および浮動小数点型のTIFFタグを追加」、2018年10月25日)で導入され、最初に5.4.0でリリースされました。バージョン2.0.0から3.0.0の間も、これらのタグはスカラーとして転送されていましたが、`9fa6634f4`(「#1597に対する部分的な修正」)によって、`_save`が`LIBTIFF_CORE`に限定されました。

### 同じ欠陥を持つ他のブランチ

上記のスカラー整数型のブランチは、同じループ内のいくつかの場所で発生する問題の一つです。これらの問題は、libtiffの定義ではなく、Pythonの値から呼び出しの形状が派生している場合に発生します。以下に示すものはすべて、このコミットで確認されており(標準ビルドでのクラッシュと/またはASanレポート)、スタックトレースは常に`memcpy ← libtiff ← ImagingLibTiffSetField src/libImaging/TiffDecode.c:934 ← PyImaging_LibTiffEncoderNew`となっています。これらはすべて、1KB未満の特別に作成されたファイルに続いて、通常の`save()`関数を呼び出すことで再現できます。

* **固定長フィールドを持つタプル → `(len, av)`**
(`src/encode.c:904-994`、SHORT/LONG/SBYTE/SSHORT/SLONG/FLOAT/DOUBLEの
タプル分岐はすべて`ImagingLibTiffSetField(..., len, av)`を呼び出す)。
libtiffの組み込みフィールドで、`passcount=0`で固定長の`writecount`を持つ
もの(例:`CFARepeatPatternDim` 33421 (SHORT×2)、`BlackLevelRepeatDim` 50713
(SHORT×2)、`ImageLayer` 34732 (LONG×2)、`ActiveArea` 50829 (LONG×4)、
`MatrixWorldToScreen/Camera` 33305/33306 (FLOAT×16))では、libtiffは
*最初の*可変長引数をソースポインタとして扱い、タプルの長さが`memcpy`のソースアドレスとなる。
完璧に正しくフォーマットされた206バイトの8×8 PackBits TIFFファイルで、
`33421 / SHORT / 2 / (2, 2)`という情報を含んでいる場合、
`im.save(out, "TIFF")`を単純に実行すると、`memcpy`でSIGSEGVが発生する(gdb: `rsi = 0x2`、タプルの長さ)。
同様に、`Image.new("L", (8, 8)).save(buf, "TIFF", compression="packbits",
tiffinfo={33421: (2, 2)})`もエラーになる。また、EXIF情報にタグが含まれているJPEGファイルを、
`exif=im.getexif()`で取得したEXIF情報を圧縮TIFFファイルに保存すると、同様のエラーが発生する。
組み込みの固定長BYTEタグ(例:`DNGVersion` 50706, BYTE×4)の場合も、
`src/encode.c:860-865`の`(PyBytes_Size, PyBytes_AsString)`呼び出しによって、
同様のエラーが発生する。
`passcount=1`のフィールドの場合の対応は以下の通り:`Photoshop` 34377は
SHORT型で、カウントは1、値は60000で、`:1009-1012`のスカラ分岐が選択される。
libtiffは60000をカウントとして読み込み、渡されていない可変長引数をポインタとして扱い、
その結果、保存は成功し、60162バイトの出力ファイルが生成される。このファイル内のタグ34377には、
60000バイトのプロセスメモリ(`_imaging.so`の`.rodata`にあるCの文字列)が含まれている。
* **`bytes`の値 → 単一の`char *`** (`src/encode.c:805-815`で`bytes`が`TIFF_ASCII`として自動検出される。
`:1033-1042`では、`PyBytes_AsString(value)`が単独で渡される)。
`ImageFileDirectory_v2._setitem`は、BYTE型の項目をそのまま`bytes`として、
ASCII型の項目を`str`として(`_save`によって`bytes`にエンコードされる)保存するため、
ファイルの宣言された型によってこの分岐が選択される。
- `ColorMap` (320)はBYTE型で、4バイトのデータを持つ。

-パレット
PackBits TIFF: 320は`core_tags[]`に存在し、型は参照されず、値は768要素の`TIFFTAG_COLORMAP`の処理をスキップし(871-895行目)、libtiffは3つの`uint16_t *`を読み込み、それぞれの`1 << BitsPerSample`個のショートをコピーします。これにより、5バイトのPythonバイトバッファを512バイト分読み込みすぎて、さらに2つの無効な引数レジスタを参照するため、`Image.open(f); im.save(out, "TIFF")`の実行ごとにSIGSEGVが発生します。`COLORMAP`は`_save`の`blocklist`には含まれていません。
- `Photoshop` (34377)で宣言されたASCII: `_save`は値*と*型をコピーします(`TiffImagePlugin.py:1802-1818行目;`IPTC_NAA_CHUNK`のみがBYTE/UNDEFINED型のチェックを行っており、これはcommit 2245df0aによるもので、`PHOTOSHOP_CHUNK`と`XMP`にはありません)。libtiffの定義はBYTEで、`passcount=1`なので、`char *`の下位32ビットが`memcpy`のバイト数として使用され、無効なレジスタからデータが読み込まれます(ASan: `size 3518384のREAD`; 通常の環境では約半数の実行でSIGSEGVが発生し、それ以外の場合は`RuntimeError`)。`ICCProfile` (34675)で宣言されたASCIIは、`im.info["icc_profile"]`を介して同じように動作し、`ifd[ICCPROFILE]`(`TiffImagePlugin.py:1822-1824行目)に影響を与えます。
* **`TIFF_RATIONAL`タプル → カウントのない配列** (`src/encode.c:995-1006行目;`TIFF_FLOAT`の処理と比較してください。これは969-981行目で、`len, av`を渡します)。
- プライベートタグ(例:50000)の場合、Pillow自体が`passcount=1`でフィールドを登録します(`TiffDecode.c:904-908行目)。そのため、libtiffは`av`の下位32ビットを要素数として、次の無効な可変引数をソースとして扱います。これにより、ASanで`size 119872のheap-buffer-overflow READ`が発生し、8バイト領域の右側に位置します(`119872 == 4 * low32(av)`)、通常の環境では5回の実行のうち約4回でSIGSEGVが発生します。これは、EXIFにプライベートRATIONAL×2タグが1つ含まれており、`im.save(out, "TIFF", compression="tiff_lzw", exif=im.info["exif"])`で保存されたJPEG、または`tiffinfo=im.tag_v2`で再保存されたTIFFによって発生します(`_setitem`は`IFDRational`のタプルをRATIONALとして自動的に型付けします。`TiffImagePlugin.py:689-701行目;`types[tag] = ifd.tagtype[tag]` at `:1968`)。
- libtiffの組み込み機能によるもの。

xed-count float タグ `WhitePoint` (318, 2)、
`PrimaryChromaticities` (319, 6) および `YCbCrCoefficients` (529, 3) は、
`_save` が、どのソース TIFF ファイルからも受け取り、`TiffTags.lookup` を介して RATIONAL 型として扱います。ポインタのみを使用する方式は偶然一致しますが、
libtiff は、ファイルで宣言された長さの配列から *固定* のカウント値をコピーします。PackBits 形式の TIFF ファイルで `PrimaryChromaticities` のカウントが 1 の場合、および `im.save(out, "TIFF")` を直接実行すると、`READ of size 24 … right of 4-byte region` というエラーが発生し、過剰に読み込まれた浮動小数点数が出力ファイルのタグ 319 に書き込まれます。
`REFERENCEBLACKWHITE`/`TRANSFERFUNCTION` は、
`TiffImagePlugin.py:1941-1952` で意図的にブロックされています。318/319/529 はブロックされていません。
* **ファイルから提供された古い型によって、スカラー値としてコアタグが到達される。** `im.save(out, "TIFF", tiffinfo=im.tag_v2)` という再保存の仕組みでは、`_save` は `ifd.tagtype[key] = info.tagtype[key]` をすべてのキーに対してコピーします ( `TiffImagePlugin.py:1768-1776`)。その後、同じタグの一部に対して独自の値を割り当てますが、型はリセットされません (`ifd[COLORMAP] = colormap` at `:1870-1877`。同様に、BitsPerSample, SamplesPerPixel, ExtraSamples, ICCProfile, …)。418 バイトの PackBits *パレット* TIFF ファイルで、ColorMap エントリが BYTE として宣言されている場合、`_setitem` (`:751-770`) は、`_save` が持つ 768 エントリのパレットを単一の `int` に切り捨てます (「エントリが多すぎます」という警告のみが表示されます)。エンコーダは `(320, 0)` を受け取り、`TIFF_LONG` を自動的に検出し、
`src/encode.c:1013-1016` が `TIFFSetField(TIFFTAG_COLORMAP, (UINT32)0)` を呼び出します。
libtiff は、3 つの `uint16_t *` を読み込み、実行ごとにアドレス ≤ 65280 でエラーが発生します。古いタグ型のコピーは、Python 側の衛生上の問題ですが、そのメモリ安全性の問題は、エンコーダが非タプルの値を `TIFFSetField(COLORMAP)` に渡すことによってのみ発生します。

## 影響

TIFFファイルを読み込み、libtiffを使って同じTIFF形式で書き出すサービスでは、
例えば、`im.save(fp, "TIFF")`でアップロードされたファイルが圧縮される場合、または`compression=`にraw以外のものが指定された場合など、
生成されたファイル内のメタデータタグの一つに、攻撃者が指定した数のバイトが、ワーカープロセス内の古くなったポインタから読み込まれます。
このメモリ領域は、`PyImaging_LibTiffEncoderNew`のコンパイラのレジスタ割り当てによって決まります。
このclangのビルドでは、ポインタは`_imaging.so + 0x1588`です。
そのため、262,144バイトの要求(テストでは常に成功し、2,000,000バイトも成功しました)は、
拡張機能の`.rodata`、`.data.rel.ro`/GOT、および`.data`をカバーし、
libpython、libc、libtiff、libjpegなどのライブラリ内の2,800以上の解決済みポインタが返されます。
これは、他のメモリバグと連携するための完全なASLR回避策となります。
同じソースコードのgccビルドでは、別の、関連性のないアドレスがその場所に残っていました。
固定長のバリアントでは、181バイトのアップロードごとに、ワーカープロセスがSIGSEGVで終了します。
`tiffinfo=im.tag_v2`を使用すると、ソースアドレスは完全に攻撃者が指定できます(ActiveAreaへの16バイトの任意の読み込み、またはクラッシュ)。
デフォルト以外のPillowの設定は一切関係ありません。唯一の前提条件は、TIFFからTIFFへの再エンコードです。

他の分岐では、さらに決定論的なプロセス終了を引き起こす入力(RGBまたはパレット形式のTIFFにおけるBYTE型のColorMapなど)が追加され、
JPEG/PNG/WebPのアップロードも対象となります。これらのファイルでは、変換時にEXIF情報が`exif=`で保持されます。
すべてのメモリエラーは読み込み操作です。libtiffは、コピーするバイト数と同じサイズでバッファを割り当てるため、範囲外への書き込みは見つかりませんでした。

## 再現方法

`repro`は、インストールされている`PIL`を使用した標準ビルドに対して実行されるPythonスクリプトです。
コンパイラや追加ファイルは必要ありません。

`upload(tag, value)` 関数は、Pillow が 16x16 の赤色の RGB 画像を LZW 圧縮された TIFF 形式で書き出し、その後、同じエントリに加えて、`<tag> / LONG / count 1 / value` という追加のエントリ(タグの順序で保持)を持つ新しい IFD を追加し、ヘッダーをそれに向ける処理を行います。この処理により、356 バイトのデータがこのツリーに書き込まれます。このデータは、意図的に LZW 圧縮されています。`im.save(out, "TIFF")` だけを実行すると、元のデータの圧縮形式が維持されます。そのため、アプリケーションが引数を渡さない場合でも、libtiff の書き込み処理が実行されます。`resave()` 関数は、この「アプリケーション」全体を表しており、`Image.open(BytesIO(src)); im.save(out, "TIFF")` という処理を行っています。

このスクリプトは、以下の 3 つのケースを実行します。

1. **コントロール: tag 65000 = 4096.** libtiff はこのタグを認識しないため、ファイルが正しく再保存され、出力されるタグの値は整数 `4096` となります。これは、すべてのタグに対して期待される動作です。
2. **情報漏洩: DNGPrivateData 50740 = 65536.** libtiff に組み込まれているこのタグの定義は、要素数が指定された BYTE 配列です。Pillow が提供する単一のスカラー値を *要素数* として解釈し、Pillow が書き込んだことのない可変引数領域からデータポインタを取得します。その結果、出力されるタグの値は、入力データには存在しない 65536 バイトのデータとなり、その先頭部分である `a661fcffc661fcff3360fcff7b61fcffd361fcff085ffcffe061fcff3360fcff` が出力されます。スクリプトは、これらのバイトのうち最初の 32 バイトと、それらがディスク上の `_imaging.so` ファイルのどのオフセットに存在するかを表示します。このビルドでは、常にオフセット `0x57588` に存在します。これは、古いポインタが拡張機能の読み取り専用データ領域を指していることを意味します。
3. **不正な読み込み: ActiveArea 50829 = 64.** libtiff は、このタグを要素数が指定されていない固定長の LONG[4] 配列として定義しています。そのため、スカラー値 64 を *ソースアドレス* として使用します。この再保存処理を行うために、スクリプトは子プロセスとして自身を再実行します (`repro child`、コアダンプは無効)。子プロセスが終了すると、`-11` (SIGSEGV) という終了コードが返されます。

影響を受けるツリーでの期待される出力は以下の通りです。

```
control tag 65000=4096 -> 4096
leak tag 50740=65536 from a 356-byte upload -> 65536 bytes not present in input, head a661fcffc661fcff3360fcff7b61fcffd361fcff085ffcffe061fcff3360fcff
leaked bytes found in _imaging.so at file offset 0x57588
crash tag 50829=64 -> child exit -11 (SIGSEGV)
BUG
```

このスクリプトは、ケース2でメモリリークが発生した場合、またはケース3でクラッシュが発生した場合に、`BUG`というメッセージを出力します。どちらも、このバージョンのソフトウェアでは常に発生します。正常なPillowライブラリを使用すると、`OK`というメッセージが出力されます。この場合、タグが65536バイトの不正なデータとして返されることはありません(`None`として返されるか、`save()`関数が`ValueError`または`RuntimeError`を発生させ、スクリプトがそれを捕捉します)。

他の分岐についても、同じように、標準のビルドで手動で確認できます(各分岐を、子プロセス内で`Image.open(f); im.save(BytesIO(), "TIFF")`というコマンドで実行します)。
- PackBits RGB TIFFで、追加のIFDエントリが`320 / BYTE / 4 / "ABCD"`の場合 → 毎回SIGSEGVが発生します。
- PackBitsパレットTIFFで、ColorMapが`320 / BYTE / 2`の場合、`tiffinfo=im.tag_v2`で保存すると → 毎回SIGSEGVが発生します。
- Pillowによって作成されたJPEGで、EXIF情報にタグ50000 = RATIONAL×2が含まれており、`compression="tiff_lzw", exif=im.info["exif"]`で保存すると → ほとんどの場合SIGSEGVが発生し、残りの場合は`RuntimeError`(「カスタムタグのバイナリオブジェクトのメモリ割り当てに失敗しました(各要素が4バイトで構成され、-N個の要素があります)」)が発生します。

## 提案される修正

エンコーダは、呼び出し規約を推測してはなりません。代わりに、`ImagingLibTiffMergeFieldInfo`の*後*に、libtiffライブラリにタグの有効な定義を問い合わせ、それに応じて処理するか、または処理を拒否する必要があります。添付のパッチは、コアではないタグについて、この処理を行います。

* `src/libImaging/TiffDecode.c` に、新しい関数 `ImagingLibTiffFindField()` が追加されました ( `TiffDecode.h` に宣言されており、他の libtiff ラッパー関数の隣にあります)。この関数は、libtiff の `TIFFFindField` 型、`passcount`、および `writecount` を、タグごとに返します。
* `src/encode.c` の `PyImaging_LibTiffEncoderNew` 関数において、libtiff が既に認識しているコアではないタグの場合、Pillow は独自の型を持たない場合に libtiff の型を採用し、`ImagingLibTiffMergeFieldInfo` の後、新しい関数 `tiff_field_matches()` が、値を libtiff の定義と比較します。スカラー値の場合、C 言語の引数型 (int / `uint64_t` / double / `char *`) が一致している必要があります。配列の場合、libtiff の要素サイズと一致し、固定長フィールドの場合は、正確に `writecount` 個の要素が必要です。それ以外の場合は、`TIFFSetField` に到達する代わりに、`ValueError("Tag value does not match libtiff field definition")` が発生します。これは、再現テストの DNGPrivateData および ActiveArea の入力、ASCII 形式の Photoshop、short 型の PrimaryChromaticities、および `LONG8` 型の `tiffinfo=` のバリアントをカバーします。
* すべてのタプル/`bytes` の処理は、`set_tiff_array_field()` 関数を経由するようになり、この関数は、フィールドに `passcount` が設定されている場合にのみ `len` を渡します。これにより、`TIFF_RATIONAL` の処理が修正され (これまでカウントが渡されていなかった)、`TIFF_SRATIONAL` にも拡張されます。
* `TIFF_UNDEFINED` フィールドは、可変長として登録され、`bytes` 値が渡される方法と一致します。
* `Tests/test_file_libtiff.py` に、リグレッションテストが追加されました (スカラー型の DNGPrivateData は許可されず、スカラー型および 2 タプルの ActiveArea は拒否されます。正しくフォーマットされた値は、往復しても変化しません)。

コアタグおよびlibtiffが認識しないタグは、現在の動作を維持します。
この修正をクリーンな状態から適用し、再ビルドすると、再現ケースが動作しなくなり、テストスイートが合格します。
ただし、これは完全な修正ではありません。なぜなら、このチェック全体が`if (!is_core_tag)`の中に存在するため、BYTE型のColorMapバリアントが引き続き開かれたままになるからです。
`320 / BYTE / 4`を含むPackBits RGB TIFFファイルを、単純な`save()`関数で再保存すると、この時点でセグメンテーション違反は発生しなくなりますが、5バイトの`bytes`オブジェクトと、2つの無効な可変引数(合計22個の有効なヒープポインタを含む)から、1536バイトが読み取られ、出力ColorMapに書き込まれます。これは依然として情報漏洩の問題です。
マージする前に、`TIFFTAG_COLORMAP`が既存の768要素の分岐を経由するのみで、スカラーまたは`bytes`分岐を経由しないようにする保護機構が必要です。また、理想的には、libtiffが`_TIFFVSetField`で特別扱いする他のコアタグ(YCbCrSubsampling 530、TransferFunction 301、ReferenceBlackWhite 532)についても、同様の形状チェックを行う必要があります。

Python側の対策として、`_save`関数内で個別のエントリーポイントを閉じることはできますが、クラス全体は閉じません。C側のチェックと合わせて追加する価値があります。具体的には、`supplied_tags`から`TiffTags.lookup(tag).type is None`であるタグを削除します(これにより、ここで示されている`Image.open()→save()`のパスは閉じられますが、`tiffinfo=`呼び出しは影響を受けません)。次に、`supplied_tags`から`COLORMAP`を`EXIFIFD`/`SAMPLEFORMAT`の隣に削除し、`IPTC_NAA_CHUNK`のBYTE/UNDEFINED型の保護を`PHOTOSHOP_CHUNK`および`XMP`に拡張します。また、318/319/529を`REFERENCEBLACKWHITE`の隣のブロックリストに追加します。さらに、`_save`関数内で、`ifd.tagtype.pop(tag, None)`を、独自のColorMap/BitsPerSample/ICCProfile/…の値を割り当てる前に実行することで、`tiffinfo=`でインポートされた型の値を変更できないようにします。C側のチェックが、信頼できる修正方法です。

## 修正内容

TIFFタグの値を、libtiff自身のフィールド定義に従ってlibtiffに渡します。また、値が定義に適合しない場合は、ValueErrorを発生させます。

これは、上記のコミットにおける`src/encode.c`に適用されます。機械的なチェックは合格し(クリーンな状態に適用し、再ビルドすると、再現ケースが動作しなくなる)、しかし、レビュー担当者はこれが正しい修正であるとは認めませんでした。

```diff
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 3d804ea4a..adc4633e4 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -395,6 +395,43 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(ValueError, match="Incorrect tag value type"):
im.save(out, tiffinfo=ifd)

+ @pytest.mark.parametrize(
+ "tag, value",
+ (
+ # DNGPrivateData は libtiff において、カウント付きの BYTE 配列です
+ (50740, 4096),
+ # ActiveArea は libtiff において、4つの値を持つ LONG 配列です
+ (50829, 64),
+ (50829, (0, 0)),
+ ),
+ )
+ def test_libtiff_field_definition(
+ self, tag: int, value: Any, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+ ) -> None:
+ monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
+
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[tag] = value
+ ifd.tagtype[tag] = TiffTags.LONG
+
+ out = tmp_path / "temp.tif"
+ im = Image.new("L", (1, 1))
+ with pytest.raises(ValueError, match="libtiff field definition"):
+ im.save(out, tiffinfo=ifd)
+
+ # libtiff の定義に一致する値は書き込まれます
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[50740] = b"private"
+ ifd.tagtype[50740] = TiffTags.BYTE
+ ifd[50829] = (0, 0, 1, 1)
+ ifd.tagtype[50829] = TiffTags.LONG
+ im.save(out, tiffinfo=ifd)
+
+ with Image.open(out) as reloaded:
+ assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
+ assert reloaded.tag_v2[50740] == b"private"
+ assert reloaded.tag_v2[50829] == (0, 0, 1, 1)
+
def test_inknames_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
diff --git a/src/encode.c b/src/encode.c
index 9e19e9002..cbbd09f0d 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -675,6 +675,106 @@ PyImaging_ZipEncoderNew
```

(PyObject *self, PyObject *args) {

#include <string.h>

+/* TIFFSetField関数が、指定された型の単一の値に対して、引数から読み取るCの型: int, uint64_t, double, または char * */
+static int
+tiff_scalar_arg_type(TIFFDataType type) {
+ switch (type) {
+ case TIFF_BYTE:
+ case TIFF_SBYTE:
+ case TIFF_UNDEFINED:
+ case TIFF_SHORT:
+ case TIFF_SSHORT:
+ case TIFF_LONG:
+ case TIFF_SLONG:
+ case TIFF_IFD:
+ return 1;
+ case TIFF_LONG8:
+ case TIFF_SLONG8:
+ case TIFF_IFD8:
+ return 2;
+ case TIFF_FLOAT:
+ case TIFF_DOUBLE:
+ case TIFF_RATIONAL:
+ case TIFF_SRATIONAL:
+ return 3;
+ case TIFF_ASCII:
+ return 4;
+ default:
+ return 0;
+ }
+}

+/* TIFFSetField関数が、指定された型の配列からコピーする値のサイズ。libtiffは、カスタムRATIONAL配列をfloatとして保存します。 */
+static int
+tiff_array_item_size(TIFFDataType type) {
+ switch (type) {
+ case TIFF_BYTE:
+ case TIFF_SBYTE:
+ case TIFF_UNDEFINED:
+ case TIFF_ASCII:
+ return 1;
+ case TIFF_SHORT:
+ case TIFF_SSHORT:
+ return 2;
+ case TIFF_LONG:
+ case TIFF_SLONG:
+ case TIFF_IFD:
+ case TIFF_FLOAT:
+ case TIFF_RATIONAL:
+ case TIFF_SRATIONAL:
+ return 4;
+ case TIFF_DOUBLE:
+ case TIFF_LONG8:
+ case TIFF_SLONG8:
+ case TIFF_IFD8:
+ return 8;
+ default:
+ return 0;
+ }
+}

+/* TIFFSetField関数に単一の値として渡される値、またはlen個の値の配列が、libtiffが使用するフィールドの定義と一致するかどうかを確認します。 */
+static int
+tiff_field_matches(
+ TIFFDataType field_type,
+ int passcount,
+ int writecount,
+ TIFFDataType type,
+ int is_var_length,
+ Py_ssize_t len
+) {
+ i

f (field_type == TIFF_ASCII && !passcount) {
+ // TIFFSetField はヌル終端文字列を読み込みます
+ return !is_var_length && type == TIFF_ASCII;
+ }
+ if (!is_var_length) {
+ // TIFFSetField は単一の値を読み込みます
+ return !passcount && writecount == 1 && tiff_scalar_arg_type(type) &&
+ tiff_scalar_arg_type(type) == tiff_scalar_arg_type(field_type);
+ }
+ // TIFFSetField は値へのポインタを読み込みます。その前に、passcount が設定されている場合は、値の数を記述します。そうでない場合は、値の数が固定されています。
+ if (!tiff_array_item_size(type) ||
+ tiff_array_item_size(type) != tiff_array_item_size(field_type)) {
+ return 0;
+ }
+ return passcount || (writecount > 1 && len == writecount);
+}
+
+static int
+set_tiff_array_field(
+ ImagingCodecState state, int key, int passcount, Py_ssize_t len, void *av
+) {
+ if (passcount) {
+ return ImagingLibTiffSetField(state, (ttag_t)key, len, av);
+ }
+ return ImagingLibTiffSetField(state, (ttag_t)key, av);
+}
+
PyObject *
PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
ImagingEncoderObject *encoder;
@@ -689,7 +789,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
PyObject *key, *value;
Py_ssize_t pos = 0;
int key_int, status, is_core_tag, is_var_length, num_core_tags, i;
- TIFFDataType type = TIFF_NOTYPE;
+ int is_known_tag, passcount, writecount;
+ TIFFDataType type = TIFF_NOTYPE, field_type;
// This list also exists in TiffTags.py
const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, 277,
278, 280, 281, 282, 283, 284, 286, 287, 296, 297,
@@ -777,6 +878,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
status = 0;
is_core_tag = 0;
is_var_length = 0;
+ is_known_tag = 0;
+ passcount = 1;
type = TIFF_NOTYPE;

for (i = 0; i < num_core_tag

s; i++) {
@@ -800,6 +903,15 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
}
Py_DECREF(tag_type);
}
+
+ // libtiffは、既知のタグに対して、登録されたフィールドではなく、独自の定義を使用します。
+ is_known_tag = ImagingLibTiffFindField(
+ &encoder->state, key_int, &field_type, &passcount, &writecount
+ );
+ if (is_known_tag && type == TIFF_NOTYPE) {
+ type = field_type;
+ }
}

if (type == TIFF_NOTYPE) {
@@ -838,7 +950,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {

if (!is_core_tag) {
// コアタグ以外のフィールドを登録します。
- if (type == TIFF_BYTE) {
+ if (type == TIFF_BYTE || type == TIFF_UNDEFINED) {
is_var_length = 1;
}
if (ImagingLibTiffMergeFieldInfo(
&encoder->state, key_int, type, is_var_length, value, passcount, writecount
@@ -847,6 +959,28 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
Py_DECREF(item);
continue;
}
+
+ if (is_known_tag) {
+ // 値が、libtiffがタグの定義として期待する形式でTIFFSetFieldに渡されることを確認します。
+ Py_ssize_t len = 0;
+ if (PyTuple_Check(value)) {
+ len = PyTuple_Size(value);
+ } else if (PyBytes_Check(value)) {
+ len = PyBytes_Size(value);
+ }
+ if (!tiff_field_matches(
+ field_type, passcount, writecount, type, is_var_length, len
+ )) {
+ Py_DECREF(encoder);
+ Py_DECREF(item);
+ PyErr_SetString(
+ PyExc_ValueError,
+ "タグの値がlibtiffのフィールド定義と一致しません"
+ );
+ r

eturn NULL;
+ }
+ }
}

if (type == TIFF_BYTE || type == TIFF_UNDEFINED ||
@@ -857,9 +991,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
PyErr_SetString(PyExc_ValueError, "Incorrect tag value type");
return NULL;
}
- status = ImagingLibTiffSetField(
+ status = set_tiff_array_field(
&encoder->state,
- (ttag_t)key_int,
+ key_int,
+ passcount,
PyBytes_Size(value),
PyBytes_AsString(value)
);
@@ -909,8 +1044,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i));
}
- status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, len, av
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
);
free(av);
}
@@ -922,8 +1057,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i));
}
- status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, len, av
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
);
free(av);
}
@@ -935,8 +1070,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i));
}
-

status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, len, av
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
);
free(av);
}
@@ -948,8 +1083,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i));
}
- status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, len, av
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
);
free(av);
}
@@ -961,8 +1096,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i));
}
- status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, len, av
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
);
free(av);
}
@@ -974,8 +1109,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i));
}
- status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, len, av
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
);
free(av);
}
@@ -987,12 +1122,12 @@ PyImaging_Lib

TiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i));
}
- status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, len, av
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
);
free(av);
}
- } else if (type == TIFF_RATIONAL) {
+ } else if (type == TIFF_RATIONAL || type == TIFF_SRATIONAL) {
FLOAT32 *av;
/* malloc check ok, calloc checks for overflow */
av = calloc(len, sizeof(FLOAT32));
@@ -1000,8 +1135,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (i = 0; i < len; i++) {
av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i));
}
- status =
- ImagingLibTiffSetField(&encoder->state, (ttag_t)key_int, av);
+ status = set_tiff_array_field(
+ &encoder->state, key_int, passcount, len, av
+ );
free(av);
}
}
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 8a57c523d..e14dd50bb 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -923,6 +923,29 @@ ImagingLibTiffMergeFieldInfo(
return TIFFMergeFieldInfo(clientstate->tiff, info, n);
}

+int
+ImagingLibTiffFindField(
+ ImagingCodecState state,
+ int key,
+ TIFFDataType *field_type,
+ int *passcount,
+ int *writecount
+) {
+ // Look up libtiff's own definition of a tag. TIFFMergeFieldInfo does not
+ // replace the definition of a tag that libtiff already knows, so this is
+ // what TIFFSetField will use for s

uch tags.
+ TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
+ const TIFFField *fip = TIFFFindField(clientstate->tiff, (ttag_t)key, TIFF_ANY);
+
+ if (!fip) {
+ return 0;
+ }
+ *field_type = TIFFFieldDataType(fip);
+ *passcount = TIFFFieldPassCount(fip);
+ *writecount = TIFFFieldWriteCount(fip);
+ return 1;
+}
+
int
ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) {
// after tif_dir.c->TIFFSetField.
diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h
index c31d0b9d8..ee1e4a5f9 100644
--- a/src/libImaging/TiffDecode.h
+++ b/src/libImaging/TiffDecode.h
@@ -41,6 +41,14 @@ ImagingLibTiffMergeFieldInfo(
ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length
);
extern int
+ImagingLibTiffFindField(
+ ImagingCodecState state,
+ int key,
+ TIFFDataType *field_type,
+ int *passcount,
+ int *writecount
+);
+extern int
ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...);

/*

def upload(tag, value):
"""Pillowで作成された16x16のLZW TIFFファイル。さらに、追加のIFDエントリ <tag> LONG[1] = value を追加します。"""
b = io.BytesIO()
Image.new("RGB", (16, 16), "red").save(b, "TIFF", compression="tiff_lzw")
data = bytearray(b.getvalue()); assert data[:4] == b"II*\0"
(off,) = struct.unpack_from("<I", data, 4)
(n,) = struct.unpack_from("<H", data, off)
ents = [bytes(data[off + 2 + 12 * i: off + 14 + 12 * i]) for i in range(n)]
ents.append(struct.pack("<HHII", tag, 4, 1, value))
ents.sort(key=lambda e: struct.unpack_from("<H", e)[0])
data += b"\0" * (len(data) & 1); new_off = len(data)
data += struct.pack("<H", len(ents)) + b"".join(ents) + struct.pack("<I", 0)
struct.pack_into("<I", data, 4, new_off)
return bytes(data)

def resave(src): # 全体的な "サービス"
im = Image.open(io.BytesIO(src)); out = io.BytesIO(); im.save(out, "TIFF")
return out.getvalue()

if len(sys.argv) > 1 and sys.argv[1] == "child":
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
resave(upload(50829, 64)); sys.exit(0)

print("PIL core:", Image.core.__file__)
ctl = Image.open(io.BytesIO(resave(upload(65000, 4096)))).tag_v2.get(65000)
print("control tag 65000=4096 ->", repr(ctl))

N = 65536
src = upload(50740, N)
try:
v = Image.open(io.BytesIO(resave(src))).tag_v2.get(50740)
except (ValueError, RuntimeError) as e: # 特定のバージョンのPillowでは、このタグが拒否される可能性があります
v = e
leak = isinstance(v, bytes) and len(v) == N and v[:64] not in src
print(f"leak tag 50740={N} from a {len(src)}-byte upload ->",
f"{len(v)} bytes not present in input, head {v[:32].hex()}" if leak else repr(v)[:80])
if leak:
so = open(Image.core.__file__, "rb").read()
print(" leaked bytes found in _imaging.so at file offset", hex(so.find(v[:256])))

```
rc = subprocess.call([sys.executable, __file__, "child"])
crash = rc in (-11, 139)
print("crash tag 50829=64 -> child exit", rc, "(SIGSEGV)" if crash else "")
print("BUG" if leak or crash else "OK")
```

</details>

---

影響を受けるパッケージ:
- pip (Python) Pillow: 影響を受けるバージョンは 12.3.0 以下、13.0.0 で修正済み。