---
id: "GHSA-6pgw-f3pv-4h8q"
page: "https://infrastack.ai/live/GHSA-6pgw-f3pv-4h8q"
kind: "vulnerability"
aliases: ["GHSA-6pgw-f3pv-4h8q"]
title: "Rust gRPC output_candidates.token_ids bypasses logprob_token_ids limits 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-20", "cwe-400", "cwe-770", "grpc", "python", "self-hosted", "vllm"]
not_affected_if: ["default-config", "no-grpc-frontend"]
published_at: "2026-10-09T07:47:33+00:00"
first_seen_at: "2026-11-09T22:53:23+00:00"
updated_at: "2026-11-09T23:01:03+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/vllm-project/vllm/security/advisories/GHSA-6pgw-f3pv-4h8q", "first_seen_at": "2026-11-09T22:53:23+00:00"}]
exploit_sources: []
---

# Rust gRPC output_candidates.token_ids bypasses logprob_token_ids limits and can terminate EngineCore

Link for people: https://infrastack.ai/live/GHSA-6pgw-f3pv-4h8q (this .md is for agents)

The vLLM Rust gRPC Generate and GenerateStream endpoints accept more than 128 output candidate token IDs without the validation the Python API enforces. A single request with output logprobs enabled can make the engine core terminate, which is a 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 vLLM version with `pip show vllm`; versions `>= 0.22.0, < 0.24.0` are affected.
- Check whether the Rust gRPC frontend is enabled and reachable from the network.

## Fix

- Upgrade `vllm` to `0.24.0`.

## Timeline

- 2026-11-09T22:53:23+00:00 ghsa-repo https://github.com/vllm-project/vllm/security/advisories/GHSA-6pgw-f3pv-4h8q

## Documents

- [advisory] Vendor advisory: github.com https://github.com/vllm-project/vllm/security/advisories/GHSA-6pgw-f3pv-4h8q

## From the sources

## Summary

The Rust gRPC `Generate` and `GenerateStream` API accepts `ResponseOptions.output_candidates.token_ids` and lowers the supplied list directly into `SamplingParams.logprob_token_ids` without enforcing the Python `SamplingParams` validator.

Python explicitly caps `logprob_token_ids` at `MAX_LOGPROB_TOKEN_IDS = 128`, range-checks each token ID against model vocabulary, and requires `logprobs == len(logprob_token_ids)` when both are set. The V1 GPU logprob sampler allocates a fixed `(max_num_reqs, MAX_LOGPROB_TOKEN_IDS)` row and raises `ValueError` if a request carries more than 128 `logprob_token_ids`.

The Rust gRPC converter bypasses that intended guard. A caller can request output logprobs with 129 or more explicit candidate token IDs. The Rust frontend accepts the request, stores the unbounded list in `SamplingParams`, and sends it toward the Python engine-core boundary, where the lower sampler path can raise after the request has entered execution. EngineCore treats uncaught worker exceptions as fatal engine errors.

## Technical Details

The protobuf exposes explicit output candidate token IDs:

```proto
optional CandidateTokens output_candidates = 7;

message CandidateTokens {
  oneof select {
    uint32 top_n = 1;
    TokenIds token_ids = 2;
    bool all = 3;
  }
}
```

The Rust gRPC converter maps `output_candidates.token_ids` directly into `SamplingParams.logprob_token_ids`:

```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;
}

Some(pb::candidate_tokens::Select::TokenIds(ids)) => (1, Some(ids.ids.clone())),
```

There is no length check against `MAX_LOGPROB_TOKEN_IDS`, no vocabulary range-check, and no `logprobs == len(logprob_token_ids)` consistency check.

By contrast, Python `SamplingParams` explicitly rejects the same unsafe state:

```python
MAX_LOGPROB_TOKEN_IDS = 128
"""Upper bound on `SamplingParams.logprob_token_ids` list length. Must match
the per-request row width allocated by the sampler's `LogprobTokenIdsState`."""

if self.logprob_token_ids is not None:
    n = len(self.logprob_token_ids)
    if n > MAX_LOGPROB_TOKEN_IDS:
        raise VLLMValidationError(...)
```

The lower GPU logprob sampler confirms this is not only a presentation-layer preference. It allocates a fixed-width row and raises if an oversized list reaches `add_request()`:

