---
id: "GHSA-qff2-492f-9fm4"
page: "https://infrastack.ai/live/GHSA-qff2-492f-9fm4"
kind: "vulnerability"
aliases: ["GHSA-qff2-492f-9fm4"]
title: "Rust HTTP/gRPC stop_token_ids bypass Python vocab-bound fix 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-129", "cwe-20", "cwe-400", "inference", "python", "self-hosted", "vllm"]
not_affected_if: ["auth-required", "default-config"]
published_at: "2026-09-12T14:43:57+00:00"
first_seen_at: "2026-11-09T22:53:23+00:00"
updated_at: "2026-11-09T23:01:09+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/vllm-project/vllm/security/advisories/GHSA-qff2-492f-9fm4", "first_seen_at": "2026-11-09T22:53:23+00:00"}]
exploit_sources: []
---

# Rust HTTP/gRPC stop_token_ids bypass Python vocab-bound fix and can terminate EngineCore

Link for people: https://infrastack.ai/live/GHSA-qff2-492f-9fm4 (this .md is for agents)

A request to vLLM's Rust HTTP or gRPC frontend with a stop_token_ids value outside the model vocabulary and min_tokens above zero is passed to the GPU logits code, which triggers a CUDA device-side assertion. The engine then enters a fatal state and needs a full service restart.

_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` and whether it falls in `>= 0.22.0, < 0.24.0`.
- Check whether the Rust HTTP or Rust gRPC frontend is serving requests on a network-reachable endpoint.

## Fix

- `vllm` (PyPI): upgrade to `0.24.0`.

## Timeline

- 2026-11-09T22:53:23+00:00 ghsa-repo https://github.com/vllm-project/vllm/security/advisories/GHSA-qff2-492f-9fm4

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/vllm-project/vllm/security/advisories/GHSA-qff2-492f-9fm4

## From the sources

## Summary

Public PR `#44968` fixes a vLLM denial-of-service condition where user-supplied `stop_token_ids` outside `[0, vocab_size)` can reach `MinTokensLogitsProcessor` when `min_tokens > 0`, get used as CUDA tensor indices in `logits.index_put_`, trigger a device-side assertion, and leave EngineCore in a fatal state requiring restart.

That PR currently changes only the Python request path. Current Rust HTTP and Rust gRPC frontends expose the same `stop_token_ids` control, validate nearby token-id fields such as `prompt`, `allowed_token_ids`, and `logit_bias`, but do not vocab-bound `stop_token_ids`. The Rust paths forward those IDs into `EngineCoreSamplingParams.all_stop_token_ids`, which is the set consumed by the same Python min-tokens logits processor.

This is a sibling-callsite/incomplete-fix variant: after the Python-side fix, Rust OpenAI-compatible HTTP and Rust gRPC callers can still submit an out-of-vocabulary stop token with `min_tokens > 0` and reach the fatal CUDA indexing sink.

## Technical Details

The Python request path currently only checks that `stop_token_ids` are integers:

```python
assert isinstance(self.stop_token_ids, list)
if not all(isinstance(st_id, int) for st_id in self.stop_token_ids):
    raise ValueError(...)
```

PR `#44968` adds the missing Python vocab-bound validator. The same PR explains the fatal sink:

```text
stop_token_ids with out-of-range values crash EngineCore via a CUDA
device-side assertion.
```

The shared runtime path is in `MinTokensLogitsProcessor`. When `min_tokens` is active, it takes `params.all_stop_token_ids`:

```python
return min_tokens, output_tok_ids, params.all_stop_token_ids
```

and later indexes logits with those token IDs:

```python
logits.index_put_(self.logits_slice, self.neg_inf_tensor)
```

and on the spec-decode path:

```python
logits.index_put_(logits_slice, self.neg_inf_tensor)
```

The Rust HTTP OpenAI routes expose and forward the same field:

```rust
pub stop_token_ids: Option<Vec<u32>>,
...
stop_token_ids: request.stop_token_ids,
```

The Rust validators already check neighboring token-id fields, but not `stop_token_ids`:

```rust
validate_prompt_token_ids(&request.prompt, prompt_bound)?;
validate_allowed_token_ids(request.allowed_token_ids.as_deref(), tokenizer_vocab_size)?;
validate_logit_bias(request.logit_bias.as_ref(), model_vocab_size.unwrap_or(usize::MAX))
```

Chat completions has the same gap:

```rust
validate_allowed_token_ids(request.allowed_token_ids.as_deref(), tokenizer_vocab_size)?;
validate_logit_bias(request.logit_bias.as_ref(), model_vocab_size.unwrap_or(usize::MAX))
```

Rust gRPC also forwards caller-supplied stopping criteria without a range check:

