vllm: Rust gRPC CandidateTokens.top_n bypasses max_logprobs and can terminate EngineCore
What fixes it
- 0.24.0vllm, pipGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
The Rust gRPC Generate and GenerateStream API accepts CandidateTokens.top_n from ResponseOptions.output_candidates and ResponseOptions.prompt_candidates, casts the protobuf uint32 value to signed i32, and lowers it directly into SamplingParams.logprobs or SamplingParams.prompt_logprobs.
Python SamplingParams rejects negative logprob counts other than -1, rejects sample and prompt logprob counts above model_config.max_logprobs, and defaults max_logprobs to 20. The official CLI docs warn that removing the cap with --max-logprobs -1 allows output_length * vocab_size logprobs and may cause OOM.
Rust gRPC bypasses that intended guard. A caller can request an oversized top_n such as 1_000_000, causing the lower sampler path to call torch.topk(logits, num_logprobs) with a request-controlled count that Python would have rejected. A caller can also send top_n = 2_147_483_648; Rust casts that uint32 to -2_147_483_648, bypassing the Rust converter and reaching the lower assert num_logprobs >= 0 / top-k path as an invalid negative count.
Technical Details
The protobuf exposes an unbounded uint32 top_n selector for both output and prompt candidate logprobs:
message ResponseOptions {
bool prompt_logprobs = 2;
optional CandidateTokens prompt_candidates = 3;
bool output_logprobs = 6;
optional CandidateTokens output_candidates = 7;
}
message CandidateTokens {
oneof select {
uint32 top_n = 1;
TokenIds token_ids = 2;
bool all = 3;
}
}The Rust gRPC converter lowers top_n without checking the configured max_logprobs cap and without checking the uint32 to i32 conversion:
if r.output_logprobs {
let (count, token_ids) = candidate_logprob_spec(r.output_candidates.as_ref());
params.logprobs = Some(count);
params.logprob_token_ids = token_ids;
}
if r.prompt_logprobs {
let (count, _) = candidate_logprob_spec(r.prompt_candidates.as_ref());
params.prompt_logprobs = Some(count);
}
Some(pb::candidate_tokens::Select::TopN(n)) => (*n as i32, None),The Rust text lowerer forwards those fields to engine-core without validation:
EngineCoreSamplingParams {
logprobs,
prompt_logprobs,
...
}By contrast, Python validates the same fields:
if self.logprobs is not None and self.logprobs != -1 and self.logprobs < 0:
raise VLLMValidationError(...)
if num_logprobs := self.logprobs:
if num_logprobs == -1:
num_logprobs = model_config.get_vocab_size()
if num_logprobs > max_logprobs:
raise VLLMValidationError(...)
if num_prompt_logprobs := self.prompt_logprobs:
if num_prompt_logprobs == -1:
num_prompt_logprobs = model_config.get_vocab_size()
if num_prompt_logprobs > max_logprobs:
raise VLLMValidationError(...)The lower V1 sampler then consumes the untrusted value:
assert num_logprobs >= 0 topk_indices = torch.topk(logits, num_logprobs, dim=-1).indices
Prompt logprobs reach the same top-k helper through compute_prompt_logprobs_with_chunking(...).
vLLM intentionally exposes logprobs and prompt logprobs, but it does not intend one request to bypass the configured max_logprobs budget. The default ModelConfig.max_logprobs is 20, and the official launch docs explicitly warn that no-cap mode can return output_length * vocab_size logprobs and may cause OOM.
The Python API and engine-level SamplingParams validators implement that budget before engine execution. The Rust gRPC converter constructs engine-facing sampling state directly, so it must enforce the same safety contract before submitting a request.
This is distinct from:
this report/GHSA-6pgw-f3pv-4h8q, which covers the explicitCandidateTokens.token_idsselector lowering intologprob_token_ids.this report/GHSA-w2v2-5662-8v5q, which covers out-of-vocabulary prompt token IDs.
Public PR #44680 also notes that top_logprobs/prompt_logprobs are the same DoS class but out of scope for that token-ID parity patch because they need max_logprobs wiring.
Impact
An attacker who can reach a vLLM Rust gRPC endpoint can send a small gRPC request with output_logprobs = true and an oversized output_candidates.top_n, or with prompt_logprobs = true and an oversized prompt_candidates.top_n.
Expected impact is single-request denial of service for affected deployments: the Rust gRPC frontend accepts the count, forwards it to engine-core sampling state, and the lower V1 logprob path can raise after execution begins. EngineCore treats uncaught worker exceptions as fatal engine errors.
This report does not claim code execution, data access, model compromise, persistence, or public internet exposure. It is an availability issue caused by missing Rust gRPC input validation and an unchecked unsigned-to-signed cast.
Suggested severity: High if the Rust gRPC endpoint is network-reachable without an additional deployment-layer credential requirement.
Suggested CVSS v3.1:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
If maintainers model the affected gRPC surface as reachable only to already-authorized clients, the conservative score is:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
Suggested CWEs:
CWE-20: Improper Input ValidationCWE-400: Uncontrolled Resource ConsumptionCWE-190: Integer Overflow or Wraparound
Suggested Fix
Validate CandidateTokens.top_n in rust/src/server/src/grpc/convert.rs before assigning params.logprobs or params.prompt_logprobs.
Recommended patch shape:
- replace
*n as i32with a checked conversion; - reject values larger than
i32::MAX; - reject values larger than the configured model/server
max_logprobs, unless the operator explicitly configured no-capmax_logprobs = -1; - preserve the explicit
allselector as the only way to request full-vocab logprobs; - return
Status::invalid_argument(...)before submitting any engine request; and - add unary
Generateand streamingGenerateStreamregression tests for: top_n = 20accepted under default config;top_n = 21rejected under default config;top_n = 1_000_000rejected;top_n = 2_147_483_648rejected; and- both output and prompt candidate selectors covered.
Affected Package/Versions
- Package:
vllm(PyPI). - Affected versions:
>= 0.22.0, < 0.24.0. - Patched version:
0.24.0.
References
- vLLM security policy:
https://github.com/vllm-project/vllm/security - vLLM vulnerability management:
https://docs.vllm.ai/en/latest/contributing/vulnerability_management/ - vLLM
SamplingParamsAPI docs:https://docs.vllm.ai/en/latest/api/vllm/sampling_params/ - vLLM
--max-logprobsCLI docs:https://docs.vllm.ai/en/latest/cli/launch/render/ - vLLM sampler docs:
https://docs.vllm.ai/en/latest/api/vllm/v1/sample/sampler/ - vLLM PR
#44680:https://github.com/vllm-project/vllm/pull/44680
Appendix: Duplicate Review
I checked local vLLM submission bundles, private-visible vLLM advisories, and GitHub PR/issue search for top_logprobs, prompt_logprobs, max_logprobs, top_n, CandidateTokens, and gRPC logprob variants.
Nearby but distinct:
this report/GHSA-6pgw-f3pv-4h8q: explicit candidate token IDs bypasslogprob_token_idsbounds. This report covers count-based top-k selection and unsigned-to-signed overflow.this report/GHSA-w2v2-5662-8v5q: prompt token-ID range validation.GHSA-3mwp-wvh9-7528: unbounded OpenAInfan-out DoS. This report uses a different gRPC response selector and a different lower sampler sink.
Public PR #44680 names top_logprobs/prompt_logprobs as out-of-scope vectors that need max_logprobs wiring. PR #45390 does not touch Rust gRPC files. Public GitHub PR and issue searches for the exact Rust gRPC CandidateTokens.top_n path returned no matches.
Text from GitHub Advisory Database
Affected versions 1
- vllm, pip>= 0.22.0, < 0.24.0GitHub