```python
self.token_ids = StagedWriteTensor(
    (max_num_reqs, MAX_LOGPROB_TOKEN_IDS),
    dtype=torch.int32,
    device=device,
)

if n > MAX_LOGPROB_TOKEN_IDS:
    raise ValueError(
        f"Too many logprob_token_ids: {n}. The max is {MAX_LOGPROB_TOKEN_IDS}."
    )
```

The intended behavior is visible in three independent places.

First, the Python API documents and enforces `MAX_LOGPROB_TOKEN_IDS = 128`, with the comment that the bound must match the sampler row width.

Second, the Rust gRPC converter already recognizes that candidate token ID selectors need special handling. It explicitly rejects `prompt_candidates.token_ids` with `INVALID_ARGUMENT` because the shared engine-core protocol cannot safely represent separate prompt candidate IDs:

```rust
"prompt_candidates token_ids selector is not supported"
```

Third, the lower sampler path is fixed-width and has a defensive runtime check. Allowing Rust gRPC to bypass the northbound validator and rely on a late worker exception is the unsafe state; the correct behavior is early request validation.

This is also distinct from the previously submitted Rust gRPC prompt token-ID range issue (`this report` / `GHSA-w2v2-5662-8v5q`). That report covers out-of-vocabulary prompt IDs submitted as the request prompt. This report covers the output-logprob candidate selector lowering into `SamplingParams.logprob_token_ids` with no count, vocabulary, or consistency validation.

## Impact

An attacker who can reach a vLLM Rust gRPC endpoint can submit a generation request with `output_logprobs = true` and `output_candidates.token_ids` containing more than 128 IDs.

Expected impact is a single-request denial of service for affected deployments: the request is accepted by the Rust gRPC frontend, reaches the engine logprob sampler state, and can terminate EngineCore. Fixed behavior should be an immediate `INVALID_ARGUMENT` response before any engine request is submitted.

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.

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-770`: Allocation of Resources Without Limits or Throttling

## Suggested Fix

Validate `ResponseOptions.output_candidates.token_ids` before assigning `params.logprob_token_ids`.

Recommended patch shape:

- define or import a Rust-side equivalent of `MAX_LOGPROB_TOKEN_IDS = 128`;
- reject `output_candidates.token_ids` when `ids.len() > MAX_LOGPROB_TOKEN_IDS`;
- range-check each supplied token ID against the served model vocabulary;
- keep `params.logprobs` consistent with the explicit ID count, or reject inconsistent combinations instead of lowering them;
- return `Status::invalid_argument(...)` from the gRPC converter before any engine request is submitted; and
- add unary `Generate` and streaming `GenerateStream` regression tests for 128 accepted IDs, 129 rejected IDs, and out-of-vocabulary candidate IDs.

The same fix should preserve the existing `prompt_candidates.token_ids` rejection unless maintainers add a safe protocol representation for it.

## References

- vLLM security policy: `https://github.com/vllm-project/vllm/security`
- vLLM vulnerability management: `https://docs.vllm.ai/en/latest/contributing/vulnerability_management/`
- vLLM security guide: `https://docs.vllm.ai/en/latest/usage/security/`
- vLLM `SamplingParams` API docs: `https://docs.vllm.ai/en/latest/api/vllm/sampling_params/`
- vLLM logprob sampler API docs: `https://docs.vllm.ai/en/stable/api/vllm/v1/worker/gpu/sample/logprob/`

## Appendix: Duplicate Review

I checked local vLLM submission bundles, private-visible vLLM advisories, and GitHub PR/issue search for `logprob_token_ids`, `CandidateTokens`, `output_candidates`, `ResponseOptions`, and gRPC logprob variants.

Nearby but distinct:

- `this report` / `GHSA-w2v2-5662-8v5q`: Rust gRPC accepts out-of-vocabulary `prompt.token_ids`. That is a prompt-token range-validation bug. It does not package the `output_candidates.token_ids` length bypass or `LogprobTokenIdsState` fixed-row failure path.
- `this report` / `GHSA-mmrv-74wf-6pxm`: Rust frontend sampler-control length bounds remain missing after PR `#45390`. This report is narrower and covers the gRPC response-candidate selector that bypasses the explicit `logprob_token_ids` Python validator.

The GitHub advisory API returned only the prior this report gRPC prompt-token advisory for these terms. Public GitHub PR and issue searches for the exact candidate-selector/logprob-token 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_
