CVE-2026-105757: vLLM Denial of Service Vulnerability

MODERATE
Advisory Identifier: GHSA-85xf-c7hm-whqw
CVSS 6.5
CWE-755 CWE-20 CWE-248
Published
Advisory Summary

CVE-2026-105757 is a MODERATE (CVSS 6.5) denial of service vulnerability (CWE-755: Improper Handling of Exceptional Conditions) affecting vLLM libraries in the PyPI ecosystem. The issue involves structured-output request errors escape the request boundary and terminate the shared enginecore — engine-fatal denial of service (3 sites). Remediation is available by upgrading affected packages to version 0.30.0.

CVE-2026-105757 at a Glance

CVE Identifier CVE-2026-105757
GHSA Identifier GHSA-85xf-c7hm-whqw
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-755: Improper Handling of Exceptional Conditions
Affected Product vLLM
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 6.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-105757

To remediate this vulnerability, upgrade all affected vLLM dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

vllm fixed: 0.30.0
pip install vllm>=0.30.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show vllm

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-105757, inspect your installed packages using pip:

pip show vllm

Analyze the terminal output to locate any version of vLLM in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.30.0), your application is affected.

Note: The pip show vllm command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-755: Improper Handling of Exceptional Conditions, where the product does not handle or incorrectly handles an exceptional condition. In vLLM, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Affected - **Ecosystem / package:** pip / `vllm` - **Affected versions:** vLLM ≤ 0.25.1 (confirmed on 0.25.1, commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34)). The lower bound predates 0.25.1; maintainers can confirm how far back each path reaches. ## Summary Three structured-output request paths let an ordinary request reach a condition that raises an **uncaught, engine-fatal** exception instead of a per-request validation error. The failure is not confined to the request that caused it: it escapes into `EngineCore`'s busy loop and triggers a fatal `_send_engine_dead()`, so one malformed structured-output request denies service to all concurrent and subsequent tenants of that engine. The requests are ordinary API calls; the client does not need any privileged configuration beyond the (often default) structured-output feature. The shared root cause is the absence of a per-request exception boundary around structured-output grammar/token handling: a value that should fail as a request-scoped validation error instead propagates as an uncaught exception (or bypasses frontend validation entirely) and reaches the scheduler/engine loop, which treats the failure as fatal. These sites are distinct from the published fixes for [GHSA-6qc9-v4r8-22xg](https://github.com/vllm-project/vllm/security/advisories/GHSA-6qc9-v4r8-22xg) and [GHSA-8wr5-jm2h-8r4f](https://github.com/vllm-project/vllm/security/advisories/GHSA-8wr5-jm2h-8r4f): GHSA-6qc9 (PR #17623) added frontend json-schema/regex/type validation but the completion check on v0.25.1 still catches only `TimeoutError`, so Site 1's valid duplicate-root EBNF via the latched xgrammar backend still re-raises and kills EngineCore; GHSA-8wr5 (PR #44744) fixes recovered-token state in the eagle spec-decode path and explicitly does not treat trailing `-1` padding as the fault, so Site 2's `-1` reaching `backend_guidance.validate_tokens()` via `ngram_gpu` survives it. ## Affected code Links pinned to the confirmed commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34) (v0.25.1). The three sites are: **Site 1 — latched-backend compile exception escapes.** `StructuredOutputManager` permanently latches one backend on the first structured-output request and always compiles through it, ignoring the per-request `auto` backend selection. A grammar that `auto` accepts only via a fallback backend (e.g. a duplicate-root EBNF) then makes the latched xgrammar compiler raise; the exception is stored in the grammar `Future`, and the completion check catches only `TimeoutError`, so `Future.result()` re-raises during scheduler promotion and kills EngineCore. - Backend defaults to `"auto"`: [`vllm/config/structured_outputs.py#L21`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/config/structured_outputs.py#L21). - The `auto` validator tries xgrammar and silently falls back, recording the resolved backend on the request: [`vllm/sampling_params.py#L1004-L1035`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/sampling_params.py#L1004-L1035) (fields at [`#L85-L88`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/sampling_params.py#L85-L88)). - The manager latches one backend and always compiles through it: [`vllm/v1/structured_output/__init__.py#L127-L159`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/__init__.py#L127-L159) and `_create_grammar()` at [`#L173-L184`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/__init__.py#L173-L184). - The xgrammar sink calls the native compiler unguarded: [`vllm/v1/structured_output/backend_xgrammar.py#L78-L110`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_xgrammar.py#L78-L110) (grammar path at [`#L90`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_xgrammar.py#L90)). - The completion check catches only `TimeoutError`: [`vllm/v1/structured_output/request.py#L48-L63`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L48-L63) (`result(timeout=0.0001)` at [`#L55`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L55), `except TimeoutError` at [`#L57`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L57)). - Scheduler promotion dereferences the grammar with no exception boundary: [`vllm/v1/core/sched/scheduler.py#L2441-L2460`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L2441-L2460) (reached from [`schedule()` at #L396](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L396)). - The uncaught exception is treated as fatal: [`vllm/v1/engine/core.py#L1229-L1234`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1229-L1234) (`_send_engine_dead()` at [`#L1470`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1470)). The escaping-exception sink: the completion check only handles `TimeoutError`, so any compile exception stored in the `Future` re-raises out of `result()` during scheduler promotion. ```python # vllm/v1/structured_output/request.py Lines 48-59 def _check_grammar_completion(self) -> bool: # NOTE: We have to lazy import to gate circular imports from vllm.v1.request import RequestStatus if isinstance(self._grammar, Future): try: # We will check whether the future is ready within 100 us self._grammar = self._grammar.result(timeout=0.0001) self.status = RequestStatus.WAITING except TimeoutError: return False return True ``` **Site 2 — guidance treats `ngram_gpu` `-1` padding as a token id.** The `ngram_gpu` proposer pads a fixed-width draft-token row with `-1` and separately records `num_valid_draft_tokens`, but the scheduler forwards the untrimmed padded row into `GuidanceGrammar.validate_tokens()`, which passes `-1` to the native `llguidance` matcher; the matcher cannot convert a negative value to its unsigned token type and raises an `OverflowError`, uncaught and fatal. - The proposer fills with `-1` and computes `num_valid_draft_tokens`: [`vllm/v1/spec_decode/ngram_proposer_gpu.py#L189-L209`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/spec_decode/ngram_proposer_gpu.py#L189-L209). - `_get_draft_token_ids_cpu()` materializes the full padded row, not trimmed to the valid count: [`vllm/v1/worker/gpu_model_runner.py#L4835-L4849`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/worker/gpu_model_runner.py#L4835-L4849). - Two scheduler call sites pass the padded row into `validate_tokens(...)`: [`vllm/v1/core/sched/scheduler.py#L1967`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L1967) and [`#L1997`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L1997). - The sink has no negative-value filter: [`vllm/v1/structured_output/backend_guidance.py#L181-L192`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_guidance.py#L181-L192) (`validate_tokens(tokens)` at [`#L192`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_guidance.py#L192)). - The uncaught `OverflowError` is treated as fatal: [`vllm/v1/engine/core.py#L1229-L1234`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1229-L1234). The sink passes the padded row straight to the native matcher with no lower-bound check on token ids: ```python # vllm/v1/structured_output/backend_guidance.py Lines 181-196 def validate_tokens(self, tokens: list[int]) -> list[int]: """Checks if the list of tokens are accepted by the parser in sequence. Will not advance the parser. Returns the prefix list of tokens that are accepted by the parser. """ if len(tokens) == 0: return [] if self.ll_matcher.is_stopped(): return [] num_tokens = self.ll_matcher.validate_tokens(tokens) self.check_error() return tokens[:num_tokens] ``` **Site 3 — Rust frontend accepts an empty structured-output value the Python frontend rejects.** The opt-in Rust frontend admits an empty `structured_outputs.json` / `structured_outputs.grammar` string that the Python frontend rejects; the empty value passes through the wire-params conversion without validation, reaches the engine, and marks EngineCore dead. - `impl TryFrom<WireStructuredOutputsParams> for StructuredOutputsParams`: [`rust/src/engine-core-client/src/protocol/structured_outputs.rs#L135-L163`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/rust/src/engine-core-client/src/protocol/structured_outputs.rs#L135-L163) — `try_from` at L138 maps `raw.json` to a `Json` constraint (L159) and `raw.grammar` to a `Grammar` constraint (L162) with no non-empty check, so an empty value is forwarded unchanged. `WireStructuredOutputsParams` is defined at [`#L115`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/rust/src/engine-core-client/src/protocol/structured_outputs.rs#L115). The conversion maps each field to a constraint with no non-empty check, so an empty `json`/`grammar` string is forwarded unchanged: ```rust // rust/src/engine-core-client/src/protocol/structured_outputs.rs Lines 135-162 impl TryFrom<WireStructuredOutputsParams> for StructuredOutputsParams { type Error = Error; fn try_from(raw: WireStructuredOutputsParams) -> Result<Self> { use StructuredOutputConstraint::*; let mut constraint = None; macro_rules! insert_constraint { ($name:literal, $value:expr) => { if let Some(value) = $value { if let Some((existing, _)) = constraint { return Err(Error::InvalidStructuredOutputsParams { message: format!( "multiple structured output constraints specified: {existing}, {}", $name ), }); } constraint = Some(($name, value)); } }; } insert_constraint!("json", raw.json.map(Json)); insert_constraint!("regex", raw.regex.map(Regex)); insert_constraint!("choice", raw.choice.map(Choice)); insert_constraint!("grammar", raw.grammar.map(Grammar)); ``` ## Impact Any client able to send an ordinary structured-output request (two requests for Site 1) can terminate the shared EngineCore, denying service to all tenants of that engine instance. Availability only; no code execution, memory corruption, or data disclosure. - **Site 1** affects the default `"auto"` structured-output backend; no speculative decoding or non-default configuration is required. - **Site 2** requires a deployment using the `guidance` backend together with `ngram_gpu` speculative decoding; the `-1` sentinel is produced by the proposer itself, so an ordinary constrained request suffices — the client does not craft the negative token. - **Site 3** requires the opt-in Rust frontend; a single request with an empty constraint value is enough. API-key middleware narrows the attacker from unauthenticated to authenticated but does not restore the missing validation. ## Suggested Fix Convert structured-output failures into request-scoped errors at the boundary. Each site is a distinct fix shape; the core hunk for each is below. **Site 1** — resolve the backend per request instead of latching one process-wide, so a grammar is always compiled with the backend that `auto` actually selected for it (and a compile failure is that request's error, not the engine's): ```python # vllm/v1/structured_output/__init__.py — grammar_init(), replacing the latched-backend path backend = self._get_or_create_backend(request.structured_output_request.backend) if self._use_async_grammar_compilation: grammar = self.executor.submit(self._create_grammar, request, backend) else: grammar = self._create_grammar(request, backend) ``` **Site 2** — drop negative sentinels before handing the row to the native matcher, inside `validate_tokens()`: ```python # vllm/v1/structured_output/backend_guidance.py — validate_tokens(), before the ll_matcher call for i, token in enumerate(tokens): if token < 0: tokens = tokens[:i] break if len(tokens) == 0: return [] ``` **Site 3** — reject empty `json`/`grammar` strings in the Rust wire-params conversion, matching the Python frontend, before the value reaches the engine: ```rust // rust/src/engine-core-client/src/protocol/structured_outputs.rs — try_from(), before constraint mapping if matches!(&raw.json, Some(Value::String(value)) if value.trim().is_empty()) { return Err(Error::InvalidStructuredOutputsParams { message: "structured_outputs.json cannot be an empty string".to_string(), }); } if matches!(&raw.grammar, Some(value) if value.trim().is_empty()) { return Err(Error::InvalidStructuredOutputsParams { message: "structured_outputs.grammar cannot be an empty string".to_string(), }); } ``` The three sites share one root cause and the same fix shape (bound the failure to the request), so they are filed as a single advisory; happy to split into per-component advisories if the maintainers prefer. Each fix carries a regression test. ## Credit **Reported by:** Patch the Planet (Trail of Bits + OpenAI collaboration) These vulnerabilities were discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative. --- **Proposed fix:** a fix for this issue is proposed in a public pull request: https://github.com/vllm-project/vllm/pull/51450

