Skip to content

Duplicate call_id silently rebinds function_call_output to the wrong function in the rendered prompt #308

Description

@AUTHENSOR

Duplicate call_id silently rebinds function_call_output to the wrong function in the rendered prompt

Affected: 7b58334 (gpt_oss/responses_api/api_server.py)

Summary

When a request input contains two function_call items that share one call_id
but target different functions, the tool outputs are bound to whichever
function was registered last under that id, regardless of which call each
output actually answers. The server renders the conversation shown to the
model with the wrong function attribution and drops the pairing entirely for
the losing function. The binding is positional over a silently overwritten
dict, not content bound, and no validation or error is raised for the
duplicate id.

Code

In gpt_oss/responses_api/api_server.py inside the /v1/responses handler:

function_call_map = {}                    # line 1263
...
elif item.type == "function_call":        # line 1299
    function_call_map[item.call_id] = item    # line 1300, silent overwrite
    messages.append(... .with_recipient(f"functions.{item.name}") ...)
elif item.type == "function_call_output": # line 1306
    function_call = function_call_map.get(item.call_id, None)
    ...
    messages.append(
        Message.from_author_and_content(
            Author.new(Role.TOOL, f"functions.{function_call.name}"),  # line 1313
            item.output,
        )
        ...
    )

There is no uniqueness check on call_id and no ambiguity error, so the last
function_call item registered under an id receives every output sent for
that id.

Reproduction

Run poc/poc_1_call_binding.py (see poc/run_all.sh). It drives the real
FastAPI app from create_api_server with the same stub-token approach the
repo's own tests use, and captures the exact prompt tokens handed to the model.

  • Control arm (distinct ids): the prompt renders
    <|start|>functions.get_weather to=assistant<|channel|>commentary<|message|>WEATHER_RESULT 21C sunny
    and the matching delete_file result, correctly paired.

  • Attack arm B (shared id, outputs after both calls): the prompt renders the
    weather result under the delete_file recipient:

    <|start|>functions.delete_file to=assistant<|channel|>commentary<|message|>WEATHER_RESULT 21C sunny

    The get_weather call receives no result at all and delete_file receives two.

  • Attack arm C (identical call/output pairs, interleaved order): the same
    output renders under get_weather, purely because of its position relative
    to the duplicate registrations.

Evidence from a byte-identical double run: attack_B_misbinds_weather_to_delete_file: true, same_pairs_different_order_different_attribution: true.
(byte-identical double run; verdicts:
attack_B_misbinds_weather_to_delete_file: true,
same_pairs_different_order_different_attribution: true).

Impact

A client that reuses call ids across different functions (per-turn counters
such as call_0, merged histories from two assistant turns, or history
replay combined with previous_response_id, which concatenates stored output
items with newly supplied input items) gets a conversation in which tool
results are attributed to the wrong tool. The model then reasons over a
fabricated pairing, for example treating a destructive call as having
returned the other tool's payload, with no error surfaced to either side.

Suggested fix

Reject or disambiguate duplicate call ids when building the input
(ValueError/422, matching the existing behavior for an output with an
unknown call id), or key the tool message on the call item the output
immediately follows rather than a last-writer-wins map.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions