All vulnerabilities
GHSA-rwfw-xvg3-3937

vllm: Rust gRPC CandidateTokens.top_n bypasses max_logprobs and can terminate EngineCore

vllm
CVSS
5.9Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in0.24.0Fixed versionGitHub
Published9 Oct 2026Updated 9 Oct 2026GitHub

What fixes it

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 explicit CandidateTokens.token_ids selector lowering into logprob_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 Validation
  • CWE-400: Uncontrolled Resource Consumption
  • CWE-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 i32 with 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-cap max_logprobs = -1;
  • preserve the explicit all selector as the only way to request full-vocab logprobs;
  • return Status::invalid_argument(...) before submitting any engine request; and
  • add unary Generate and streaming GenerateStream regression tests for:
  • top_n = 20 accepted under default config;
  • top_n = 21 rejected under default config;
  • top_n = 1_000_000 rejected;
  • top_n = 2_147_483_648 rejected; 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 SamplingParams API docs: https://docs.vllm.ai/en/latest/api/vllm/sampling_params/
  • vLLM --max-logprobs CLI 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 bypass logprob_token_ids bounds. 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 OpenAI n fan-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