GHSA-8wr5-jm2h-8r4f CVE-2026-54234 PYSEC-2026-3542
vLLM has Remote DoS via Invalid Recovered Token Reinjection
- Severity
- 7.5 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.62%
- Chance of exploitation in the next 30 days. Higher than 49.54% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-20 Improper Input Validation
- Fix
0.24.0- Upgrade vllm to this version or later
Description
Summary
A frontend-legal multi-request speculative workload can make vLLM produce an out-of-vocabulary recovered token equal to vocab_size, convert that value to -1 when choosing the next live token for a request, and then feed that -1 back into the next drafter input ids. On Qwen3 GPTQ this reaches the worker-side drafting / attention path and crashes the engine with a GPU device-side assert.
The same issue is reachable through the public gRPC request surface by sending a specific overlapping Generate / Abort sequence.
Impact
- A remote client that can send public gRPC generation requests can crash the shared vLLM engine worker
- The triggering request sequence aborts concurrent requests and prevents later requests from completing until the worker is restarted
- In shared deployments, this is a service-wide denial of service for other clients, not just a failure isolated to the attacking requests
- The failure is reproducible, so repeated request sequences can sustain the outage
Affected version
- Confirmed on vLLM
0.17.1 - Earlier and later versions have not been checked yet in this report
Repro model
- Official Hugging Face repo:
- Anyone wants to reproduce the bug with my PoC scripts should download
Qwen3-0.6B-GPTQ-Int8first
Trigger chain
- A legal multi-request speculative workload keeps structured-output state, speculative decoding, overlap, and request cancellation active in the same live engine.
- During rejection sampling, vLLM produces a recovered token equal to the
model
vocab_sizeboundary value. - That recovered token appears in position 0 of the sampled speculative row
for a live request. The same row also contains trailing padding entries
equal to
-1, but those padding entries are not the key fault by themselves. - The next-token preparation step treats the position-0 recovered token as the
real next token for that request and converts that out-of-vocabulary value
to
-1. - The drafter writes that converted
-1back into the live next-step input-id row for the request. - The drafting / embedding / attention path later consumes that live invalid token and the worker crashes on GPU.
Details
Simple example
The important distinction is:
- trailing
-1values in a speculative row can be ordinary padding - the bug appears when the first live token for a request becomes
151936 == vocab_size, and that live token is then converted into-1
In simplified form, the bad transition looks like this:
sampled speculative row:
[151936, -1, -1, -1, ...]
At this point, the trailing -1 values are only padding. The critical problem
is that the first position holds 151936, which is out of vocabulary and is
being treated as the request's real next token.
Then vLLM prepares the next-token buffer:
next_token_ids:
[-1, ...]
Finally, that converted -1 is written back into the live model input ids:
input_ids_after:
[-1, 0, 0, 0, ...]
The crash happens because the live next token became -1 and was later consumed by the drafting / embedding / attention path, not merely because the speculative row contained padded -1 entries.
Trigger path in code
- The workload is frontend-legal. The requests use normal
SamplingParamsfeatures such as structured outputs,stop,bad_words,min_tokens, and streaming overlap. No malformed token-id list is required at the request boundary. - In speculative decoding, the rejection sampler can generate recovered tokens
when drafted tokens are rejected.
On the verified Qwen3 run, the recovered-token trace shows# vllm/v1/sample/rejection_sampler.py def sample_recovered_tokens(...): recovered_token_ids = torch.empty_like(draft_token_ids) sample_recovered_tokens_kernel[(batch_size, max_spec_len)](...) return recovered_token_idsrecovered_token_ids[0] = 151936, which is exactlyvocab_sizefor this checkpoint. - The speculative proposer then prepares the next-token row from the sampled
speculative row.
In the verified trace, this step receives a sampled row beginning with# vllm/v1/spec_decode/eagle.py def prepare_next_token_ids_padded(...): ... eagle_prepare_next_token_padded_kernel[grid]( sampled_token_ids, discard_request_mask, backup_tokens_gpu, next_token_ids, valid_sampled_tokens_count, gpu_input_batch.vocab_size, ... ) return next_token_ids, valid_sampled_tokens_count151936, followed by-1padding. The important point is that151936occupies the first live token position for the request. This step then producesnext_token_ids[0] = -1, meaning the live next token for the request has been converted to-1. - The drafter then rotates the draft input ids and inserts those
next_token_idsback into the live input-id buffer.
In the verified trace, this produces# vllm/v1/spec_decode/eagle.py def set_inputs_first_pass(...): ... self.input_ids[token_indices_to_sample] = next_token_idsinput_ids_after[0] = -1. - The model-side embed path later consumes those input ids.
In the verified trace, this is the first point where the converted# vllm/model_executor/models/qwen2.py def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: return self.embed_tokens(input_ids)-1becomes visible as a real model input. The bug is not merely that the sampled speculative row contained padding-1; the bug is that the live next token for the request became-1and was written back into input ids. - After that point, the visible sink depends on timing and backend state. On
the attached Qwen3 reproducer, the engine commonly dies later in the
drafting / attention path with
CUDA error: device-side assert triggered, for example underflash_attn_varlen_func(...).
Local script breakdown
repro_g4_recovered_minus1_local.py is a standalone local reproducer.
- It reads the Qwen3 checkpoint path from
VLLM_POC_G4_MODELor the built-in/path/to/qwen3placeholder - It creates
EngineCoredirectly without any external helper dependency - It submits one fixed multi-request workload that preserves the same overlap and speculative-decoding state needed for the bug
- It writes:
request_payloads.jsonrepro_config.jsontimeline.jsonresponses.jsonerror.txtrecovered_chain_trace.jsonl
recovered_chain_trace.jsonlis the key attribution artifact. It records the recovered-token chain directly from the standalone reproducer
gRPC script breakdown
repro_g4_recovered_minus1_grpc.py is a standalone public gRPC reproducer.
- It reads the Qwen3 checkpoint path from
VLLM_POC_G4_MODELor the built-in/path/to/qwen3placeholder - It starts a temporary
vllm.entrypoints.grpc_serverprocess - It sends only public
GenerateandAbortRPCs - It submits one fixed overlapping request sequence that preserves the same speculative-decoding state needed for the bug
- After the crash window, it sends one more public
Generateprobe request to confirm that later gRPC requests also fail after the worker dies - It writes:
request_payloads.jsontimeline.jsonserver_command.jsonresponses.jsonpost_crash_probe.jsonserver.stdout.logserver.stderr.log
Observed result
Local repro typically ends with:
- a recovered-token trace showing:
sample_recovered_tokens_return -> recovered_token_ids[0] = 151936prepare_next_token_ids_padded -> next_token_ids[0] = -1set_inputs_first_pass -> input_ids_after[0] = -1embed_input_ids_out_of_range -> input_ids[0] = -1
CUDA error: device-side assert triggered- a fatal engine-side failure
gRPC repro typically ends with:
- the triggering gRPC requests failing with
INTERNAL: EngineCore encountered an issue. See stack trace (above) for the root cause. - server logs showing the worker dies with
CUDA error: device-side assert triggered - a later public probe request also failing after the worker is dead
This demonstrates that the issue is reachable through the public gRPC request surface, not only through a local reproducer.
Log snippets
Local recovered-chain trace
sample_recovered_tokens_return:
recovered_token_ids = [151936, ...]
vocab_size = 151936
prepare_next_token_ids_padded:
sampled_token_ids_head = [[151936, -1, -1, ...], ...]
next_token_ids = [-1, ...]
set_inputs_first_pass:
input_ids_after = [-1, 0, 0, 0, ...]
embed_input_ids_out_of_range:
input_ids = [-1, 0, 0, 0, ...]
gRPC server log
torch.AcceleratorError: CUDA error: device-side assert triggered
...
vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.
...
Error in Generate for request post_crash_probe
vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.
Root cause
This is a speculative-decoding state-handling bug, not an invalid frontend token-id input bug.
The root cause is that a recovered speculative token can become equal to vocab_size, then be selected as the live next token for a request, then be converted to -1, and that converted -1 is still written back into live drafter input ids and later consumed by the drafting / embedding / attention path.
For the Qwen3 checkpoint used here:
151936 == vocab_size
This value should be described as the model vocab_size boundary value, not as a legal token id.
Attachments
The attached bundle for this report should contain:
repro_g4_recovered_minus1_local.pyrepro_g4_recovered_minus1_grpc.py
These two standalone scripts are sufficient to reproduce the issue and its public gRPC reachability.
Fix
A fix for this vulnerability has been merged in: https://github.com/vllm-project/vllm/pull/44744
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| vllm | >= 0.17.1, < 0.24.0 |
0.24.0 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
vllm PyPI → 0.24.0
- pip
pip install --upgrade "vllm>=0.24.0"- poetry
poetry add "vllm>=0.24.0"- uv
uv add "vllm>=0.24.0"
Severity breakdown
CVSS 3.1 7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Weakness
CWE-20: Improper Input Validation
The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.
How to prevent it
- Architecture and Design: Consider using language-theoretic security (LangSec) techniques that characterize inputs using a formal language and build "recognizers" for that language. This effectively requires parsing to be a distinct layer that effectively enforces a boundary between raw input and internal data representations, instead of allowing parser code to be scattered throughout the program, where it could be subject to errors or inconsistencies that create weaknesses. [REF-1109] [REF-1110] [REF-1111]
- Architecture and Design: Use an input validation framework such as Struts or the OWASP ESAPI Validation API. Note that using a framework does not automatically address all input validation problems; be mindful of weaknesses that could arise from misusing the framework itself (CWE-1173).
- Architecture and Design, Implementation: Understand all the potential areas where untrusted inputs can enter the product, including but not limited to: parameters or arguments, cookies, anything read from the network, environment variables, reverse DNS lookups, query results, request headers, URL components, e-mail, files, filenames, databases, and any external systems that provide data to the application. Remember that such inputs may be obtained indirectly through API calls.
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
- Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server. Even though client-side checks provide minimal benefits with respect to server-side security, they are still useful. First, they can support intrusion detection. If the server receives input that should have been rejected by the client, then it may be an indication of an attack. Second, client-side error-checking can provide helpful feedback to the user about the expectations for valid input. Third, there may be a reduction in server-side processing time for accidental input errors, although this is typically a small savings.
- Implementation: When your application combines data from multiple sources, perform the validation after the sources have been combined. The individual data elements may pass the validation step but violate the intended restrictions after they have been combined.
- Implementation: Be especially careful to validate all input when invoking code that crosses language boundaries, such as from an interpreted language to native code. This could create an unexpected interaction between the language boundaries. Ensure that you are not violating any of the expectations of the language with which you are interfacing. For example, even though Java may not be susceptible to buffer overflows, providing a large argument in a call to native code might trigger an overflow.
- Implementation: Directly convert your input type into the expected data type, such as using a conversion function that translates a string into a number. After converting to the expected data type, ensure that the input's values fall within the expected range of allowable values and that multi-field consistencies are maintained.
- Implementation: Inputs should be decoded and canonicalized to the application's current internal representation before being validated (CWE-180, CWE-181). Make sure that your application does not inadvertently decode the same input twice (CWE-174). Such errors could be used to bypass allowlist schemes by introducing dangerous inputs after they have been checked. Use libraries such as the OWASP ESAPI Canonicalization control. Consider performing repeated canonicalization until your input does not change any more. This will avoid double-decoding and similar scenarios, but it might inadvertently modify inputs that are allowed to contain properly-encoded dangerous content.
- Implementation: When exchanging data between components, ensure that both components are using the same character encoding. Ensure that the proper encoding is applied at each interface. Explicitly set the encoding you are using whenever the protocol allows you to do so.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-8397-8qf6-f2gm vLLM: Phi-4 multimodal audio encoder materializes and retains full attention matrices: a single max-length audio OOM-crashes the vLLM engine | Medium | Also affects vllm |
| GHSA-x567-49g5-p4cf vLLM: Responses API bypasses trust_request_chat_template for chat_template_kwargs | Medium | Also affects vllm |
| GHSA-vvpg-rrww-wpmh vLLM: Permissive default CORS origins allow cross-origin requests to the vLLM HTTP server | Medium | Also affects vllm |
| GHSA-q97c-p368-75c4 vLLM: Out-of-vocabulary prompt token id passes input validation and triggers a CUDA device-side assert (engine crash / DoS) | Medium | Also affects vllm |
| GHSA-h3rc-j82p-2c3h vLLM: Untrusted EPS inputs can trigger denial of service or Ghostscript RCE | High | Also affects vllm |
| GHSA-gq5q-89c7-vhvx vLLM DeepSeek-OCR image processing vulnerable to denial of service | Medium | Also affects vllm |
| GHSA-6757-hggm-hp2w vLLM: Public multimodal SHM handle forgery reaches worker-side `pickle.loads()` with insecure serialization disabled | High | Also affects vllm |
| GHSA-542m-9234-8gvf vLLM: DoS via unbounded chat_template Jinja2 rendering exhausts the renderer thread pool | Medium | Also affects vllm |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor