/Indexedルックアップテーブルは、RGBトリプレットではなくベースカラースペースのコンポーネントを格納します。DeviceGrayベースはエントリごとに1バイト、DeviceCMYKは4バイト、Labはレンジスケーリングが必要な3バイト、ICCBasedはプロファイルの/Nが宣言する数です。HotPDFはこれらすべてをパース時に正規化し、THPDFColorSpaceが公開するパブリックなRGBパレットにします
その一文こそ、バグのクラス全体です。ほとんどすべてのパレットレンダラは3バイトストライドを仮定して誕生します。実世界の/Indexed空間の圧倒的多数が/DeviceRGBの上にあり、その仮定が何年も成立するからです。そしてある日、スキャンした請求書が[/Indexed /DeviceCMYK 15 <...>]付きで届きます。読み手はルックアップ文字列を3バイトずつ歩き、0より後のすべてのインデックスが他人の色の真ん中に落ちます。出力は真っ白でもクラッシュでもありません。もっともらしく見えるゴミです。それははるかに悪い。色が少しおかしいだけでは、誰もバグを報告しないからです
Indexedパレットがなぜ間違って出てくるのか
ストライドが間違うのは、ストライドが定数ではないからです。ISO 32000-1 §8.6.6.3はルックアップテーブルをm × (hival + 1)バイトと定義します。mはベース空間のコンポーネント数で、ベースがRGBであるとはどこにも書いていません。同じ節が全面的に禁じるベースは2つだけです。/Indexed自身と/Patternです。他はすべて対象で、§8.6.5.2から§8.6.5.5までの4つのCIEベースファミリも含みます
だから実際の作業はバイトを読むことではなく、まずベースを解決して、エントリがどれだけ広いかを尋ねることです。HotPDFのResolveIndexedPaletteはまさにそれをします。ベースをTHPDFColorSpaceに解決し、解決済みレコードからComponentCountを取り、ベースがcsfIndexed、csfPattern、csfUnsupportedで返ってきたら配列を即座に拒否します。その後に初めてバイト予算をPaletteCount * Countとして計算します。これはhival 255の法定最大値と4コンポーネントベースで1024バイトが上限です。そのサイズ計算の2つの細部が、悪意のあるファイルが探ってくる場所です。HotPDFは§8.6.6.3の制限に従い、使う前にhivalを0..255にクランプするため、hival 100000を主張するルックアップ配列がギガバイト級の割り当てになりません。予算はルックアップデータが実際に含む量に切り詰められ、バイトが末尾を越える最初のエントリでパレットの充填を止めます。短いテーブルは短いパレットになり、バッファを越えた読み取りにはなりません
4つの異なるベース空間を1つのRGBテーブルに正規化する
HotPDFはパレットを描画時ではなくパース中にRGBへ変換し、その決定が設計の残りを駆動します。パブリックなTHPDFColorSpace.Paletteフィールドは長さ3 * PaletteCountのRGBトリプレットのTBytesで、CIEベースがサポートされるずっと前からその形です。レコードレイアウトを書き換えてネイティブなベースコンポーネントを持たせると、そのフィールドのすべての消費者を壊していたでしょう。パース時の変換はレイアウトを無傷に保ち、おまけにGray、CMYK、Lab、CalGray、CalRGB、ICCBasedのエントリを、レンダラが直接のカラーオペレータで既に使っているのと同じHPDFResolveColor経路に通します
スケーリングのステップこそ、ベース空間が互換性を失う場所です。ルックアップバイトは常に0..255ですが、そのバイトが意味するものは、ベースが宣言するコンポーネントレンジに依存します。Device系と校正系のコンポーネントは0..1に対応します。Labは違います。L*コンポーネントは0..100を走り、a*とb*は空間が宣言する/Rangeを走ります。HotPDFはそれらの境界をTHPDFCIEParamsのAMin、AMax、BMin、BMaxフィールドから取得します。Labパーサは辞書の/Rangeエントリから埋め、エントリがなければデフォルトで−108..101になるため、記憶から対称レンジをハードコードしないでください。ICCBasedベースは3つ目の真実の源を追加します。コンポーネント数はプロファイルストリームの/Nから来てICCNとして表出され、ICCBasedストリーム自身が/Range配列を持つこともあり、HotPDFはそれをHPDFReadNumericArrayUpToで同じコンポーネント別境界テーブルに読み込みます。したがって3コンポーネントのICCパレットと3コンポーネントのLabパレットは、同じストライドでルックアップ文字列を歩きながら完全に異なる算術をします。固定RGBストライドでは絶対に生み出せない結果です
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObjはimage XObjectまたはリソース辞書の
// /ColorSpaceエントリ。名前、配列、または間接参照の可能性がある
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// ベース空間が何であれ、PaletteはすでにRGB
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // 短いルックアップテーブルなのできれいに停止
R := CS.Palette[Entry * 3];
G := CS.Palette[Entry * 3 + 1];
B := CS.Palette[Entry * 3 + 2];
UseColor(Entry, R, G, B);
end;
end;
ルックアップテーブルがストリームにあるとき
ISO 32000-1 §8.6.6.3はルックアップテーブルに文字列かストリームを許し、噛みつくのはストリーム形式の方です。ストリームは/FlateDecodeを持つことができ、ファイルの無関係な部分と共有される間接オブジェクトになり得て、パーサが到達した時点でTStreamはどこにでも位置づけられています。HotPDFは圧縮ケースを、HPDFSetColorStreamReaderで登録されたデコード済みバイトコールバックに委任して処理します。これはType 0サンプルド関数エンジンが使うのと同じコールバックです
リーダが登録されていないとき、フォールバックはベストエフォートではなく意図的に狭くします。HotPDFはストリーム辞書の/Filterを検査し、あればパレット全体を拒否します。圧縮バイトをルックアップに流し込んで自信満々の間違ったテーブルを作る代わりです。フィルタがなければ、hivalとベースコンポーネント数から計算したバイト予算で上限を切って生のバイトを読みます。ストリーム分岐全体はtry..finallyの中にあり、拒否を含むすべての終了経路で元のStream.Positionを復元します。オブジェクトが共有されており、次の消費者は動かされたカーソルに感謝しないからです。回帰スイートのLookupStreamUsesDecodedReaderAndRestoresPositionはその挙動の両半分を固定しています
// カラースペースを解決する前に両方のコールバックを登録する。さもないと
// 間接ベースと圧縮ルックアップストリームは単に拒否される
HPDFSetColorSpaceResolver(MyResolveIndirectObject);
HPDFSetColorStreamReader(MyDecodedStreamReader);
try
CS := HPDFResolveColorSpace(ColorSpaceObj);
if (CS.Family = csfIndexed) and (Length(CS.Palette) = 0) then
LogWarning('lookup table unreadable: filtered stream or no reader');
finally
HPDFSetColorStreamReader(nil);
HPDFSetColorSpaceResolver(nil);
end;
Indexedカラースペースが永久に再帰しないようにするものは何か
/Indexedを直接ベースとして拒否することは必要ですが、それだけでは到底足りません。面白い攻撃は横から来ます。正当な[/Indexed /Separation ...]を取り、§8.6.6.4がSeparationに代替空間の命名を許し、§8.6.6.5がDeviceNにも同じことを許すのを思い出してください。その代替を、元のIndexed配列を指し戻す間接参照にします。チェーンのどの単一ホップも直接ベース規則を破りません。ベースはSeparationで、Separationの代替は配列で、その配列はたまたま出発点だったものです。素朴に解決すると、パーサはスタックが尽きるまでループを歩きます。これらの空間が頼るティントトランスフォーム機構はそれ自体がテーマで、SeparationとDeviceNスポットカラーのレンダリングに関する記事でカバーされています。ここではサイクルのリンクとしてだけ重要です
HotPDFは、すべての経路がすでに通る単一のエントリポイントでこれを塞ぎます。HPDFResolveColorSpaceは内部リゾルバを、HPDFColorSpaceMaxDepth(64)で制限される深さカウンタで包みます。その境界を越えるとガードがフラグを立て、そのフラグこそが重要です。アンワインド時にフラグを見たすべてのネストレベルは、自分が部分的に解決したレコードを捨ててcsfUnsupportedを返します。だからグラフ全体が拒否され、半分が残ることはありません。最も外側の呼び出しが戻ると両方のカウンタはリセットされます。それらのカウンタはユニットグローバルではなくthreadvarとして宣言されており、それは偶然の整頓ではありません。GResolveObjとGReadStreamも同じ理由でスレッドローカルストレージにあります。ページが並列でレンダリングされるからです。共有グローバルだと、ネストしたDeviceNチェーンの奥にいるスレッドが、ごく普通の[/Indexed /DeviceRGB 255 <...>]をパースしている別のスレッドのために上限を超えさせ、非決定的で負荷依存の失敗を生みます。それは最もデバッグしにくい種類のものです。64は正当なコンテンツにとって余裕のあるヘッドルームで、4〜5レベルを超えてネストすることは滅多にありません。構造的に正しく見える配列からのcsfUnsupportedはログに記録する価値があります。実際には、サイクルか、本物のプロデューサが生成しない深さを意味するからです。SeparationとDeviceNでパラメータ化されたIndirectAlternateCycleIsUnsupportedとExcessivePatternDepthIsUnsupportedが両方のケースをカバーします
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// ここでのcsfUnsupportedは、真に未知のファミリ、間接代替を通る
// サイクル、そして64レベルを超える深さをカバーする
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray。PDFの初期空間
end;
呼び出し側のために文書化する価値のあるAPI境界が1つ
HotPDFはIndexedパレットを構築するときに、パース時点で有効なICC変換設定を適用し、後からHPDFSetColorSpaceRenderingIntentを呼んでも、すでに解決済みのパレットは再生成されません。その関数は渡したレコードのICCBased変換と代替変換を歩いてパイプラインを再構築します。Paletteのフラット化されたRGBバイトはパース時の副産物で、そのまま残ります。これは早期正規化の結果であって見落としではなく、顧客に発見させるより自分のAPIドキュメントに書き込む価値があります。アプリケーションがセッション途中のレンダリングインテント切り替えを許すなら、切り替え後にカラースペースを再解決してください。パイプライン設定時に一度だけインテントを設定するのが圧倒的に一般的なケースで、その場合は何もする必要はありません。同じ順序はHPDFSetICCColorPipelineでインストールされるプルーフィングプロファイルにも当てはまります。解決を始める前に設定し、後からではありません
書き込み側の鏡像バグ
上記はすべて読み取りについてです。書き込み側の対称的な欠陥は、出荷しやすく、見落としもはるかに簡単です。HotPDFがWindows 8ビットビットマップから/Indexed配列を構築するとき、GetPaletteEntriesを呼びます。このAPIはパレットが実際に保持する数のエントリだけを埋め、その数を返します。元のコードは結果が= 0かどうかを検査し、スタック配列に対して固定の256エントリを反復しました。16色ビットマップの場合、初期化されていないスタックメモリの240エントリが、その後顧客に渡すPDFのルックアップ文字列へ直接書き込まれました。hivalは255にハードコードされているため、すべての読み手が残骸を本物のカラーデータとして忠実に扱います。これは正しさのバグであると同時に、同じ服を着た情報漏えいのバグです。修正は地味で完全です。呼び出しの前に配列をゼロ化し、ループ境界とhivalの両方で返された数に従います。自分のコードのどこかで/Indexed画像を生成しているなら、今すぐhivalを確認してください
隣接する2つの部品が全体像を完成させます。IndexedパレットとType 0サンプルド関数はデコード済みストリームリーダと同じクランプ規律を共有し、Type 0関数のカラールックアップテーブルに関する記事で論じられています。ルックアップストリームを支配するフィルタ規則は、デコードフィルタを通して読み込んだ画像を抽出するで画像データを支配するものと同じです。ここで説明したカラースペースリゾルバは、DelphiとC++Builder向けのHotPDF Delphi Componentの一部として同梱され、製品ページに完全なTHPDFColorSpaceリファレンスがあります