Skip to content

server: fix client-triggerable double-free in JSON request parsing#301

Open
ferdinandobons wants to merge 1 commit into
antirez:mainfrom
ferdinandobons:fix/json-parser-double-free
Open

server: fix client-triggerable double-free in JSON request parsing#301
ferdinandobons wants to merge 1 commit into
antirez:mainfrom
ferdinandobons:fix/json-parser-double-free

Conversation

@ferdinandobons
Copy link
Copy Markdown

What

Fix a client-triggerable double-free in the HTTP JSON request parsers in ds4_server.c.

The request parsers free a string field and then re-parse into the same field:

free(r->model);
if (!json_string(&p, &r->model)) { /* ... */ goto bad; }

The readers — json_string, json_content, json_raw_value, parse_prompt, parse_anthropic_system, parse_responses_content_array — never write *out on their failure paths. So on a malformed value the field keeps pointing at the just-freed buffer, and the error path frees it again (request_free, a cleanup label bad:/done:/fail:/item_fail:, or tool_call_free) → a double-free fully controlled by the request body.

Impact

r->model is initialized non-NULL by request_init(), so the double-free fires on the first malformed occurrence — no duplicate key needed:

POST /v1/chat/completions   {"model": 5}
POST /v1/messages           {"model": 123}
POST /v1/responses , /v1/completions   likewise

Every other free-then-reparse field is the same class, reachable via a duplicated object key whose second value is malformed, e.g. a content block {"type":"text","type":5} or tool_calls {"arguments":"x","arguments":"\u"}.

Fix

Set the pointer to NULL immediately after the free() at every free-then-reparse site (48 sites). Safe and behaviour-preserving:

  • On failure the reader doesn't touch *out, so the field stays NULL and the cleanup free() is a no-op.
  • On success the field is overwritten by the parsed value, so valid input is unchanged. No leak is introduced (the old value was already freed before the re-parse).

Testing — AddressSanitizer

A harness that #includes ds4_server.c and calls the real parsers (no reimplementation). Three independent triggers, each hitting a different cleanup path:

trigger endpoint path
{"model": 5} parse_chat_requestrequest_free
[{"function":{"arguments":"x","arguments":"\u"}}] parse_function_calltool_call_free
{"prompt":"x","prompt":"\u"} parse_completion_request cleanup
  • Unpatched (main): all three abort with AddressSanitizer: attempting double-free.
  • With this change: all three return cleanly (invalid JSON request), no double-free.
  • Builds clean with the project flags (-O3 -ffast-math -Wall -Wextra -std=c99); a completeness scan finds no remaining free-then-reparse site without a following = NULL.

No functional change on valid requests.

The HTTP request parsers free a string field and then re-parse into the
same field, e.g.:

    free(r->model);
    if (!json_string(&p, &r->model)) { ...; goto bad; }

The readers (json_string, json_content, json_raw_value, parse_prompt,
parse_anthropic_system, parse_responses_content_array) never write *out on
their failure paths, so on a malformed value the field keeps pointing at the
just-freed buffer and the error path frees it again (request_free, or a
cleanup label such as bad:/done:/fail:/item_fail:, or tool_call_free) — a
double-free fully controlled by the request body.

For r->model the field is non-NULL from request_init(), so a single request
is enough, no duplicate key needed:

    POST /v1/chat/completions  {"model": 5}
    POST /v1/messages          {"model": 123}
    POST /v1/responses, /v1/completions  likewise

Every other free-then-reparse field is the same class, reachable via a
duplicated object key whose second value is malformed, e.g. a content block
{"type":"text","type":5} or tool_calls {"arguments":"x","arguments":"\u"}.

Fix: set the pointer to NULL immediately after the free at every
free-then-reparse site (48 sites), so a failed re-parse leaves it NULL and the
cleanup free() becomes a no-op. On success the field is overwritten by the
parsed value, so behaviour on valid input is unchanged.

Verified under AddressSanitizer with a harness that #includes ds4_server.c and
calls the real parsers: on the unpatched code {"model":5},
[{"function":{"arguments":"x","arguments":"\u"}}] and
{"prompt":"x","prompt":"\u"} each abort with "attempting double-free"; with
this change all three return cleanly with no double-free. Builds clean with
-Wall -Wextra.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant