llama.cpp b11871は、過度に深くネストされたJSONリクエストを処理前に拒否するよう変更された。サーバーを運用する開発者は、1件のリクエストのエラーでサーバー全体が終了する状況を減らせる。深さが基準を超えるとHTTP 400の応答を受け取る。
1. 再帰処理がサーバーを停止させた原因
問題は、JSONを読み込んだ後、内部構造をたどって同じ関数を繰り返し呼び出す再帰処理で発生した。幾重にも重なった構造をたどるうちに、HTTPリクエストの処理に使うメモリ領域であるスタックを使い果たすと、サーバー全体が終了する可能性があった。
リリースノートによると、入力形式を定義するツールスキーマのリクエストは、macOSでは数百段階から、Linuxでは約1万段階からサーバー終了を引き起こした。
変更提案(PR)の以下のコードは、深さを1000に設定したツール定義をサーバーに送る再現手順だ。実行すると従来のサーバーを終了させる可能性がある例で、macOSではこの深さで終了し、Linuxでは深さを30000に上げるよう説明している。
llama-server -hf LiquidAI/LFM2.5-VL-450M-GGUF -hff LFM2.5-VL-450M-F16.gguf --port 8080 &
python3 -c "d=1000; s='{\"type\": \"array\", \"items\": '*d+'{\"type\": \"string\"}'+'}'*d; print('{\"messages\": [{\"role\": \"user\", \"content\": \"hi\"}], \"max_tokens\": 1, \"tools\": [{\"type\": \"function\", \"function\": {\"name\": \"f\", \"description\": \"d\", \"parameters\": {\"type\": \"object\", \"properties\": {\"x\": '+s+'}}}}]}')" > deep.json
curl -s localhost:8080/v1/chat/completions -H 'Content-Type: application/json' --data-binary @deep.json2. 128段階を超えると処理前に拒否
変更は、パース、つまり文字列を構造として読み込む処理を終えたJSONの深さを先に確認し、その後の再帰処理が過度に進まないようにするものだ。PRの実装説明によると、検査関数自体の再帰呼び出しも最大129回に制限される。
リリースノートは、128段階を超える入力にHTTP 400を返すと説明している。呼び出し側はサーバーが終了したかを確認する代わりに、深さが過度なリクエストを拒否したという応答を受け取れる。
検査対象は、チャットリクエストのツール定義とツール呼び出し引数、ツールに渡す実行値、テンプレート変数、ひな型に値を埋め込む項目、スキーマだ。補完リクエストのスキーマと状態管理リクエストの本文全体も検査する。深すぎるgemma4のツール結果は構造化された結果として解釈せず、テキストのまま残す。
これにより、正常範囲のリクエストの流れを維持しつつ、深い入力がサーバーのスタックを使い果たす経路を遮断する。一方、128段階を超えるJSONは処理されなくなるため、その深さに依存する呼び出し側はリクエスト構造を浅くする必要がある。
3. 従来のコードと適用版の応答の違い
以下は、PR作成者がmacOS M3 Ultraで変更前のコードと適用版を比較した結果だ。200は正常処理、400は深さ超過で拒否された応答であり、適用版は測定したリクエスト種別で130段階から400を返した。
表:ネスト段階別のHTTP応答
条件:macOS M3 Ultra、PRの変更前と適用版の比較。状態管理はd1-omni、ツール呼び出し引数は文字列入力。
単位:HTTPステータスコード。—はサーバー終了により応答なし。
| ネスト段階 | 変更前の状態管理 | 変更前のツールパラメーター | 変更前の応答形式 | 変更前のツール呼び出し引数 | 適用版の全種別 |
|---|---|---|---|---|---|
| 100 | 200 | 200 | 200 | 200 | 200 |
| 120 | 200 | 200 | 200 | 200 | 200 |
| 130 | 200 | 200 | 200 | 200 | 400 |
| 700 | 500 | — | — | 200 | 400 |
| 1000 | 500 | — | — | 200 | 400 |
| 2000 | — | — | — | — | 400 |
| 10000 | — | — | — | — | 400 |
| 30000 | — | — | — | — | 400 |
| 1000000 | — | — | — | — | 400 |
変更前も100段階と120段階の応答は適用版と同じだった。状態管理リクエストの700段階と1000段階で記録された500は、サーバー終了ではなくバッチサイズのエラー、つまり一度に処理するまとまりのサイズのエラーだ。
PRは、Linuxでは従来のスキーマリクエストは1万段階、状態と引数のリクエストは3万段階から終了し、変更適用版は130段階から400を返したと付け加えた。
4. 検査の負荷と残された範囲
PR作成者はM3 Maxで検査コストを測定した。0.33MBのチャットリクエストは0.02msで、JSONのパース時間1.4msより短く、10MBのリクエストは0.54ms(パース35ms)だった。整数100万個の配列は3.8ms(パース41ms)だった。この数値はPRの当該環境での測定であり、入力が大きくなると検査時間も増えるが、報告された3事例ではパース時間より短かった。
PRは、macOSでは一部のテンプレートが128段階未満でもスタックを超過する可能性があると明らかにした。アシスタントメッセージで終わるリクエストでモデルの応答前にあらかじめ入れる事前入力と、ネスト以外の原因で発生するJSONスキーマ変換の再帰問題も、今回の変更対象から除外した。
b11871の要点は、深い入力による失敗をサーバー全体の終了からリクエスト単位の拒否へと移し、運用負担を減らしたことにある。