```rust
if !s.stop_token_ids.is_empty() {
    params.stop_token_ids = Some(s.stop_token_ids.clone());
}
```

Finally, Rust text lowering copies those IDs into the engine-facing stop set:

```rust
let mut all_stop_token_ids = BTreeSet::from_iter(stop_token_ids.iter().copied());
...
EngineCoreSamplingParams {
    stop_token_ids,
    all_stop_token_ids,
    ...
}
```

The issue is therefore not that `stop_token_ids` exist. The issue is that Rust frontend paths bypass the same vocab-bound validation that Python is adding before the exact fatal sink documented in PR `#44968`.

vLLM intentionally supports stop token IDs, and `min_tokens` intentionally masks stop tokens until the minimum length is reached. It does not intend arbitrary client token IDs outside the served vocabulary to be used as GPU tensor indices.

The intended contract is visible in three places:

- PR `#44968` adds `_validate_stop_token_ids()` to reject IDs outside `[0, vocab_size)` before the request reaches GPU logits processing.
- Rust HTTP validation already rejects out-of-vocab prompt IDs, `allowed_token_ids`, and `logit_bias` keys.
- The public PR's test plan includes an E2E test for `stop_token_ids_out_of_vocab`, confirming out-of-vocab stop IDs should be a rejected request, not an EngineCore crash.

The Rust HTTP and gRPC frontends construct engine-facing sampling state outside the Python protocol model path, so they must enforce the same safety contract before submitting a request.

## Impact

An attacker who can reach an affected Rust HTTP or Rust gRPC vLLM endpoint can send a small generation request with `min_tokens = 1` and a `stop_token_ids` entry greater than or equal to the served model vocabulary size.

Expected impact is single-request denial of service for affected deployments: the Rust frontend accepts and lowers the request, and the shared Python EngineCore min-tokens processor can use the out-of-range token ID as a GPU logits tensor index. Public PR `#44968` states that this condition puts EngineCore into a fatal state and requires full service restart.

This report does not claim code execution, data access, model compromise, or persistence. It is an availability issue caused by missing Rust-side input validation for a field already identified as security-sensitive.

Suggested severity: High if the Rust HTTP or 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 endpoint 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
```

The vLLM security policy states that DoS or partial disruption without code execution or data breach is generally Moderate, while High is appropriate for higher-impact issues or privileged network surfaces. Because the public root cause describes fatal EngineCore restart from a single malformed request, the technical impact is high availability; maintainers can choose PR:N or PR:L based on their expected Rust deployment exposure.

Suggested CWEs:

- `CWE-20`: Improper Input Validation
- `CWE-129`: Improper Validation of Array Index
- `CWE-400`: Uncontrolled Resource Consumption

## Suggested Fix

Apply the same vocab-bound validation model to Rust HTTP and Rust gRPC before request lowering:

- add a Rust helper equivalent to Python `_validate_stop_token_ids()`;
- validate every `stop_token_ids` entry against the served vocabulary boundary when `stop_token_ids` is present;
- apply it in both Rust OpenAI completions and chat completions validators;
- apply equivalent validation for gRPC `StoppingCriteria.stop_token_ids` before building `SamplingParams`;
- reject with `ApiError` or `Status::invalid_argument(...)` before submitting an engine request;
- add regression tests for valid in-vocab IDs and invalid out-of-vocab IDs on:
- Rust HTTP `/v1/completions`;
- Rust HTTP `/v1/chat/completions`;
- Rust gRPC `Generate`;
- Rust gRPC `GenerateStream`; and
- `min_tokens > 0`, which is the documented fatal sink precondition.

Keeping the only fix in Python `SamplingParams.verify()` is insufficient for Rust frontend deployments because Rust constructs engine-facing sampling state directly.

## References

- vLLM security policy: `https://github.com/vllm-project/vllm/security/policy`
- vLLM vulnerability management: `https://docs.vllm.ai/en/latest/contributing/vulnerability_management.html`
- vLLM security guide: `https://docs.vllm.ai/en/latest/usage/security.html`
- vLLM PR `#44968`: `https://github.com/vllm-project/vllm/pull/44968`

## Appendix: Duplicate Review

This is distinct from prior local/private submissions:

- `this report` / `GHSA-7rx8-56c4-pfh6`: Python OpenAI request parameters can exceed fixed sampler array budgets; it covers list length, including more than 128 stop token IDs. It does not cover a single out-of-vocabulary stop token reaching the CUDA index sink through Rust frontends.
- `this report` / `GHSA-w2v2-5662-8v5q`: Rust gRPC accepts out-of-vocabulary prompt token IDs. That is a prompt-token embedding path, not `stop_token_ids` plus `MinTokensLogitsProcessor`.
- `this report` / `GHSA-mmrv-74wf-6pxm`: Rust frontend remains missing sampler-con

---

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