mudler / mudler/vllm.cpp

fix(SAMPLE-CORE): stop_token_ids is never range-checked, and the value becomes an unclamped logits column index

Open
#2,724 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
423
Forks
53
Avg merge
20h 26m
Merged PRs (30d)
310

Description

Row: SAMPLE-CORE (.agents/engine-matrix.md:140), whose description is "Ordered temperature, top-k/p, min-p, penalties, seed, stop, length, output-kind pipeline" and whose local-anchor column names src/vllm/sampling_params.cpp and src/vllm/v1/worker/gpu/input_batch.cpp. The allowed_token_ids half belongs to SAMPLE-LOGIT-FILTERS (.agents/engine-matrix.md:145). Both rows were resolved by line number before this issue was filed.

Found by wave PORTQ-7 (#2717) re-deriving PORT-NOW entry [283], upstream 5b0e5b69ac vllm#54196. Nothing was executed. Every line below is a static reading of source.

The defect

stop_token_ids is never range-checked against the vocabulary, and the value flows unclamped into a logits column index.

SamplingParams::Verify() checks that allowed_token_ids is non-empty and nothing else (src/vllm/sampling_params.cpp:184-186). The file states the omission itself, twice: "the model-config vocab-range check stays engine-time / deferred" (:182-183, and again at :133-134). The engine side does not close it either — InputProcessor::ValidateParams (src/vllm/v1/engine/input_processor.cpp:130-156) validates only the logprobs cap.

The chain from an unvalidated request id to an unbounded write, each link read:

  1. PostInit folds stop_token_ids into all_stop_token_ids (src/vllm/sampling_params.cpp:256).
  2. input_batch.cpp:274-280 copies that set into MinTokensState whenever sp.min_tokens > 0.
  3. apply_min_tokens pushes each id straight into cols (src/vllm/v1/sample/logits_processor/builtin.cpp:30-32). The loop VT_CHECKs the request index i at :26-27 and applies no bound to tok.
  4. vt::ApplyTokenMask guards shape and dtype only — CheckSamplingLogits plus CheckPairList (src/vt/ops.cpp:5095-5099). No value bound.
  5. Both kernels index raw: CPU lp[rp[k] * v + cp[k]] = kNegInf (src/vt/cpu/cpu_sample.cpp:305), CUDA logits[rows[k] * v + cols[k]] = kNegInf (src/vt/cuda/cuda_sample.cu:874).

The path is reachable from an unauthenticated POST /v1/completions or POST /v1/chat/completions: both parse stop_token_ids (src/vllm/entrypoints/openai/protocol.cpp:360, :539, forwarded at :645 and :701) and min_tokens (:379, :569). A negative id underflows the same expression.

This tree already has the idiom, for a sibling parameter. bad_words is range-checked against tokenizer_.VocabSize() - 1 at src/vllm/v1/engine/input_processor.cpp:226-240, with upstream's own message. So the omission for stop_token_ids is an omission, not a design.

The same shape has already cost this project once. input_processor.cpp:137-141 records it in a comment: "Without this a single {\"logprobs\": 999999} reached GatherLogprobs, which partial_sorts k entries out of a vocab-sized index array and walked off the end — one unauthenticated request killed the server process." That was #249.

The allowed_token_ids half behaves differently and is not memory-unsafe: its write site is guarded (src/vllm/v1/worker/gpu/input_batch.cpp:349-351, if (0 <= tid && tid < vocab_size)). It silently ignores an out-of-vocab id where upstream refuses.

Pre-pin split, which changes what "porting the diff" means

  • The stop_token_ids half is genuinely new. _validate_stop_token_ids does not exist at 5559679229, so for that half the commit is the distance.
  • The allowed_token_ids half is a pre-pin hole. The vocab-range check existed at 5559679229:vllm/sampling_params.py:844+, bounded by len(tokenizer), and was never ported. This commit only rebases that bound onto model_config.get_vocab_size(). Porting the commit's diff for this half would land the rebase of a check this tree does not have.

What is missing

  1. A [0, vocab_size) refusal for every stop_token_ids entry.
  2. The same for allowed_token_ids — the whole check, not the rebase.

Neither can live in SamplingParams::Verify() as written, which takes no config. Both belong in InputProcessor::ValidateParams (src/vllm/v1/engine/input_processor.cpp:130), which already holds tokenizer_.VocabSize() at :145 and already carries the bad_words equivalent at :226-240.

Size: ~25-35 product lines (two loops, two messages, and the bound derived properly), ~60-90 test lines. The test must be red-first: a request carrying an out-of-vocab stop_token_ids together with min_tokens > 0 must refuse where it currently writes to a logits column nothing bounds.

The one thing this issue does not settle

Which vocabulary size to refuse against. Upstream now uses model_config.get_vocab_size(); this tree has no get_vocab_size and would reach for tokenizer_.VocabSize(). #2699 already records that those two can differ. The allowed_token_ids_mask width is sized by vocab_size at input_batch.cpp:342-344; reading which quantity that is would settle it.

Three neighbours, checked, no overlap

  • #2699 (PORTQ-5 entry [164]) is about prompt token ids — ValidatePromptLen, the embedding index. Different parameter, different function, different consequence. Its body was read, not its title. What the two share is the plumbing #2699 identifies as the real cost: InputProcessor deliberately does not retain HfConfig (include/vllm/v1/engine/input_processor.h:80-84), so both need a model vocab size at validation time. Whoever lands first should carry it for the other.
  • #2688 (SERVE-REQUEST-LENGTH-GUARD) touches stop_token_ids but only its count bounds. No value or range bound.
  • #2707 is the adjacent /tokenize HTTP 500, a different handler.

Caveat on the evidence

Nothing here was run. The out-of-bounds write is read from source, not observed; whether a given v and id place the write inside the same allocation or outside it is undetermined and the guard is warranted either way.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/vllm/v1/engine/input_processor.cpp at InputProcessor::ValidateParams, comparing the existing bad_words range check with the stop_token_ids and allowed_token_ids paths. Read src/vllm/v1/worker/gpu/input_batch.cpp to determine which vocabulary size its mask uses, then trace the request parsing in protocol.cpp. Done means both token-id sets reject out-of-vocabulary values, with a regression test covering stop_token_ids together with min_tokens > 0.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend-api-design, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.