---
id: "GHSA-rwfw-xvg3-3937"
page: "https://infrastack.ai/live/GHSA-rwfw-xvg3-3937"
kind: "vulnerability"
aliases: ["GHSA-rwfw-xvg3-3937"]
title: "Rust gRPC CandidateTokens.top_n bypasses max_logprobs and can terminate EngineCore"
severity: "medium"
cvss: 5.9
epss: null
exploited: false
status: "open"
category: "ai-tooling"
products: [{"name": "vllm", "fixed": ["0.24.0"], "affected": ">= 0.22.0, < 0.24.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["ai-tooling", "cwe-190", "cwe-20", "cwe-400", "grpc", "python", "self-hosted", "vllm"]
not_affected_if: ["auth-required", "default-config"]
published_at: "2026-10-09T07:47:04+00:00"
first_seen_at: "2026-11-09T22:53:23+00:00"
updated_at: "2026-11-09T23:01:04+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/vllm-project/vllm/security/advisories/GHSA-rwfw-xvg3-3937", "first_seen_at": "2026-11-09T22:53:23+00:00"}]
exploit_sources: []
---

# Rust gRPC CandidateTokens.top_n bypasses max_logprobs and can terminate EngineCore

Link for people: https://infrastack.ai/live/GHSA-rwfw-xvg3-3937 (this .md is for agents)

A request to vLLM's Rust gRPC Generate or GenerateStream API can set a top_n logprob count that skips the max_logprobs limit enforced by the Python API. A single request with an oversized or overflowed count can make the engine core fail, which stops generation (denial of service).

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:vllm affected >= 0.22.0, < 0.24.0 fixed 0.24.0

## How to check

- Check the installed version with `pip show vllm` and whether it is between `0.22.0` and `0.24.0`.
- Check whether the gRPC frontend is enabled and reachable from untrusted networks.

## Fix

- `vllm` (PyPI): move to `0.24.0` or later.

## Timeline

- 2026-11-09T22:53:23+00:00 ghsa-repo https://github.com/vllm-project/vllm/security/advisories/GHSA-rwfw-xvg3-3937

## Documents

- [advisory] Vendor advisory: github.com https://github.com/vllm-project/vllm/security/advisories/GHSA-rwfw-xvg3-3937

## From the sources

## 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:

```proto
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:

```rust
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:

```rust
EngineCoreSamplingParams {
    logprobs,
    prompt_logprobs,
    ...
}
```

By contrast, Python validates the same fields:

```python
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:

```python
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:

```text
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:

```text
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.

## 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.

## Affected versions

- Package: `vllm` (PyPI).
- Affected versions: `>= 0.22.0, < 0.24.0`.
- First stable patched version: `0.24.0`.

Advisory fixed in [#45674](https://github.com/vllm-project/vllm/pull/45674).

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
