GitHub Advisoryadvisory · PyPI
GHSA-2823-qmq8-rwvj vLLM: Loose `cache_salt` validation lets a single request kill EngineCore on LMCache-MP deployments — uncaught downstream `ValueError` denial of service
MEDIUM
Official page ↗
- Aliases
- CVE-2026-105756
- Ecosystem
- PyPI
- Packages
- vllm
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-20, CWE-248
- Reviewed
- 2026-10-05
Affected packages
• PyPI vllm: < 0.30.0 → fixed in 0.30.0
Description
Affected
• Ecosystem / package: pip / `vllm`
• Affected versions: vLLM ≤ 0.25.1 (confirmed on 0.25.1, commit `752a3a504485`). The lower bound predates 0.25.1; maintainers can confirm how far back the loose `cache_salt` validator and unguarded scheduling-path lookup reach.
Summary
vLLM's OpenAI-compatible request models (Completions, Chat Completions, Responses) accept a client-supplied `cache_salt` field and validate it only as "must be a non-empty string" — no character or length restrictions. On a deployment with the built-in LMCache-MP KV connector enabled, that value is stored verbatim on the request tracker and forwarded unguarded as a keyword argument into the scheduler's per-step cache lookup. The downstream LMCache library applies a *stricter* check in `IPCCacheServerKey.__post_init__`, which raises `ValueError` for any `cache_salt` containing `@`, `/`, `\`, or NUL (or longer than 128 characters).
Neither the LMCache-MP connector lookup call site nor `Scheduler.schedule()` wraps that call in a request-scoped `try`/`except`, so the `ValueError` propagates uncaught into `EngineCore`'s top-level handler, which treats any uncaught exception as fatal and kills the whole engine process. A single publicly reachable request with, for example, `cache_salt="/"` therefore takes down the engine for all concurrent users. vLLM's boundary validator is looser than the downstream consumer's, and the gap is never converted into a request-scoped failure on the scheduling path.
Affected code
Links pinned to the confirmed commit `752a3a504485` (v0.25.1):
• The three `check_cache_salt_support` validators require only a non-empty string — no character or length bound: `vllm/entrypoints/openai/completion/protocol.py#L502-L508` (field at `#L172`), `vllm/entrypoints/openai/chat_completion/protocol.py#L913-L919` (field at `#L425`), and `vllm/entrypoints/openai/responses/protocol.py#L459-L465` (field at `#L235`). The loose test itself is at `completion #L503-L505`, `chat_completion #L914-L916`, `responses #L460-L462`.
• Two further request models accept `cache_salt` with the same or weaker checking, and should be hardened at the same time: `vllm/entrypoints/pooling/base/protocol.py#L74-L85` carries the identical non-empty-string-only validator (field at `#L59`), and the token-in-token-out scale-out `GenerateRequest` exposes the field with no `cache_salt` validator at all: `vllm/entrypoints/scale_out/token_in_token_out/protocol.py#L110`.
• The LMCache-MP request tracker stores the salt verbatim: `vllm/distributed/kv_transfer/kv_connector/v1/lmcache_mp_connector.py#L189` (`LMCacheMPRequestTracker`), assignment at `#L223`.
• The lookup call forwards `cache_salt` with no surrounding `try`: `vllm/distributed/kv_transfer/kv_connector/v1/lmcache_mp_connector.py#L733-L774` (`get_num_new_matched_tokens` → `maybe_submit_lookup_request(...)` at `#L770`).
• The scheduler invokes the connector unguarded: `vllm/v1/core/sched/scheduler.py#L739` (`self.connector.get_num_new_matched_tokens(...)`, inside `schedule()` at #L396).
• The generic top-level handler treats the exception as fatal: `vllm/v1/engine/core.py#L1229-L1235` — the `except Exception` in `run_engine_core` (`#L1154`) logs `EngineCore encountered a fatal error.`, calls `_send_engine_dead()`, and re-raises.
• Downstream strict validator (external LMCache library, not vLLM): `IPCCacheServerKey.__post_init__` in `lmcache/v1/multiprocess/custom_types.py` rejects `@ / \` NUL and >128-char salts (`_SALT_FORBIDDEN_CHARS = frozenset("@/\\\x00")`) by raising `ValueError`.
The three OpenAI `check_cache_salt_support` validators are identical; the completion one is representative — a non-empty-string test with no character or length bound:
```python
vllm/entrypoints/openai/completion/protocol.py Lines 503-509
if data.get("cache_salt") is not None and (
not isinstance(data["cache_salt"], str) or not data["cache_salt"]
):
raise VLLMValidationError(
"Parameter 'cache_salt' must be a non-empty string if provided.",
parameter="cache_salt",
)
```
On the scheduling path the connector is invoked with no surrounding `try` — a `ValueError` from the downstream salt check propagates straight out of `schedule()`:
```python
vllm/v1/core/sched/scheduler.py Lines 736-742
# Get externally-cached tokens if using a KVConnector.
if self.connector is not None:
ext_tokens, load_kv_async = (
self.connector.get_num_new_matched_tokens(
request, num_new_local_computed_tokens
)
)
```
`run_engine_core`'s generic handler — the next `except` up the stack — treats that as fatal, marks the engine dead, and re-raises:
```python
vllm/v1/engine/core.py Lines 1229-1235
except Exception as e:
if engine_core is None:
…
Related
Alias · CVE (1)
- CVE-2026-105756 vLLM is an inference and serving engine for large language models. vulnerability
References
- https://github.com/vllm-project/vllm/security/advisories/GHSA-2823-qmq8-rwvj web
- https://github.com/vllm-project/vllm/pull/51444 web
- https://github.com/vllm-project/vllm/commit/e962733e08d10f7ca65dac4df99e116460b8b174 web
- https://github.com/vllm-project/vllm package
- https://github.com/vllm-project/vllm/releases/tag/v0.30.0 web