fix(SAMPLE-CORE): stop_token_ids is never range-checked, and the value becomes an unclamped logits column index
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:
PostInitfoldsstop_token_idsintoall_stop_token_ids(src/vllm/sampling_params.cpp:256).input_batch.cpp:274-280copies that set intoMinTokensStatewheneversp.min_tokens > 0.apply_min_tokenspushes each id straight intocols(src/vllm/v1/sample/logits_processor/builtin.cpp:30-32). The loopVT_CHECKs the request indexiat:26-27and applies no bound totok.vt::ApplyTokenMaskguards shape and dtype only —CheckSamplingLogitsplusCheckPairList(src/vt/ops.cpp:5095-5099). No value bound.- Both kernels index raw: CPU
lp[rp[k] * v + cp[k]] = kNegInf(src/vt/cpu/cpu_sample.cpp:305), CUDAlogits[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_idshalf is genuinely new._validate_stop_token_idsdoes not exist at5559679229, so for that half the commit is the distance. - The
allowed_token_idshalf is a pre-pin hole. The vocab-range check existed at5559679229:vllm/sampling_params.py:844+, bounded bylen(tokenizer), and was never ported. This commit only rebases that bound ontomodel_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
- A
[0, vocab_size)refusal for everystop_token_idsentry. - 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:InputProcessordeliberately does not retainHfConfig(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) touchesstop_token_idsbut only its count bounds. No value or range bound. - #2707 is the adjacent
/tokenizeHTTP 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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