CVE-2026-105757 Affected vLLM Versions

1 component(s)
Fixed Versions:
0.30.0
Product / Package Ecosystem Affected Versions Fixed Version
vllm
PyPI
< 0.30.0
0.30.0

References

Frequently Asked Questions

What is CVE-2026-105757?
CVE-2026-105757 is a security vulnerability in vLLM involving Structured-output request errors escape the request boundary and terminate the shared EngineCore — engine-fatal denial of service (3 sites) under CWE-755: Improper Handling of Exceptional Conditions.
Which vLLM versions are affected?
Affected versions include releases prior to 0.30.0 across affected packages: vllm.
What version fixes CVE-2026-105757?
Upgrading to 0.30.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is vllm affected?
Yes, vllm is affected in releases prior to 0.30.0. Upgrade your dependency to 0.30.0 or later.
What is the CVSS score of CVE-2026-105757?
CVE-2026-105757 has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-105757?
CVE-2026-105757 is classified under CWE-755: Improper Handling of Exceptional Conditions (The product does not handle or incorrectly handles an exceptional condition.).
How can I check PyPI dependencies for CVE-2026-105757?
Run the dependency tree command: 'pip show vllm'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-85xf-c7hm-whqw",
  "modified" : "2026-10-05T23:42:44Z",
  "published" : "2026-10-05T23:42:44Z",
  "aliases" : [ "CVE-2026-105757" ],
  "summary" : "vLLM: Structured-output request errors escape the request boundary and terminate the shared EngineCore — engine-fatal denial of service (3 sites)",
  "details" : "## Affected\n\n- **Ecosystem / package:** pip / `vllm`\n- **Affected versions:** vLLM ≤ 0.25.1 (confirmed on 0.25.1, commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34)). The lower bound predates 0.25.1; maintainers can confirm how far back each path reaches.\n\n## Summary\n\nThree structured-output request paths let an ordinary request reach a condition that raises an **uncaught, engine-fatal** exception instead of a per-request validation error. The failure is not confined to the request that caused it: it escapes into `EngineCore`'s busy loop and triggers a fatal `_send_engine_dead()`, so one malformed structured-output request denies service to all concurrent and subsequent tenants of that engine. The requests are ordinary API calls; the client does not need any privileged configuration beyond the (often default) structured-output feature.\n\nThe shared root cause is the absence of a per-request exception boundary around structured-output grammar/token handling: a value that should fail as a request-scoped validation error instead propagates as an uncaught exception (or bypasses frontend validation entirely) and reaches the scheduler/engine loop, which treats the failure as fatal.\n\nThese sites are distinct from the published fixes for [GHSA-6qc9-v4r8-22xg](https://github.com/vllm-project/vllm/security/advisories/GHSA-6qc9-v4r8-22xg) and [GHSA-8wr5-jm2h-8r4f](https://github.com/vllm-project/vllm/security/advisories/GHSA-8wr5-jm2h-8r4f): GHSA-6qc9 (PR #17623) added frontend json-schema/regex/type validation but the completion check on v0.25.1 still catches only `TimeoutError`, so Site 1's valid duplicate-root EBNF via the latched xgrammar backend still re-raises and kills EngineCore; GHSA-8wr5 (PR #44744) fixes recovered-token state in the eagle spec-decode path and explicitly does not treat trailing `-1` padding as the fault, so Site 2's `-1` reaching `backend_guidance.validate_tokens()` via `ngram_gpu` survives it.\n\n## Affected code\n\nLinks pinned to the confirmed commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34) (v0.25.1). The three sites are:\n\n**Site 1 — latched-backend compile exception escapes.** `StructuredOutputManager` permanently latches one backend on the first structured-output request and always compiles through it, ignoring the per-request `auto` backend selection. A grammar that `auto` accepts only via a fallback backend (e.g. a duplicate-root EBNF) then makes the latched xgrammar compiler raise; the exception is stored in the grammar `Future`, and the completion check catches only `TimeoutError`, so `Future.result()` re-raises during scheduler promotion and kills EngineCore.\n\n- Backend defaults to `\"auto\"`: [`vllm/config/structured_outputs.py#L21`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/config/structured_outputs.py#L21).\n- The `auto` validator tries xgrammar and silently falls back, recording the resolved backend on the request: [`vllm/sampling_params.py#L1004-L1035`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/sampling_params.py#L1004-L1035) (fields at [`#L85-L88`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/sampling_params.py#L85-L88)).\n- The manager latches one backend and always compiles through it: [`vllm/v1/structured_output/__init__.py#L127-L159`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/__init__.py#L127-L159) and `_create_grammar()` at [`#L173-L184`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/__init__.py#L173-L184).\n- The xgrammar sink calls the native compiler unguarded: [`vllm/v1/structured_output/backend_xgrammar.py#L78-L110`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_xgrammar.py#L78-L110) (grammar path at [`#L90`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_xgrammar.py#L90)).\n- The completion check catches only `TimeoutError`: [`vllm/v1/structured_output/request.py#L48-L63`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L48-L63) (`result(timeout=0.0001)` at [`#L55`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L55), `except TimeoutError` at [`#L57`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L57)).\n- Scheduler promotion dereferences the grammar with no exception boundary: [`vllm/v1/core/sched/scheduler.py#L2441-L2460`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L2441-L2460) (reached from [`schedule()` at #L396](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L396)).\n- The uncaught exception is treated as fatal: [`vllm/v1/engine/core.py#L1229-L1234`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1229-L1234) (`_send_engine_dead()` at [`#L1470`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1470)).\n\nThe escaping-exception sink: the completion check only handles `TimeoutError`, so any compile exception stored in the `Future` re-raises out of `result()` during scheduler promotion.\n\n```python\n# vllm/v1/structured_output/request.py Lines 48-59\n    def _check_grammar_completion(self) -> bool:\n        # NOTE: We have to lazy import to gate circular imports\n        from vllm.v1.request import RequestStatus\n\n        if isinstance(self._grammar, Future):\n            try:\n                # We will check whether the future is ready within 100 us\n                self._grammar = self._grammar.result(timeout=0.0001)\n                self.status = RequestStatus.WAITING\n            except TimeoutError:\n                return False\n        return True\n```\n\n**Site 2 — guidance treats `ngram_gpu` `-1` padding as a token id.** The `ngram_gpu` proposer pads a fixed-width draft-token row with `-1` and separately records `num_valid_draft_tokens`, but the scheduler forwards the untrimmed padded row into `GuidanceGrammar.validate_tokens()`, which passes `-1` to the native `llguidance` matcher; the matcher cannot convert a negative value to its unsigned token type and raises an `OverflowError`, uncaught and fatal.\n\n- The proposer fills with `-1` and computes `num_valid_draft_tokens`: [`vllm/v1/spec_decode/ngram_proposer_gpu.py#L189-L209`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/spec_decode/ngram_proposer_gpu.py#L189-L209).\n- `_get_draft_token_ids_cpu()` materializes the full padded row, not trimmed to the valid count: [`vllm/v1/worker/gpu_model_runner.py#L4835-L4849`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/worker/gpu_model_runner.py#L4835-L4849).\n- Two scheduler call sites pass the padded row into `validate_tokens(...)`: [`vllm/v1/core/sched/scheduler.py#L1967`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L1967) and [`#L1997`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L1997).\n- The sink has no negative-value filter: [`vllm/v1/structured_output/backend_guidance.py#L181-L192`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_guidance.py#L181-L192) (`validate_tokens(tokens)` at [`#L192`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_guidance.py#L192)).\n- The uncaught `OverflowError` is treated as fatal: [`vllm/v1/engine/core.py#L1229-L1234`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1229-L1234).\n\nThe sink passes the padded row straight to the native matcher with no lower-bound check on token ids:\n\n```python\n# vllm/v1/structured_output/backend_guidance.py Lines 181-196\n    def validate_tokens(self, tokens: list[int]) -> list[int]:\n        \"\"\"Checks if the list of tokens are accepted by the parser in sequence.\n        Will not advance the parser.\n\n        Returns the prefix list of tokens that are accepted by the parser.\n        \"\"\"\n        if len(tokens) == 0:\n            return []\n        if self.ll_matcher.is_stopped():\n            return []\n\n        num_tokens = self.ll_matcher.validate_tokens(tokens)\n\n        self.check_error()\n\n        return tokens[:num_tokens]\n```\n\n**Site 3 — Rust frontend accepts an empty structured-output value the Python frontend rejects.** The opt-in Rust frontend admits an empty `structured_outputs.json` / `structured_outputs.grammar` string that the Python frontend rejects; the empty value passes through the wire-params conversion without validation, reaches the engine, and marks EngineCore dead.\n\n- `impl TryFrom<WireStructuredOutputsParams> for StructuredOutputsParams`: [`rust/src/engine-core-client/src/protocol/structured_outputs.rs#L135-L163`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/rust/src/engine-core-client/src/protocol/structured_outputs.rs#L135-L163) — `try_from` at L138 maps `raw.json` to a `Json` constraint (L159) and `raw.grammar` to a `Grammar` constraint (L162) with no non-empty check, so an empty value is forwarded unchanged. `WireStructuredOutputsParams` is defined at [`#L115`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/rust/src/engine-core-client/src/protocol/structured_outputs.rs#L115).\n\nThe conversion maps each field to a constraint with no non-empty check, so an empty `json`/`grammar` string is forwarded unchanged:\n\n```rust\n// rust/src/engine-core-client/src/protocol/structured_outputs.rs Lines 135-162\nimpl TryFrom<WireStructuredOutputsParams> for StructuredOutputsParams {\n    type Error = Error;\n\n    fn try_from(raw: WireStructuredOutputsParams) -> Result<Self> {\n        use StructuredOutputConstraint::*;\n\n        let mut constraint = None;\n\n        macro_rules! insert_constraint {\n            ($name:literal, $value:expr) => {\n                if let Some(value) = $value {\n                    if let Some((existing, _)) = constraint {\n                        return Err(Error::InvalidStructuredOutputsParams {\n                            message: format!(\n                                \"multiple structured output constraints specified: {existing}, {}\",\n                                $name\n                            ),\n                        });\n                    }\n                    constraint = Some(($name, value));\n                }\n            };\n        }\n\n        insert_constraint!(\"json\", raw.json.map(Json));\n        insert_constraint!(\"regex\", raw.regex.map(Regex));\n        insert_constraint!(\"choice\", raw.choice.map(Choice));\n        insert_constraint!(\"grammar\", raw.grammar.map(Grammar));\n```\n\n## Impact\n\nAny client able to send an ordinary structured-output request (two requests for Site 1) can terminate the shared EngineCore, denying service to all tenants of that engine instance. Availability only; no code execution, memory corruption, or data disclosure.\n\n- **Site 1** affects the default `\"auto\"` structured-output backend; no speculative decoding or non-default configuration is required.\n- **Site 2** requires a deployment using the `guidance` backend together with `ngram_gpu` speculative decoding; the `-1` sentinel is produced by the proposer itself, so an ordinary constrained request suffices — the client does not craft the negative token.\n- **Site 3** requires the opt-in Rust frontend; a single request with an empty constraint value is enough. API-key middleware narrows the attacker from unauthenticated to authenticated but does not restore the missing validation.\n\n\n## Suggested Fix\n\nConvert structured-output failures into request-scoped errors at the boundary. Each site is a distinct fix shape; the core hunk for each is below.\n\n**Site 1** — resolve the backend per request instead of latching one process-wide, so a grammar is always compiled with the backend that `auto` actually selected for it (and a compile failure is that request's error, not the engine's):\n\n```python\n# vllm/v1/structured_output/__init__.py — grammar_init(), replacing the latched-backend path\nbackend = self._get_or_create_backend(request.structured_output_request.backend)\nif self._use_async_grammar_compilation:\n    grammar = self.executor.submit(self._create_grammar, request, backend)\nelse:\n    grammar = self._create_grammar(request, backend)\n```\n\n**Site 2** — drop negative sentinels before handing the row to the native matcher, inside `validate_tokens()`:\n\n```python\n# vllm/v1/structured_output/backend_guidance.py — validate_tokens(), before the ll_matcher call\nfor i, token in enumerate(tokens):\n    if token < 0:\n        tokens = tokens[:i]\n        break\nif len(tokens) == 0:\n    return []\n```\n\n**Site 3** — reject empty `json`/`grammar` strings in the Rust wire-params conversion, matching the Python frontend, before the value reaches the engine:\n\n```rust\n// rust/src/engine-core-client/src/protocol/structured_outputs.rs — try_from(), before constraint mapping\nif matches!(&raw.json, Some(Value::String(value)) if value.trim().is_empty()) {\n    return Err(Error::InvalidStructuredOutputsParams {\n        message: \"structured_outputs.json cannot be an empty string\".to_string(),\n    });\n}\nif matches!(&raw.grammar, Some(value) if value.trim().is_empty()) {\n    return Err(Error::InvalidStructuredOutputsParams {\n        message: \"structured_outputs.grammar cannot be an empty string\".to_string(),\n    });\n}\n```\n\nThe three sites share one root cause and the same fix shape (bound the failure to the request), so they are filed as a single advisory; happy to split into per-component advisories if the maintainers prefer. Each fix carries a regression test.\n\n## Credit\n\n**Reported by:** Patch the Planet (Trail of Bits + OpenAI collaboration)\n\nThese vulnerabilities were discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative.\n\n---\n\n**Proposed fix:** a fix for this issue is proposed in a public pull request: https://github.com/vllm-project/vllm/pull/51450",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "vllm"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.30.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/vllm-project/vllm/security/advisories/GHSA-85xf-c7hm-whqw"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/vllm-project/vllm/pull/51450"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/vllm-project/vllm/commit/c55e15a44ec4127832d4a86928a356fdd9e68dbd"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/vllm-project/vllm"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/vllm-project/vllm/releases/tag/v0.30.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-20", "CWE-248", "CWE-755" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-05T23:42:44Z",
    "nvd_published_at" : null
  }
}