Repository navigation
chat : reject JSON nested more than 128 levels before walking it - #30242
Conversation
|
it should already be fixed upstream, we need to bump version: nlohmann/json#5389 |
|
@ngxson please re-open if needed |
|
@ngxson #30253 does not fix this. Master f2cc728 includes #30253 and still crashes at the same depths as before it. Lowest tested depth that crashes the server on master f2cc728 (tested 100, 120, 130, 700, 1,000, 2,000, 10,000, 30,000, 1,000,000):
The recursion that overflows the stack is in llama.cpp code. #30253 covers nlohmann's copy, gdb, Linux, master f2cc728, tool schema nested 10,000 levels (19,523 frames):
Reproduction on master ( llama-server -hf LiquidAI/LFM2.5-VL-450M-GGUF:F16 --jinja --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.jsonWith this branch rebased on f2cc728, the same requests get HTTP 400 from 130 levels (500 for tool-call GitHub does not let me reopen this PR: the open pull request limit is reached. |
|
if you think this is a valid bug in json parsing, better to just open a PR on upstream https://github.com/nlohmann/json and we will apply it as a patch if it gets merged |
|
@ngxson I checked this. The crash is in llama.cpp, not in nlohmann. nlohmann reads the deep JSON fine. The server dies later, in llama.cpp functions that call themselves once per level: The nlohmann bug is already fixed upstream. nlohmann can report the depth while it parses (the parser callback), so llama.cpp does not need its own bracket counting. In 3.12.0 that callback is very slow on big inputs, which is why this PR counted brackets itself. nlohmann fixed it on develop in nlohmann/json#5457 (on top of nlohmann/json#5342). Both are merged, not released. Parse time with the depth callback, Linux x86,
New version of this PR, 2 commits, no own bracket counting:
static bool json_check_depth(int depth, ordered_json::parse_event_t event, ordered_json & /*parsed*/) {
const bool opens = event == ordered_json::parse_event_t::object_start ||
event == ordered_json::parse_event_t::array_start;
if (opens && depth >= COMMON_JSON_MAX_DEPTH) {
throw common_json_error("JSON is nested more than " + std::to_string(COMMON_JSON_MAX_DEPTH) + " levels deep");
}
return true;
}
common_json common_json::parse(const std::string & text) {
...
as_json(&out) = ordered_json::parse(text, ordered_json::parser_callback_t(json_check_depth));
...
}Evidence
I will update this PR with this version once I can reopen it (I am at the open PR limit). |
not sure but sounds like the fix should land in the
|
8dd0081 to
93c66c8
Compare
|
@ngxson Updated: Two points for you:
|
The schema builder, the jinja engine and the decision code walk request JSON with recursion. A request nested deep enough overflows the stack of the HTTP thread and llama-server dies (SIGBUS on macOS, SIGSEGV on Linux): for a tool schema from a few hundred levels on macOS and about 10000 on Linux. Check the depth first and answer 400 past 128 levels: the chat inputs in common_chat_templates_apply (tools, tool-call arguments, chat_template_kwargs, json_schema), the json_schema of /completion, and the whole /v1/systemone body. A gemma4 tool result that is too deep stays text.
93c66c8 to
1b705fe
Compare
There was a problem hiding this comment.
LGTM, the check now lives in chat.cpp as requested. Confirmed on Linux that every path that crashed on master now returns a 400, and that #31032 and #31099 still segfault on a nested response_format schema. 128 sounds right to me, the macOS cases come from the HTTP thread stack size and can be a follow-up.
|
I was not convinced with either fixes you proposed, so I pushed 7964e8d It's a stronger fix because the root cause is that we don't have any bound for recursive functions. Fix by adding a call-side validation works, but cost (1) walking the json tree twice and (2) won't establish a strong pattern for future code to follow The code base may have many other places where recursive functions having no bound, will be just a simple prompt away from fixing them. |
aldehir
left a comment
There was a problem hiding this comment.
I agree, doing a second parsing pass feels wrong and it won't protect against $ref loops in JSON schemas, so there would need to be a second guard in place when a recursion limit would address both.
|
Indeed, even better: bounding each recursive function fixes the root cause, avoids walking the JSON twice and also catches $ref loops. Retested on Linux: the same paths return a 400, 120 levels still work, and a $ref loop no longer crashes. The Windows failure is test-thread-safety, which fails the same way on master. |
…l-org#30242) * chat : reject JSON nested more than 128 levels before walking it The schema builder, the jinja engine and the decision code walk request JSON with recursion. A request nested deep enough overflows the stack of the HTTP thread and llama-server dies (SIGBUS on macOS, SIGSEGV on Linux): for a tool schema from a few hundred levels on macOS and about 10000 on Linux. Check the depth first and answer 400 past 128 levels: the chat inputs in common_chat_templates_apply (tools, tool-call arguments, chat_template_kwargs, json_schema), the json_schema of /completion, and the whole /v1/systemone body. A gemma4 tool result that is too deep stays text. * fix the root, not the call side --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Overview
A request with deeply nested JSON crashes
llama-server. nlohmann parses it fine. The stack overflows later, in llama.cpp code that walks the parsed JSON with recursion:parametersschema,response_formatschemacommon_chat_schema_builder::build_node/build_arrayargumentsvalue_to_string_repr/value_array_t::as_string/v1/systemonestatevalue_to_json_internal(the template'stojson); deeper,decision_d1omni_escapeThis PR checks the nesting depth before that code runs and answers 400 past 128 levels:
common_chat_templates_apply: tools, tool-call arguments,chat_template_kwargsandjson_schema, after they are parsed. This covers/v1/chat/completions,/v1/responses,/v1/messagesand/apply-template./completionand/v1/completions:json_schema(server-schema.cpp)./v1/systemone: the whole body (parse_questions).common/json.cppis not changed. The check iscommon_chat_check_json_depthinchat.cpp; it stops one level past the limit, so its own recursion is at most 129 calls deep.Additional information
Proof 1, reproduction on master (crashes at
d=1000on macOS; used=30000on Linux):Proof 2, the same kinds of request, master and this PR (HTTP code per nesting depth, macOS M3 Ultra):
sys=/v1/systemonestate (d1-omni),tool= tool parameters,rf=response_format,strargs= tool-call arguments as a string (as an object they give the same codes). master's 500 forsysat 700 and 1000 is the batch size error, not a crash. On Linux master dies from 10,000 (schemas) and 30,000 levels (state, arguments); this PR answers 400 from 130. At 100 and 120 levels the answers are the same as on master.Proof 3, tests.
test-chatgetstest_chat_json_depth(the limit, and 1,000 levels in tool parameters, tool-call arguments,chat_template_kwargsandjson_schema);test-server-componentgetsdecision_depth. With master's code (without the three direct calls of the new function) both fail:test-chatbecause the 1,000-level inputs are not rejected,decision_depthwith another error (too many options (2), this model supports at most 0). With this PR both pass on macOS and Linux.Not fixed here:
$refchains, many optional properties, one long property name in an object withadditionalProperties.response_format, 1,000 levels in the prefill still crashllama-serveron macOS with this PR.Requirements