See how vllm compares to other vendors in security performance
Summary
An assert-based security check in vLLM's activation function loading allows any unauthenticated attacker to achieve arbitrary code execution on the server by publishing a malicious HuggingFace model, when vLLM runs in Python optimized mode (python -O or PYTHONOPTIMIZE=1).
Details
vLLM uses an assert statement at vllm/modelexecutor/layers/pooler/activations.py:48 as its sole security control to restrict which activation functions can be loaded from a HuggingFace model's config.json:
python vllm/modelexecutor/layers/pooler/activations.py:35-53 functionname: str | None = None if ( hasattr(config, "sentencetransformers") and "activationfn" in config.sentencetransformers ): functionname = config.sentencetransformers["activationfn"] elif ( hasattr(config, "sbertcedefaultactivationfunction") and config.sbertcedefaultactivationfunction is not None ): functionname = config.sbertcedefaultactivationfunction
if functionname is not None: assert functionname.startswith("torch.nn.modules."), ( "Loading of activation functions is restricted to " "torch.nn.modules for security reasons" ) fn = resolveobjbyqualname(functionname)()
Python's assert statements are stripped at compile time when running in optimized mode (python -O or PYTHONOPTIMIZE=1). When the assert is absent, the attacker-controlled functionname from the model's config.json is passed directly to resolveobjbyqualname() — an unrestricted import gadget:
python def resolveobjbyqualname(qualname: str) -> Any: modulename, objname = qualname.rsplit(".", 1) module = importlib.importmodule(modulename) return getattr(module, objname)
This is the same vulnerability class as CVE-2017-1000433 (pysaml2 assert-based auth bypass), flagged by Bandit B101 and Ruff S101, and the reason Django proactively replaced all assert-based security checks (ticket #32508).
Attacker-controlled input sources: - config.sentencetransformers["activationfn"] (line 40) - config.sbertcedefaultactivationfunction (line 45)
Affected call sites — getactfn() is called via resolveclassifieractfn() from: - vllm/modelexecutor/layers/pooler/seqwise/poolers.py:122 — SequencePooler - vllm/modelexecutor/layers/pooler/tokwise/poolers.py:130 — TokenPooler
Broader systemic risk: resolveobjbyqualname is called from ~20 locations across the codebase with no validation of its own. Any future caller feeding user-controlled input to it without validation creates the same vulnerability class.
Suggested fix: Replace the assert with an explicit conditional raise:
python if not functionname.startswith("torch.nn.modules."): raise ValueError( "Loading of activation functions is restricted to " "torch.nn.modules for security reasons" )
Impact
Arbitrary code execution. A malicious model author publishes a HuggingFace model with a crafted config.json. When a victim loads this model with vLLM running under python -O or PYTHONOPTIMIZE=1, arbitrary code executes during model initialization with the privileges of the vLLM process.
The attack requires: 1. Victim loads a malicious model from HuggingFace (user interaction) 2. vLLM runs under python -O or PYTHONOPTIMIZE=1 (documented in production use) 3. Model uses a cross-encoder architecture (e.g. BERT or RoBERTa with sequence classification)
Coordinated disclosure note: This vulnerability was also reported via huntr.com on April 2, 2026 (https://huntr.com/bounties/dcb05b04-e625-41e7-adbc-bbae0cc2d64c). A GitHub Security Advisory was also filed because it is vLLM's stated preferred disclosure channel per SECURITY.md.
Fix
A fix for this was introduced in this commit: https://github.com/vllm-project/vllm/commit/b3c7ffcab82c2439726f8cb213800f6f38c023d3
Summary
A vulnerability in ASGI web servers and starlette's trust on those web servers enables an authentication bypass of the OpenAI API AuthenticationMiddleware, which was discovered during @x41sec's source code audit. It allows to use the API without providing the configured VLLMAPIKEY or --api-key.
Details
In https://github.com/vllm-project/vllm/blob/v0.14.0/vllm/entrypoints/openai/apiserver.py#L689-L692 the urlpath is taken from the URL, which is reconstructed by starlette based on the request scope.
py from starlette.datastructures import URL, Headers, MutableHeaders, State
...
urlpath = URL(scope=scope).path.removeprefix(rootpath) headers = Headers(scope=scope) if urlpath.startswith("/v1") and not self.verifytoken(headers): response = JSONResponse(content={"error": "Unauthorized"}, statuscode=401) return response(scope, receive, send) return self.app(scope, receive, send)
The request scope includes the request's Host: header and reconstructs the URL as shown below:
py f"{scheme}://{hostheader}{path}"
Neither starlette nor any of the ASGI servers (including uvicorn, which vllm uses) properly filter the Host: header for invalid characters. This allows an attacker to include special URL characters such as / or ? in the Host: header and thereby control the reconstructed URL and it's .path attribute.
FastAPI/starlette's routing uses the HTTP path and does not depend on the parsed url.path attribute, allowing attackers to reach an endpoint via a certain path while providing a different value in the .path.
Impact - Instances of vllm that use an API Key for the OpenAI API and expose the API to attackers. - Instances behind an RFC-conforming web server (such as nginx) are not affected.
vLLM up to and including 0.17.0 allows remote attackers to cause a Denial of Service via memory exhaustion. The AsyncMediaIO.fetchaudio and AsyncMediaIO.fetchimage functions in multimodal/inputs.py fetch user-supplied media URLs using aiohttp and call r.read() without enforcing a maximum response size, allowing an attacker to exhaust server memory by providing a URL to an arbitrarily large file.
A vulnerability was found in vLLM up to 0.19.0. The affected element is the function hasmambalayers of the file vllm/v1/kvcacheinterface.py of the component KV Block Handler. Performing a manipulation results in uninitialized resource. It is possible to initiate the attack remotely. The attack is considered to have high complexity. The exploitability is described as difficult. The exploit has been made public and could be used. The existence of this vulnerability is still disputed at present. The proposed patch did not fix the issue. A 3rd party explains: "The divergence could be explained by a benign and expected vLLM behavior where vLLM server could group concurrent requests together resulting in different input shapes based on varying request arrival time. The differences in grouped input shapes could call different kernels with could produce difference results due to rounding and differences in order of operations. There is an environment variable VLLMBATCHINVARIANT=1 for users that desire to have deterministic output with temperature 0.0."
Summary A Denial of Service vulnerability exists in the vLLM OpenAI-compatible API server. Due to the lack of an upper bound validation on the n parameter in the ChatCompletionRequest and CompletionRequest Pydantic models, an unauthenticated attacker can send a single HTTP request with an astronomically large n value. This completely blocks the Python asyncio event loop and causes immediate Out-Of-Memory crashes by allocating millions of request object copies in the heap before the request even reaches the scheduling queue.
Details The root cause of this vulnerability lies in the missing upper bound checks across the request parsing and asynchronous scheduling layers:
1. Protocol Layer: In vllm/entrypoints/openai/chatcompletion/protocol.py, the n parameter is defined simply as an integer without any pydantic.Field constraints for an upper bound. python class ChatCompletionRequest(OpenAIBaseModel): # Ordered by official OpenAI API documentation # https://platform.openai.com/docs/api/reference/chat/create messages: list[ChatCompletionMessageParam] model: str | None = None frequencypenalty: float | None = 0.0 logitbias: dict[str, float] | None = None logprobs: bool | None = False toplogprobs: int | None = 0 maxtokens: int | None = Field( default=None, deprecated="maxtokens is deprecated in favor of " "the maxcompletiontokens field", ) maxcompletiontokens: int | None = None n: int | None = 1 presencepenalty: float | None = 0.0
1. SamplingParams Layer (Incomplete Validation): When the API request is converted to internal SamplingParams in vllm/samplingparams.py, the verifyargs method only checks the lower bound (self.n < 1), entirely omitting an upper bounds check. python def verifyargs(self) -> None: if not isinstance(self.n, int): raise ValueError(f"n must be an int, but is of type {type(self.n)}") if self.n < 1: raise ValueError(f"n must be at least 1, got {self.n}.")
1. Engine Layer (The OOM Trigger): When the malicious request reaches the core engine (vllm/v1/engine/asyncllm.py), the engine attempts to fan out the request n times to generate identical independent sequences within a synchronous loop. python # Fan out child requests (for n>1). parentrequest = ParentRequest(request) for idx in range(parentparams.n): requestid, childparams = parentrequest.getchildinfo(idx) childrequest = request if idx == parentparams.n - 1 else copy(request) childrequest.requestid = requestid childrequest.samplingparams = childparams await self.addrequest( childrequest, prompttext, parentrequest, idx, queue ) return queue Because Python's asyncio runs on a single thread and event loop, this monolithic for-loop monopolizes the CPU thread. The server stops responding to all other connections (including liveness probes). Simultaneously, the memory allocator is overwhelmed by cloning millions of request object instances via copy(request), driving the host's Resident Set Size (RSS) up by gigabytes per second until the OS OOM-killer terminates the vLLM process.
Impact Vulnerability Type: Resource Exhaustion / Denial of Service
Impacted Parties: - Any individual or organization hosting a public-facing vLLM API server (vllm.entrypoints.openai.apiserver), which happens to be the primary entrypoint for OpenAI-compatible setups. - SaaS / AI-as-a-Service platforms acting as reverse proxies sitting in front of vLLM without strict HTTP body payload validation or rate limitations.
Because this vulnerability exploits the control plane rather than the data plane, an unauthenticated remote attacker can achieve a high success rate in taking down production inference hosts with a single HTTP request. This effectively circumvents any hardware-level capacity planning and conventional bandwidth stress limitations.
Summary
The VideoMediaIO.loadbase64() method at vllm/multimodal/media/video.py:51-62 splits video/jpeg data URLs by comma to extract individual JPEG frames, but does not enforce a frame count limit. The numframes parameter (default: 32), which is enforced by the loadbytes() code path at line 47-48, is completely bypassed in the video/jpeg base64 path. An attacker can send a single API request containing thousands of comma-separated base64-encoded JPEG frames, causing the server to decode all frames into memory and crash with OOM.
Details
Vulnerable code
python video.py:51-62 def loadbase64(self, mediatype: str, data: str) -> tuple[npt.NDArray, dict[str, Any]]: if mediatype.lower() == "video/jpeg": loadframe = partial(self.imageio.loadbase64, "image/jpeg") return np.stack( [np.asarray(loadframe(framedata)) for framedata in data.split(",")] # ^^^^^^^^^^ # Unbounded split — no frame count limit ), {} return self.loadbytes(base64.b64decode(data))
The loadbytes() path (line 47-48) properly delegates to a video loader that respects self.numframes (default 32). The loadbase64("video/jpeg", ...) path bypasses this limit entirely — data.split(",") produces an unbounded list and every frame is decoded into a numpy array.
video/jpeg is part of vLLM's public API
video/jpeg is a vLLM-specific MIME type, not IANA-registered. However it is part of the public API surface:
- encodevideourl() at vllm/multimodal/utils.py:96-108 generates data:video/jpeg;base64,... URLs - Official test suites at tests/entrypoints/openai/testvideo.py:62 and tests/entrypoints/testchatutils.py:153 both use this format
Memory amplification
Each JPEG frame decodes to a full numpy array. For 640x480 RGB images, each frame is ~921 KB decoded. 5000 frames = ~4.6 GB. np.stack() then creates an additional copy. The compressed JPEG payload is small (~100 KB for 5000 frames) but decompresses to gigabytes.
Data flow
POST /v1/chat/completions → chatutils.py:1434 videourl type → mmparser.parsevideo() → chatutils.py:872 parsevideo() → self.connector.fetchvideo() → connector.py:295 fetchvideo() → loadfromurl(url, self.videoio) → connector.py:91 loaddataurl(): urlspec.path.split(",", 1) → mediatype = "video/jpeg" → data = "<frame1>,<frame2>,...,<frame10000>" → connector.py:100 mediaio.loadbase64("video/jpeg", data) → video.py:54 data.split(",") ← UNBOUNDED → video.py:55-57 all frames decoded into numpy arrays → video.py:56 np.stack([...]) ← massive combined array → OOM
connector.py:91 uses split(",", 1) which splits on only the first comma. All remaining commas stay in data and are later split by video.py:54.
Comparison with existing protections
| Code Path | Frame Limit | File | |-----------|-------------|------| | loadbytes() (binary video) | Yes — numframes (default 32) | video.py:46-49 | | loadbase64("video/jpeg", ...) | No — unlimited data.split(",") | video.py:51-62 |
vLLM is an inference and serving engine for large language models. Prior to 0.26.0, the structuredoutputs.regex parameter in vllm/v1/structuredoutput/backendlmformatenforcer.py is passed to lmformatenforcer.RegexParser without compileregexwithtimeout or validation in validatestructuredoutputrequestlmformatenforcer, allowing an unauthenticated /v1/completions request against the lm-format-enforcer backend to consume a CPU core and stall the structured-output engine path with a catastrophic regular expression. This issue is fixed in version 0.26.0.
Improper input validation for some vLLM Hardware Plugin for Intel(R) Gaudi(R) software before version 0.16.0 within Ring 3: User Applications may allow a denial of service. Authorized adversary with an authenticated user combined with a low complexity attack may enable denial of service. This result may potentially occur via local access when attack requirements are not present without special internal knowledge and requires no user interaction. The potential vulnerability may impact the confidentiality (none), integrity (none) and availability (high) of the vulnerable system, resulting in subsequent system confidentiality (none), integrity (none) and availability (none) impacts.
Summary
Two model implementation files hardcode trustremotecode=True when loading sub-components, bypassing the user's explicit --trust-remote-code=False security opt-out. This enables remote code execution via malicious model repositories even when the user has explicitly disabled remote code trust.
### Details
Affected files (latest main branch):
1. vllm/modelexecutor/models/nemotronvl.py:430 python visionmodel = AutoModel.fromconfig(config.visionconfig, trustremotecode=True)
2. vllm/modelexecutor/models/kimik25.py:177 python cachedgetimageprocessor(self.ctx.modelconfig.model, trustremotecode=True)
Both pass a hardcoded trustremotecode=True to HuggingFace API calls, overriding the user's global --trust-remote-code=False setting.
Relation to prior CVEs: - CVE-2025-66448 fixed automap resolution in vllm/transformersutils/config.py (config loading path) - CVE-2026-22807 fixed broader automap at startup - Both fixes are present in the current code. These hardcoded instances in model files survived both patches — different code paths.
Impact
Remote code execution. An attacker can craft a malicious model repository that executes arbitrary Python code when loaded by vLLM, even when the user has explicitly set --trust-remote-code=False. This undermines the security guarantee that trustremotecode=False is intended to provide.
Remediation: Replace hardcoded trustremotecode=True with self.config.modelconfig.trustremotecode in both files. Raise a clear error if the model component requires remote code but the user hasn't opted in.
Issue Description Librosa defaults to using numpy.mean for mono downmixing (tomono), while the international standard ITU-R BS.775-4 specifies a weighted downmixing algorithm. This discrepancy results in: - Inconsistency between audio heard by humans (e.g., through headphones/regular speakers) and audio processed by AI models (Which infra via Librosa, such as vllm, transformer).
https://github.com/librosa/librosa/blob/af8c839fb15317fa2712ea66e7a22da6a9267b32/librosa/core/audio.py#L478 Attack Scenario and Impact
LFE (Low-Frequency Effects) Channel Exploit Attackers can craft special multichannel audio files containing: 1. Normal content in front channels (L/R) 2. Either interference signals or hidden content in the LFE channel
Notice: It is worth noting that not only the LFE channel is excluded, but in fact, channels beyond the 6th (such as rear surround channels, overhead channels, height speakers, etc.) are also not supported.
Attack Methodology:
Attackers can create specially engineered multichannel audio with LFE interference, where front channels (L/R) contain normal content while the LFE channel carries interference signals or hidden content. When played on consumer devices that ignore LFE channels, only the normal content is heard. However, when processed by AI systems using Librosa (which mixes all channels), the LFE interference affects speech recognition feature extraction or masks critical detection features. This enables malicious content to bypass AI detection while still reaching end users, potentially compromising voice authentication systems, evading content moderation, or disrupting speech recognition accuracy.
Potential Exploitation Scenarios: - Voice authentication systems may be tricked into accepting anomalous audio - Content moderation systems may fail to detect prohibited content hidden in LFE channels - Speech recognition systems may produce incorrect transcriptions
Note: torch.audio implements this correctly. Failure to do so may lead to inconsistencies between training and test audio, resulting in performance degradation.
Resources
- ITU-R BS.775-4 Standard - Librosa Source Code - Librosa securty report
Fixes
- https://github.com/vllm-project/vllm/pull/37058, which removes the librosa dependency from vLLM.
vLLM versions 0.8.0 and later are vulnerable to an Out-of-Memory (OOM) Denial of Service (DoS) attack due to unbounded frame count processing in the VideoMediaIO.loadbase64() method. When processing video/jpeg data URLs, the method splits the base64 data string on commas to extract individual JPEG frames without enforcing a frame count limit. An attacker can exploit this by crafting a single API request containing thousands of comma-separated base64-encoded JPEG frames in a data URL, causing the server to decode all frames into memory and crash due to excessive memory consumption. This vulnerability is reachable via the OpenAI-compatible chat completions API and does not require authentication.
vLLM versions >= 0.10.2 and < 0.13.0 are missing sparse tensor validation in multimodal embeddings processing. Because PyTorch disables sparse tensor invariant checks by default, an attacker can submit crafted embedding requests with malformed (negative or out-of-bounds) tensor indices, when the prompt-embeds feature is enabled, to trigger crashes or resource exhaustion (denial of service), with potential for out-of-bounds/write-what-where memory corruption. This continues CVE-2025-62164, whose prior fix only disabled the feature by default rather than addressing the root cause.
Summary
The SSRF protection fix for https://github.com/vllm-project/vllm/security/advisories/GHSA-qh4c-xf7m-gxfc can be bypassed in the loadfromurlasync method due to inconsistent URL parsing behavior between the validation layer and the actual HTTP client.
Affected Component
- File: vllm/connections.py - Function: loadfromurlasync
Vulnerability Details
Root Cause
The SSRF fix uses urllib3.util.parseurl() to validate and extract the hostname from user-provided URLs. However, loadfromurlasync uses aiohttp for making the actual HTTP requests, and aiohttp internally uses the yarl library for URL parsing.
These two URL parsers handle backslash characters (\) differently:
| Parser | Input URL | Parsed Host | Parsed Path | Behavior | |--------|-----------|-------------|-------------|----------| | urllib3.parseurl() | https://httpbin.org\@evil.com/ | httpbin.org | /%5C@evil.com/ | URL-encodes \ as %5C, treats \@evil.com/ as part of the path | | yarl (via aiohttp) | https://httpbin.org\@evil.com/ | evil.com | / | Treats \ as part of userinfo (user: httpbin.org\), the @ acts as the userinfo/host separator |
Attack Scenario
python Attacker provides this URL maliciousurl = "https://httpbin.org\\@evil.com/"
1. Validation layer (urllib3.parseurl) parsed = urllib3.util.parseurl(maliciousurl) parsed.host == "httpbin.org" ✅ Passes validation
2. Actual request (aiohttp with yarl) async with aiohttp.ClientSession() as session: async with session.get(maliciousurl) as response: # Request actually goes to evil.com! ❌ Bypass!
Why This Happens
1. yarl: Interprets httpbin.org\ as the userinfo component, and @ as the userinfo/host separator, so the URL is parsed as user=httpbin.org\, host=evil.com, path=/ 2. urllib3: URL-encodes the backslash as %5C, so \@evil.com/ becomes /%5C@evil.com/ which is treated as part of the path, leaving host=httpbin.org
This inconsistency allows an attacker to: - Bypass the hostname allowlist check - Access arbitrary internal/external services - Perform full SSRF attacks
Fixes
- https://github.com/vllm-project/vllm/pull/34743
Summary A Server-Side Request Forgery (SSRF) vulnerability exists in the MediaConnector class within the vLLM project's multimodal feature set. The loadfromurl and loadfromurlasync methods obtain and process media from URLs provided by users, using different Python parsing libraries when restricting the target host. These two parsing libraries have different interpretations of backslashes, which allows the host name restriction to be bypassed. This allows an attacker to coerce the vLLM server into making arbitrary requests to internal network resources.
This vulnerability is particularly critical in containerized environments like llm-d, where a compromised vLLM pod could be used to scan the internal network, interact with other pods, and potentially cause Denial of Service or access sensitive data. For example, an attacker could make the vLLM pod send malicious requests to an internal llm-d management endpoint, leading to system instability by falsely reporting metrics like the KV cache state.
Details The core of the vulnerability lies in the MediaConnector.loadfromurl method and its asynchronous counterpart. These methods accept a URL string to fetch media content (images, audio, video).
def loadfromurl( self, url: str, mediaio: MediaIO[M], , fetchtimeout: int | None = None, ) -> M: # type: ignore[type-var] urlspec = urlparse(url) if urlspec.scheme.startswith("http"): self.asserturlinallowedmediadomains(urlspec) connection = self.connection data = connection.getbytes( url, timeout=fetchtimeout, allowredirects=envs.VLLMMEDIAURLALLOWREDIRECTS, ) return mediaio.loadbytes(data)
The URL validation uses the urlparse function from Python's urllib module, while the request is made using the request function from Python's requests module. The requests module's underlying URL parsing is implemented using the parseurl function from Python's urllib3. These two parsing functions follow different URL specifications; one is implemented according to the RFC 3986 specification, and the other is implemented according to the WHATWG Living Standard. There is a difference in how the two functions handle backslashes (\) in URLs, which allows the hostname restriction to be bypassed.
Fix
https://github.com/vllm-project/vllm/pull/32746
Summary
vLLM loads Hugging Face automap dynamic modules during model resolution without gating on trustremotecode, allowing attacker-controlled Python code in a model repo/path to execute at server startup.
---
Impact
An attacker who can influence the model repo/path (local directory or remote Hugging Face repo) can achieve arbitrary code execution on the vLLM host during model load. This happens before any request handling and does not require API access.
---
Affected Versions
All versions where vllm/modelexecutor/models/registry.py resolves automap entries with trygetclassfromdynamicmodule without checking trustremotecode (at least current main).
---
Details
During model resolution, vLLM unconditionally iterates automap entries from the model config and calls trygetclassfromdynamicmodule, which delegates to Transformers’ getclassfromdynamicmodule and executes the module code.
This occurs even when trustremotecode is false, allowing a malicious model repo to embed code in a referenced module and have it executed during initialization.
Relevant code
- vllm/modelexecutor/models/registry.py:856 — automap resolution - vllm/transformersutils/dynamicmodule.py:13 — delegates to getclassfromdynamicmodule, which executes code
---
Fixes
https://github.com/vllm-project/vllm/pull/32194
Credits
Reported by bugbunny.ai
Summary Short summary of the problem. Make the impact and severity as clear as possible. For example: An unsafe deserialization vulnerability allows any unauthenticated user to execute arbitrary code on the server.
Sending a pure prompt embeds payload in a /v1/completions request with a model using M-RoPE causes the EngineCore to fail an assertion and fatally crash, shutting down the entire server application.
Any remote user who is authorized to make a /v1/completions endpoint can trivially make such a request and induce a crash.
Details Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.
In commit 56669c1, a simple assert intended to be a type-narrowing assert was added to the initmropepositions method in GPUModelRunner (the offending line on main at the time of writing: https://github.com/vllm-project/vllm/blob/2d481f8a946ee0521872af0f098674a8ee01ce4a/vllm/v1/worker/gpumodelrunner.py#L1588-L1607).
python assert reqstate.prompttokenids is not None, ( "M-RoPE requires prompttokenids to be available." )
This type narrowing assert is to prevent mypy errors later in the function because None is not a valid type for mropemodel.getmropeinputpositions. Unfortunately, this assertion is not always true. /v1/completions requests that specify prompt=None and promptembeds=<not none> will indeed create a CachedRequestState where prompttokenids is None. This triggers the assertion, which in turn crashes the EngineCore and the Server application.
(EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpumodelrunner.py", line 3997, in executemodel (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] deferredstatecorrectionsfn = self.updatestates(scheduleroutput) (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpumodelrunner.py", line 1239, in updatestates (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] self.initmropepositions(reqstate) (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpumodelrunner.py", line 1582, in initmropepositions (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] assert reqstate.prompttokenids is not None, ( (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] AssertionError: M-RoPE requires prompttokenids to be available. (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] AsyncLLM outputhandler failed. (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] Traceback (most recent call last): (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/engine/asyncllm.py", line 660, in outputhandler (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] outputs = await enginecore.getoutputasync() (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/engine/coreclient.py", line 1030, in getoutputasync (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] raise self.formatexception(outputs) from None (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.
All requests using the /v1/chat/completions endpoints will have text/prompttokenids parts (corresponding to the chat template), and promptembeds parts are handled as mmfeatures. This method (rightly) filters out those promptembeds content parts as they are treated as text positions.
A sufficient solution to type narrowing here without raising a fatal assertion is to instead replace the assertion with a using dummy token ids:
python def initmropepositions(self, reqstate: CachedRequestState): model = self.getmodel() assert supportsmrope(model), "M-RoPE support is not implemented." mropemodel = cast(SupportsMRoPE, model)
# Filter out promptembeds modality (text-only position info) mropefeatures = [ f for f in reqstate.mmfeatures if f.modality != "promptembeds" ] # Handle both tokenids and embeddings-only inputs if reqstate.prompttokenids is not None: inputtokens = reqstate.prompttokenids elif reqstate.promptembeds is not None: # For text-only embeddings, dummy token IDs are safe since # getmropeinputpositions only uses len(inputtokens) when mmfeatures is empty seqlen = reqstate.promptembeds.shape[0] inputtokens = list(range(seqlen)) # Verify no mmfeatures remain (should be true after promptembeds filter) assert len(mropefeatures) == 0, ( "M-RoPE with promptembeds-only input should have no multimodal features" ) else: raise ValueError( "M-RoPE requires either prompttokenids or promptembeds." )
reqstate.mropepositions, reqstate.mropepositiondelta = ( mropemodel.getmropeinputpositions( inputtokens, mropefeatures, ) )
Technically, in isolation, this method still crashes in the case where reqstate.prompttokenids is None and reqstate.mmfeatures, so the solution above still leaves that potential vector open. As far as can be determined, however, such a reqstate is impossible in the first place in online mode, because it would require a /v1/completions request with promptembeds AND multimodal features, but the /v1/completions request schema does not expose multimodal inputs in any discernible way. Today, those are the only two endpoints with promptembeds support.
When in offline mode, it is technically possible to directly create an EngineCoreRequest that has promptembeds and not prompttokenids and mmfeatures, and pass that to LLM.generate. That would trigger this same assertion, and no validation would prevent that combination. It is strongly suspected, though, that this combination would be undefined in any model that support M-RoPE, because it would not be possible to determine which token positions correspond to mmfeatures. The proposed solution above would end up not setting reqstate.mropepositions and reqstate.mropepositiondelta in this scenario, which could result in undefined behavior.
promptembeds is far more familiar here than M-RoPE, and it is understood that each model that supports it is responsible for defining its own getmropeinputpositions which have varying implementations. There is insufficient knowledge to be prescriptive in how the two features should interact in the offline case, other than possibly raising a validation error earlier on preventing that combination (which would emulate the current assertion behavior). Regardless, in offline mode, the chances of a remote user being able to exploit this are slim-to-nil compared to the online case which is incredibly straightforward.
Impact What kind of vulnerability is it? Who is impacted?
- Denial of Service caused by an incorrect assertion inside of the GPUModelRunner which causes a fatal EngineCore exception - Any configuration with --enable-prompt-embeds and M-RoPE-supported model is vulnerable - The attack is extremely easy from the remote attacker's perspective (copying the official promptembeds online mode docs examples almost-verbatim, accounting for model-name and connection details, of course, will induce a guaranteed shutdown)
Summary
Current-head vLLM documents VLLMMAXAUDIOCLIPFILESIZEMB as the maximum audio file size accepted by the speech-to-text APIs. The default is 25 MB. vllm/envs.py also describes files larger than this value as rejected.
The /v1/audio/transcriptions and /v1/audio/translations routes call await request.file.read() before vLLM checks that limit. In FastAPI and Starlette, UploadFile.read() returns bytes from the uploaded file object; when called without a size argument, the route materializes the remaining file contents. vLLM then performs the compressed file-size check later in preprocessspeechtotext() against the already-created bytes object.
As a result, the documented compressed audio file-size limit does not bound the memory allocated by vLLM endpoint code before validation. An oversized multipart upload can cause vLLM to allocate memory proportional to the uploaded file size before rejecting the request as too large.
This is distinct from GHSA-6pr9-rp53-2pmc, which covered decoded PCM expansion after compressed input was accepted. This report covers compressed upload materialization before compressed-size validation.
Technical Details
The upload routes perform an unbounded read before vLLM checks the documented compressed audio file-size limit:
- vllm/entrypoints/speechtotext/transcription/apirouter.py: audiodata = await request.file.read() - vllm/entrypoints/speechtotext/translation/apirouter.py: audiodata = await request.file.read() - vllm/entrypoints/speechtotext/base/serving.py later checks: if len(audiodata) / 10242 > self.maxaudiofilesizemb
There is no route-level check of request.file.size, Content-Length, a bounded read(maxbytes + 1), or a streaming copy that stops at the configured limit before the full file is materialized.
This does not appear to be intended behavior. vLLM's security guide treats request-controlled resource use as a security boundary: for example, requests that exceed VLLMMAXNSEQUENCES are rejected before reaching the engine. The speech-to-text upload limit is documented in the same spirit as an API enforced limit, but the first vLLM check happens after the over-limit upload has already been copied into a bytes object.
Impact
Attack requirements:
- the deployment exposes /v1/audio/transcriptions or /v1/audio/translations; - a speech-to-text capable model/task is configured; and - the caller can submit requests to the endpoint, including any API key the deployment requires.
An API caller who meets those requirements can send an oversized audio file. vLLM reads the full uploaded file into memory before applying the configured compressed audio file-size limit. This can create memory pressure or, depending on process/container limits and concurrency, terminate the process before the request is rejected.
Suggested severity: Moderate, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H (6.5), CWE-770/CWE-400.
The impact is availability-only. This is not a claim of code execution, data access, cross-tenant data exposure, parser-level multipart memory exhaustion, or persistence after process restart. Deployment body-size limits at a reverse proxy or ASGI layer can mitigate the issue before vLLM sees the request, but vLLM's own documented file-size limit does not currently provide that memory boundary.
Suggested Fix
Enforce the compressed audio upload limit before the unbounded read:
- Check reliable upload size metadata before reading when available. - Read at most maxbytes + 1 bytes in chunks as a defense-in-depth guard against missing or unreliable metadata. - Share the helper between transcription and translation routes. - Add regression tests that prove an over-limit UploadFile is rejected without calling an unbounded read().
The important property is that over-limit compressed uploads are rejected before vLLM allocates the full uploaded file as bytes.
Resources
- vLLM security policy: https://github.com/vllm-project/vllm/security/policy - vLLM speech-to-text docs: https://docs.vllm.ai/en/latest/serving/onlineserving/speechtotext/ - vLLM security guide, request parameter resource limits: https://docs.vllm.ai/en/latest/usage/security/ - vLLM vulnerability management docs: https://docs.vllm.ai/en/latest/contributing/vulnerabilitymanagement/ - FastAPI file uploads: https://fastapi.tiangolo.com/reference/uploadfile/ - Starlette uploaded files: https://www.starlette.io/requests/ - Adjacent published audio advisory: https://github.com/vllm-project/vllm/security/advisories/GHSA-6pr9-rp53-2pmc - Request-parameter resource DoS precedent: https://github.com/vllm-project/vllm/security/advisories/GHSA-3mwp-wvh9-7528
Appendix: Affected Version
Validated against current head:
- commit: 1033ffac2eccf986fdd880f4dee64ca3b22c63c9 - described version: v0.22.1rc0-491-g1033ffac2e
Known affected range: current head. It has not been determined the introducing commit or release range.
Appendix: Proof Of Vulnerability
The attached proof is a non-destructive static probe. It does not upload a large file or contact a running vLLM server:
bash python3 attached-evidence/poc/audiouploadsizeprecheckprobe.py
Observed result:
json { "pov": "this report", "validated": true, "defaultlimitmb": 25, "documentedapilimit": true, "routes": { "transcriptionroute": { "unboundeduploadread": true, "earlysizeguardbeforeread": false, "chunkedboundedread": false }, "translationroute": { "unboundeduploadread": true, "earlysizeguardbeforeread": false, "chunkedboundedread": false } }, "latesizecheck": { "present": true } }
Expected behavior: vLLM rejects over-limit audio files before materializing the entire upload into memory in vLLM endpoint code.
Actual behavior: the route materializes the upload into memory first, and only then does vLLM reject the request as exceeding VLLMMAXAUDIOCLIPFILESIZEMB.
Summary
The structuredoutputs.regex API parameter passes a user-supplied regex string directly to grammar compiler backends with no compilation timeout. In the xgrammar backend, the string reaches compileregex() with no guard. In the outlines backend, validateregexisbuildable() blocks structural issues (lookarounds, backreferences) but provides zero protection against exponential DFA state-space explosion. Patterns like (a+)+b pass all checks and hang the inference worker.
Root Cause
backendxgrammar.py:91 — no timeout: python ctx = self.compiler.compileregex(grammarspec)
backendoutlines.py:299–330 — structural checks only, no complexity analysis: python def validateregexisbuildable(regex: str) -> None: sreparse.parse(regex) # AST parse only — does not detect exponential patterns checkunsupported(...) # blocks lookarounds/backrefs, not nested quantifiers
backendoutlines.py:64 — no timeout: python oc.Index(regexstring, vocabulary.inner)
Impact
Denial of service — one request with an adversarial regex pattern hangs an inference worker indefinitely.
Remediation
Wrap compileregex() and oc.Index() calls in a thread with a deadline (e.g., 5 seconds). Add complexity analysis to validateregexisbuildable() to detect nested quantifier patterns before compilation.
Summary
A frontend-legal multi-request speculative workload can make vLLM produce an out-of-vocabulary recovered token equal to vocabsize, 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: - Qwen/Qwen3-0.6B-GPTQ-Int8 - Anyone wants to reproduce the bug with my PoC scripts should download Qwen3-0.6B-GPTQ-Int8 first
Trigger chain
1. A legal multi-request speculative workload keeps structured-output state, speculative decoding, overlap, and request cancellation active in the same live engine. 2. During rejection sampling, vLLM produces a recovered token equal to the model vocabsize boundary value. 3. 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. 4. 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. 5. The drafter writes that converted -1 back into the live next-step input-id row for the request. 6. 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 -1 values in a speculative row can be ordinary padding - the bug appears when the first live token for a request becomes 151936 == vocabsize, and that live token is then converted into -1
In simplified form, the bad transition looks like this:
text 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:
text nexttokenids: [-1, ...]
Finally, that converted -1 is written back into the live model input ids:
text inputidsafter: [-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
1. The workload is frontend-legal. The requests use normal SamplingParams features such as structured outputs, stop, badwords, mintokens, and streaming overlap. No malformed token-id list is required at the request boundary. 2. In speculative decoding, the rejection sampler can generate recovered tokens when drafted tokens are rejected. python # vllm/v1/sample/rejectionsampler.py def samplerecoveredtokens(...): recoveredtokenids = torch.emptylike(drafttokenids) samplerecoveredtokenskernel(batchsize, maxspeclen) return recoveredtokenids On the verified Qwen3 run, the recovered-token trace shows recoveredtokenids[0] = 151936, which is exactly vocabsize for this checkpoint. 3. The speculative proposer then prepares the next-token row from the sampled speculative row. python # vllm/v1/specdecode/eagle.py def preparenexttokenidspadded(...): ... eaglepreparenexttokenpaddedkernelgrid return nexttokenids, validsampledtokenscount In the verified trace, this step receives a sampled row beginning with 151936, followed by -1 padding. The important point is that 151936 occupies the first live token position for the request. This step then produces nexttokenids[0] = -1, meaning the live next token for the request has been converted to -1. 4. The drafter then rotates the draft input ids and inserts those nexttokenids back into the live input-id buffer. python # vllm/v1/specdecode/eagle.py def setinputsfirstpass(...): ... self.inputids[tokenindicestosample] = nexttokenids In the verified trace, this produces inputidsafter[0] = -1. 5. The model-side embed path later consumes those input ids. python # vllm/modelexecutor/models/qwen2.py def embedinputids(self, inputids: torch.Tensor) -> torch.Tensor: return self.embedtokens(inputids) In the verified trace, this is the first point where the converted -1 becomes 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 -1 and was written back into input ids. 6. 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 under flashattnvarlenfunc(...).
Local script breakdown
reprog4recoveredminus1local.py is a standalone local reproducer.
- It reads the Qwen3 checkpoint path from VLLMPOCG4MODEL or the built-in /path/to/qwen3 placeholder - It creates EngineCore directly 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: - requestpayloads.json - reproconfig.json - timeline.json - responses.json - error.txt - recoveredchaintrace.jsonl - recoveredchaintrace.jsonl is the key attribution artifact. It records the recovered-token chain directly from the standalone reproducer
gRPC script breakdown
reprog4recoveredminus1grpc.py is a standalone public gRPC reproducer.
- It reads the Qwen3 checkpoint path from VLLMPOCG4MODEL or the built-in /path/to/qwen3 placeholder - It starts a temporary vllm.entrypoints.grpcserver process - It sends only public Generate and Abort RPCs - 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 Generate probe request to confirm that later gRPC requests also fail after the worker dies - It writes: - requestpayloads.json - timeline.json - servercommand.json - responses.json - postcrashprobe.json - server.stdout.log - server.stderr.log
Observed result
Local repro typically ends with:
- a recovered-token trace showing: - samplerecoveredtokensreturn -> recoveredtokenids[0] = 151936 - preparenexttokenidspadded -> nexttokenids[0] = -1 - setinputsfirstpass -> inputidsafter[0] = -1 - embedinputidsoutofrange -> inputids[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
text samplerecoveredtokensreturn: recoveredtokenids = [151936, ...] vocabsize = 151936
preparenexttokenidspadded: sampledtokenidshead = [[151936, -1, -1, ...], ...] nexttokenids = [-1, ...]
setinputsfirstpass: inputidsafter = [-1, 0, 0, 0, ...]
embedinputidsoutofrange: inputids = [-1, 0, 0, 0, ...]
gRPC server log
text 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 postcrashprobe 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 vocabsize, 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 == vocabsize
This value should be described as the model vocabsize boundary value, not as a legal token id.
Attachments
The attached bundle for this report should contain:
- reprog4recoveredminus1local.py - reprog4recoveredminus1grpc.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
Summary
A chain of vulnerabilities in vLLM allow Remote Code Execution (RCE):
1. Info Leak - PIL error messages expose memory addresses, bypassing ASLR 2. Heap Overflow - JPEG2000 decoder in OpenCV/FFmpeg has a heap overflow that lets us hijack code execution
Result: Send a malicious video URL to vLLM Completions or Invocations for a video model -> Execute arbitrary commands on the server
Completely default vLLM instance directly from pip, or docker, does not have authentication so "None" privileges are required, but even with non-default api-key enabled configuration this exploit is feasible through invocations route that allows payload to execute pre-auth.
Example heap target is provided, other heap targets can be exploited as well to achieve rce. Leak allows for simple ASLR bypass. Leak + heap overflow achieves RCE on versions prior to 0.14.1.
Deployments not serving a video model are not affected.
---
1. Vulnerability Overview
1.1 The Bug: JPEG2000 cdef Box Heap Overflow The JPEG2000 decoder used by OpenCV (cv2) honors a cdef box that can remap color channels. When Y (luma) is mapped into the U (chroma) plane buffer, the decoder writes a large Y plane into the smaller U buffer, causing a heap overflow.
Root Cause - cdef allows channel remapping (e.g., Y→U, U→Y). - Y plane size: W×H; U plane size: (W/2)×(H/2). - Overflow size = W×H - (W/2×H/2) = 0.75 × W × H bytes.
Example (150×64) - Y plane: 150×64 = 9,600 bytes - U plane: 75×32 = 2,400 bytes - Overflow: 7,200 bytes past the U buffer
1.2 Malicious cdef Box Offset Size Field Value 0 4 Box Length 0x00000016 (22 bytes) 4 4 Box Type 'cdef' 8 2 N (channels) 0x0003 10 2 Channel 0 Cn 0x0000 (Y channel) 12 2 Channel 0 Typ 0x0000 (color) 14 2 Channel 0 Asoc 0x0002 (→ maps Y into U plane) 16 2 Channel 1 Cn 0x0001 (U channel) 18 2 Channel 1 Typ 0x0000 (color) 20 2 Channel 1 Asoc 0x0001 (→ maps U into Y plane) 22 2 Channel 2 Cn 0x0002 (V channel) 24 2 Channel 2 Typ 0x0000 (color) 26 2 Channel 2 Asoc 0x0003 (→ maps V plane) Key control: Asoc=2 for channel 0 forces Y data into the U buffer, triggering the overflow.
---
Vulnerable Code Chain
1) Entry: vLLM accepts a remote videourl and downloads raw bytes
vLLM’s OpenAI-compatible API supports a videourl content part:
python class VideoURL(TypedDict, total=False): url: Required[str]
class ChatCompletionContentPartVideoParam(TypedDict, total=False): videourl: Required[VideoURL] type: Required[Literal["videourl"]]
Source: src/vllm/entrypoints/chatutils.py.
When the URL is HTTP(S), vLLM downloads it as raw bytes and passes the bytes into the modality loader:
python if urlspec.scheme.startswith("http"): data = connection.getbytes(url, timeout=fetchtimeout, allowredirects=...) return mediaio.loadbytes(data)
Source: src/vllm/multimodal/utils.py (MediaConnector.loadfromurl).
---
2) Decode: vLLM uses OpenCV (cv2) VideoCapture on an in-memory byte stream
The default video backend is OpenCV, and it constructs cv2.VideoCapture over a BytesIO buffer containing the downloaded bytes:
python backend = cls().getcv2videoapi() cap = cv2.VideoCapture(BytesIO(data), backend, []) if not cap.isOpened(): raise ValueError("Could not open video stream")
Source: src/vllm/multimodal/video.py (OpenCVVideoBackend.loadbytes).
The backend is selected from OpenCV’s stream-buffered backends registry:
python import cv2.videoioregistry as vr for backend in vr.getStreamBufferedBackends(): if vr.hasBackend(backend) and ...: apipref = backend break return apipref
Source: src/vllm/multimodal/video.py (OpenCVVideoBackend.getcv2videoapi).
Implication: vLLM is delegating container parsing + codec decode to OpenCV’s Video I/O stack (which, in typical builds, is backed by FFmpeg for MOV/MP4 and codecs like JPEG2000).
---
3) The actual overflow: Y (full-res) written into U (quarter-res)
When the decoder honors the remap and writes Y into the U-plane buffer, it writes too many bytes:
- Y plane bytes: \(W \times H\) - U plane bytes: \((W/2) \times (H/2)\) - Overflow bytes: \(W \times H - (W/2 \times H/2) = 0.75 \times W \times H\)
Concrete example tried (150×64):
- Y: \(150 \times 64 = 9600\) bytes - U: \(75 \times 32 = 2400\) bytes - Overflow: \(9600 - 2400 = 7200\) bytes past the end of the U allocation
This is a heap buffer overflow into whatever allocations follow the U-plane buffer in the decoder’s heap layout (structures, metadata, other buffers, etc.). The exact victims depend on build + runtime allocator layout.
---
The Exploit Chain
Vuln 1: PIL BytesIO Address Leak (ASLR Bypass)
When you send an invalid image to vLLM's multimodal endpoint, PIL throws an error like:
cannot identify image file <io.BytesIO object at 0x7a95e299e750> ^^^^^^^^^^^^^^^^ LEAKED ADDRESS!
vLLM returns this error to the client, leaking a heap address. This address is ~10.33 GB before libc in memory. With this leak, we reduce ASLR from 4 billion guesses to ~8 guesses.
Vuln 2: JPEG2000 cdef Heap Overflow (RCE)
vLLM uses OpenCV (cv2) to decode videos. OpenCV bundles FFmpeg 5.1.x which has a heap overflow in the JPEG2000 decoder. The OpenCV is used for video decoding so if we build a video from JPEG2000 frames it will reach the vuln:
vLLM API Request to Completions/Invocation ↓ OpenCV cv2.VideoCapture() ↓ FFmpeg 5.1 (bundled in OpenCV) ↓ JPEG2000 decoder (libopenjp2) ↓ HEAP OVERFLOW via malicious "cdef" box ↓ Overwrite function pointer → RCE!
How the overflow works: - JPEG2000 has a cdef box that remaps color channels - We remap Y (luma) into the U (chroma) buffer - Y plane = 9,600 bytes, U plane = 2,400 bytes - On small geometry like 150x64 pixel image we get 7,200 bytes overflow past the U buffer. We can grow that exponentially by making bigger images. - This overwrites an AVBuffer structure containing a free() function pointer. This could be any function pointer or other targets. - We set free = system() and opaque = "command string" - When the buffer is freed → system("our command") executes
---
vLLM Attack Surface
Affected Endpoints
Both multimodal endpoints are vulnerable:
POST /v1/chat/completions (with videourl in content) POST /v1/invocations (with videourl in content)
Request Flow
1. Attacker sends request with videourl pointing to malicious .mov file 2. vLLM fetches the video from the URL 3. vLLM passes video bytes to cv2.VideoCapture() 4. OpenCV's bundled FFmpeg decodes JPEG2000 frames 5. Malicious cdef box triggers heap overflow 6. AVBuffer.free pointer overwritten with system() 7. When buffer is released → system("attacker command") executes
---
Versions Affected
| Component | Version | Notes | |-----------|---------|-------| | vLLM | >= 0.8.3, < 0.14.1 | Default config vulnerable when serving a video model | | OpenCV (cv2) | 4.x with FFmpeg bundle | Bundled FFmpeg is vulnerable | | FFmpeg | 5.1.x (bundled) | JPEG2000 cdef overflow | | libopenjp2 | 2.x | Honors malicious cdef box |
---
Fixes
https://github.com/vllm-project/vllm/pull/31987 https://github.com/vllm-project/vllm/pull/32319 https://github.com/vllm-project/vllm/pull/32668
vLLM versions >= 0.6.3 and < 0.9.0 contain multiple regular expression denial of service (ReDoS) vulnerabilities. Several regex patterns — in vllm/lora/utils.py, the phi4mini tool parser, and the OpenAI-compatible serving chat endpoint — are susceptible to catastrophic backtracking. An attacker submitting crafted input with nested or repeated structures can trigger severe CPU consumption and performance degradation, resulting in denial of service.
vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.22.0, an assert-based security check in vLLM's activation function loading allows any unauthenticated attacker to achieve arbitrary code execution on the server by publishing a malicious HuggingFace model, when vLLM runs in Python optimized mode (python -O or PYTHONOPTIMIZE=1). This vulnerability is fixed in 0.22.0.
vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via the Anthropic API router
Researcher: Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research Severity: CVSS 3.1 5.3 (Medium) AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N Target: https://github.com/vllm-project/vllm
---
Summary
The fix for CVE-2026-22778 / GHSA-4r2x-xpjr-7cvv (PRs #31987 and #32319) introduced sanitizemessage and applied it at four FastAPI exception-handling sites in the OpenAI router. The sanitizer strips object-repr memory addresses (<io.BytesIO object at 0x7a95e299e750> → <io.BytesIO object>) before error messages reach the client, defeating the ASLR-bypass primitive that CVE-2026-22778 chained with a libopenjp2 heap overflow for RCE.
The fix is incomplete: response paths added to vLLM at or after the same time as the fix continue to echo str(exc) directly to clients without sanitizemessage. The original Stage 1 primitive — sending malformed image bytes so PIL raises UnidentifiedImageError whose message contains the BytesIO object repr — reaches all of them unmodified and leaks the heap address verbatim in the response body.
All five lines below are present in main HEAD (771e1e48b, 2026-05-26).
Affected sites
Current main HEAD (771e1e48b, 2026-05-26):
| # | File | Line | Code | |---|---|---|---| | 1 | vllm/entrypoints/anthropic/apirouter.py | 78 | message=str(e), (inside POST /v1/messages exception handler) | | 2 | vllm/entrypoints/anthropic/apirouter.py | 124 | message=str(e), (inside POST /v1/messages/counttokens) | | 3 | vllm/entrypoints/anthropic/serving.py | 808 | error=AnthropicError(type="internalerror", message=str(e)), (SSE streaming converter) | | 4 | vllm/entrypoints/speechtotext/realtime/connection.py | 75 | await self.senderror(str(e), "processingerror") (WebSocket event loop) | | 5 | vllm/entrypoints/speechtotext/realtime/connection.py | 265 | await self.senderror(str(e), "processingerror") (WebSocket generation loop) |
Why the global exception handler does not save these paths
apiserver.py registers a catch-all app.exceptionhandler(Exception)(exceptionhandler) at line 262, and that handler calls createerrorresponse(exc) which DOES apply sanitizemessage. However, FastAPI exception handlers fire only on unhandled exceptions that propagate out of a route function.
All affected HTTP paths catch Exception inside the route coroutine and construct the response themselves:
python vllm/entrypoints/anthropic/apirouter.py:71-81 (POST /v1/messages) try: generator = await handler.createmessages(request, rawrequest) except Exception as e: logger.exception("Error in createmessages: %s", e) return JSONResponse( statuscode=HTTPStatus.INTERNALSERVERERROR.value, content=AnthropicErrorResponse( error=AnthropicError( type="internalerror", message=str(e), # <-- unsanitized ) ).modeldump(), )
Because the exception is caught and a JSONResponse is returned in-route, every registered FastAPI exception handler — including the sanitizing global one — is bypassed. The WebSocket path bypasses it for a different reason: WebSocket frames don't traverse FastAPI's HTTP exception handler chain at all.
Reachability — the same primitive as the parent CVE
The Anthropic Messages API accepts image content parts in the request body (type: "image" with base64 source.data or type: "imageurl"). Image bytes are passed to the same multimodal loader used by the OpenAI router. Malformed bytes cause PIL.Image.open to raise:
UnidentifiedImageError: cannot identify image file <io.BytesIO object at 0x7a95e299e750>
The exception propagates up through handler.createmessages into the except Exception as e: at apirouter.py:75. str(e) returns the exception message verbatim, including the address. The address ends up in the error.message field of the JSON response body returned to the attacker. ASLR entropy on the affected process drops from ~4 billion to ~8 candidates, identically to CVE-2026-22778 Stage 1.
The same primitive is reachable on POST /v1/messages/counttokens (route #2), inside the SSE streaming converter when an exception is raised mid-stream (route #3), and over the realtime speech-to-text WebSocket when audio decoder or generation paths raise an exception containing any object repr (routes #4, #5).
Chronology — these are scope misses, not legacy code
- 2026-01-09: PR #31987 (aa125ecf0) introduces sanitizemessage and applies it to OpenAI router HTTP exception handlers. - 2026-01-15 (six days later): PR #32369 (4c1c501a7) adds vllm/entrypoints/anthropic/apirouter.py containing line 78's message=str(e). The fix was not applied to the new router. - 2026-03-02 (~two months later): PR #35588 (9a87b0578) adds the Anthropic counttokens endpoint, replicating the same message=str(e) pattern at line 124. - 2026-05-12 (~four months later): PR #42370 (d37e25ffb) consolidates speech-to-text entrypoints and the realtime WebSocket uses senderror(str(e), ...) for both error paths. - 2026-05-26: current main HEAD, all five lines still present.
Remediation
1. Apply sanitizemessage symmetrically to the five sites
python vllm/entrypoints/anthropic/apirouter.py — add at top: from vllm.entrypoints.utils import sanitizemessage
Line 78 (POST /v1/messages) and Line 124 (counttokens): message=sanitizemessage(str(e)),
python vllm/entrypoints/anthropic/serving.py — add at top: from vllm.entrypoints.utils import sanitizemessage
Line 808: error=AnthropicError(type="internalerror", message=sanitizemessage(str(e))),
python vllm/entrypoints/speechtotext/realtime/connection.py — add at top: from vllm.entrypoints.utils import sanitizemessage
Lines 75 and 265: await self.senderror(sanitizemessage(str(e)), "processingerror")
2. Tighten the regex (defense in depth)
The current regex r" at 0x[0-9a-f]+>" is narrow — it only matches the exact CPython builtin object-repr suffix in lowercase hex with a trailing >. Future Python versions, C extensions, or custom repr methods could produce non-matching formats that re-enable the leak:
python vllm/entrypoints/utils.py def sanitizemessage(message: str) -> str: # Strip any standalone hex address; downstream observers don't need them. return re.sub(r"\b0x[0-9a-fA-F]{6,}\b", "0x?", message)
3. Future-proofing: consider a response middleware
Both the route-local exception handling pattern (Anthropic router) and the WebSocket path bypass FastAPI's exception handler chain. A response-level middleware that always invokes sanitizemessage on outgoing error bodies would prevent this class of regression entirely.
Affected versions
- All vLLM versions containing vllm/entrypoints/anthropic/apirouter.py (introduced 2026-01-15 in PR #32369). - All vLLM versions containing vllm/entrypoints/speechtotext/realtime/connection.py (introduced 2026-05-12 in PR #42370). - Confirmed present in main HEAD 771e1e48b (2026-05-26).
Steps to reproduce
1. Clone the target: git clone --depth 1 https://github.com/vllm-project/vllm 2. Run the proof of concept (PoC.py) against the cloned source. 3. Observe the result shown under Verified result below.
Credit
Kai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.
Fix
A fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/45119
Summary
All temperature validation gates use comparison operators (<, >), which silently evaluate to False for NaN and for positive Infinity in Python's IEEE 754 float semantics. Both values pass every guard and propagate to GPU sampling kernels, where they produce undefined behavior or CUDA errors that can crash the inference worker. Note: -Infinity is correctly caught.
Root Cause
samplingparams.py:384: python if 0 < self.temperature < MAXTEMP: # NaN → False; +Inf → False
samplingparams.py:462: python if self.temperature < 0.0: # NaN → False; +Inf → False raise VLLMValidationError(...)
No math.isnan() or math.isinf() check exists anywhere in samplingparams.py.
Python semantics (verified): float('nan') < 0.0 → False, float('inf') < 0.0 → False.
Impact
Crash of inference worker on GPU kernel execution with NaN/Inf softmax input, degrading service for all concurrent users.
Remediation
Add math.isfinite(self.temperature) check in verifyargs(). Reject non-finite float values with a 400 error.
Fix
A fix for this vulnerability was merged here: https://github.com/vllm-project/vllm/pull/45116
Summary vLLM's /v1/audio/transcriptions endpoint limits compressed upload size but not decoded PCM output. A 25MB OPUS file expands to ~14.9GB of float32 PCM at decode time. Tested on vLLM v0.19.0.
Details SpeechToTextProcessor rejects uploads over VLLMMAXAUDIOCLIPFILESIZEMB (default 25MB) based on compressed byte length, but the audio decoder in audio.py accumulates all decoded frames into memory with no size limit before returning:
python speechtotext.py L184-189 if len(audiodata) / 1024 2 > self.maxaudiofilesizemb: raise VLLMValidationError(...) y, sr = loadaudio(buf, sr=self.asrconfig.samplerate) # decoded size unchecked
audio.py L77-107 chunks: list[npt.NDArray] = [] for frame in container.decode(stream): chunks.append(frame.tondarray()) audio = np.concatenate(chunks, axis=-1).astype(np.float32) # single contiguous allocation
A 25MB OPUS file at 6kbps encodes ~8.7 hours of audio. Decoding produces ~5.7GB of float32 PCM (232x amplification), and np.concatenate then allocates a second contiguous array, bringing peak RSS to ~14.9GB from a single request. SpeechToTextConfig.maxaudioclips (default 30s) applies only after the full decode and does not prevent the allocation.
Impact An unauthenticated attacker can exhaust server memory with a small number of concurrent requests, each a valid upload within the documented size limit. Severity was assessed with reference to prior OOM vulnerability reports in vLLM.
Fix
A fix for this vulnerability was merged here: https://github.com/vllm-project/vllm/pull/44970
vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.22.1, the vLLM Dockerfile is vulnerable to a dependency confusion attack through the flashinfer-jit-cache package. The package is installed from a custom index (flashinfer.ai/whl/) using --extra-index-url, but the package name was not registered on PyPI, and UVINDEXSTRATEGY="unsafe-best-match" is set globally. An attacker who registers flashinfer-jit-cache on PyPI with version 0.6.11.post2 can execute arbitrary code as root during the Docker build and backdoor every resulting container image, enabling exfiltration of all user prompts, API credentials, and model data from production vLLM deployments This vulnerability is fixed in 0.22.1.
Summary
Integer truncation of tensor dimensions in vLLM's GGUF dequantize kernels (csrc/quantization/gguf/ggufkernel.cu) causes partial tensor processing. The output tensor is allocated at full size via torch::empty (uninitialized memory), but the dequantize CUDA kernel processes only a truncated number of elements. The unfilled portion of the output tensor retains whatever was previously in GPU memory. In multi-tenant inference deployments, this residual GPU memory may contain tensor data from other users' inference requests, constituting information disclosure.
Root Cause
The tocudaggmlt function pointer type at ggml-common.h:1067 declares its element count parameter as int (32-bit):
cpp using tocudaggmlt = void ()(const void restrict x, dstt restrict y, int k, // 32-bit cudaStreamt stream);
All dequantize kernel functions (dequantizeblockcuda, dequantizerowq2Kcuda, etc. in dequantize.cuh) inherit this int k parameter and use it as the kernel launch grid size:
cpp static void dequantizeblockcuda(..., const int k, cudaStreamt stream) { const int numblocks = (k + 2CUDADEQUANTIZEBLOCKSIZE - 1) / (2CUDADEQUANTIZEBLOCKSIZE); dequantizeblock<<<numblocks, CUDADEQUANTIZEBLOCKSIZE, 0, stream>>>(vx, y, k); }
In ggmldequantize() at ggufkernel.cu:85, the caller passes m n (an int64t product) to this int k parameter:
cpp at::Tensor DW = torch::empty({m, n}, options); // line 80: full-size, UNINITIALIZED // ... tocuda((void)W.dataptr(), (scalart)DW.dataptr(), m n, stream); // line 85: mn truncated to int
When m n > INTMAX, the truncated k is smaller than the actual tensor size. The kernel processes k elements. The remaining (m n) - k elements in DW are never written and contain stale GPU memory.
This is a single root cause -- the int type on the k parameter in tocudaggmlt -- with a single fix: change int k to int64t k. All dequantize functions inherit this type through the same typedef.
Affected Functions
All in csrc/quantization/gguf/ggufkernel.cu:
| Function | Line | Allocation | Info Disclosure? | |----------|------|-----------|-----------------| | ggmldequantize | 74 | torch::empty({m, n}) at line 80 | Yes -- mn truncated to int k at line 85 | | ggmlmulmatveca8 | 91 | torch::empty({vecs, row}) at line 99 | Yes -- int col = X.sizes()[1] at line 94 | | ggmlmulmata8 | 207 | torch::empty({batch, row}) at line 215 | Yes -- int col = X.sizes()[1] at line 210 | | ggmlmoea8 | 279 | torch::empty({tokenstopk, row}) at line 289 | Yes -- int col = X.sizes()[1] at line 285 |
All four functions allocate output tensors with torch::empty (uninitialized) and then run CUDA kernels that use truncated dimension values as loop bounds. The unfilled portion of each output tensor retains stale GPU memory.
ggmlmoea8vec (line 382) uses torch::zeros instead of torch::empty, so it is not affected by the info disclosure variant.
Impact: Information Disclosure in Multi-Tenant Serving
vLLM is designed for multi-tenant inference serving. GPU memory is reused across requests from different users. When the dequantize kernel partially fills an output tensor:
1. The output tensor DW is allocated with torch::empty -- the buffer contains whatever was previously in that GPU memory region 2. The dequantize kernel fills only a truncated portion of the buffer 3. The unfilled portion retains residual data from prior GPU operations, which may include tensor data from other users' inference requests 4. The contaminated tensor proceeds through the model computation 5. No error or warning is generated -- the partial fill is silent
This is a confidentiality violation. In shared inference deployments (the primary vLLM use case), one user's inference data can leak into another user's model computation through residual GPU memory.
Attacker Control
The attacker crafts a GGUF model file with weight tensor dimensions whose product exceeds INTMAX (e.g., a matrix with shape [65536, 65536] gives m n = 4,294,967,296). The model is hosted on HuggingFace or any model hub. The victim loads the model with vLLM for inference serving. The truncation happens automatically during model weight dequantization.
Fix
A fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/44971
Summary
vLLM's revision pinning controls do not consistently apply to all artifacts loaded for a model. A deployment that supplies --revision or --code-revision can still load dynamic code, GGUF files, image processors, retrieval side weights, or same-repository subfolder weights/config from an unpinned/default revision.
This is a supply-chain integrity issue for pinned vLLM deployments. Operators can believe they are serving a reviewed model revision while vLLM resolves behavior-affecting nested or sibling artifacts outside that reviewed revision.
Details
The expected invariant is:
When a vLLM operator supplies a model or code revision pin, every code, config, processor, weight file, side weight, and same-repository subfolder artifact loaded as part of that model should resolve under that pin unless vLLM exposes and enforces a separate explicit pin for that artifact.
Current main was verified affected at commit 3795d7acf431980e62e738493f437ae2a51549da.
Affected source boundaries:
- vllm/modelexecutor/models/registry.py:1045-1051 and :1058-1064 - tryresolvetransformers() passes revision=modelconfig.revision and trustremotecode=modelconfig.trustremotecode, but omits coderevision=modelconfig.coderevision for external automap dynamic module imports. - vllm/modelexecutor/modelloader/ggufloader.py:58-60 - The direct-file GGUF form repo/file.gguf calls hfhubdownload(repoid=repoid, filename=filename) without passing revision. - vllm/modelexecutor/models/roberta.py:203-209 - BGE-M3 secondary sparse and ColBERT side weights are declared with revision=None. - vllm/modelexecutor/models/kimik25.py:111-114 - Kimi-K2.5 calls cachedgetimageprocessor() without passing modelconfig.revision. - vllm/modelexecutor/models/kimiaudio.py:92-95 - Kimi-Audio loads Whisper config from the whisper-large-v3 subfolder without a revision argument. - vllm/modelexecutor/models/kimiaudio.py:425-430 - Kimi-Audio declares same-repository whisper-large-v3 secondary weights with revision=None. - vllm/modelexecutor/modelloader/defaultloader.py:287-301 - The default loader preserves modelconfig.revision for the primary source, then consumes model-supplied secondary sources as declared.
The strongest example is Kimi-Audio: the primary moonshotai/Kimi-Audio-7B-Instruct weights preserve the configured model revision, but the same-repository whisper-large-v3 audio tower config/weights do not. A pinned Kimi-Audio deployment can therefore load the Whisper subfolder outside the audited revision.
This report does not claim a trustremotecode=False bypass, unauthenticated RCE, or real artifact compromise. The issue is improper propagation of explicit artifact pins across supported loader paths.
Impact
Affected users are operators who pin vLLM model deployments to a reviewed Hugging Face revision for safety review, provenance, rollback, or reproducibility. The impact is that the pin does not reliably describe the full set of artifacts vLLM serves. Even when the operator selects an audited revision, vLLM can resolve behavior-affecting secondary artifacts from the repository default branch or another mutable ref.
Depending on the model path, the unpinned artifact can be dynamic model code, a GGUF file, an image processor, retrieval side weights, or the same-repository Kimi-Audio Whisper subfolder weights/config.
This breaks the operational guarantee of a pinned deployment: "serve the exact artifact set I reviewed." A later change to an unpinned secondary artifact can alter model behavior without changing the operator's configured revision, making review, rollback, incident response, and audit records unreliable.
Occurrences
- vllm/modelexecutor/models/kimik25.py L111-L114 — Kimi-K2.5 loads its image processor with cachedgetimageprocessor() but does not pass self.ctx.modelconfig.revision. The processor can therefore resolve from the default repository revision even when the model deployment is pinned. - vllm/modelexecutor/models/kimiaudio.py L425-L430 — Kimi-Audio declares same-repository whisper-large-v3 secondary weights with revision=None. A pinned Kimi-Audio deployment can therefore load the Whisper audio tower weights from an unpinned/default revision. - vllm/modelexecutor/models/kimiaudio.py L92-L95 — Kimi-Audio loads Whisper config from the same repository's whisper-large-v3 subfolder without passing the top-level model revision. The config for this behavior-affecting subcomponent can be resolved outside the audited model revision. - vllm/modelexecutor/models/registry.py L1058-L1064 — The later dynamic model-class resolution repeats the same pin-decay pattern: it forwards revision and trustremotecode, but omits coderevision. This means an operator-provided code pin is not enforced at the dynamic module loader boundary. - vllm/modelexecutor/modelloader/ggufloader.py L58-L60 — The direct GGUF form repo/file.gguf calls hfhubdownload(repoid=repoid, filename=filename) without passing modelconfig.revision. A deployment that pins the model revision can therefore resolve this GGUF file from the repository default revision. - vllm/modelexecutor/models/registry.py L1045-L1051 — trygetclassfromdynamicmodule() is called for external automap config/model classes with revision=modelconfig.revision, but without forwarding modelconfig.coderevision. When --code-revision is set, this dynamic module resolution can still fall back to the default code revision instead of the audited code revision. - vllm/modelexecutor/models/roberta.py L203-L209 — BgeM3EmbeddingModel creates same-repository secondary sparse/ColBERT weight sources with revision=None. The primary model revision is not propagated to these side weights, so they can be downloaded outside the operator-selected model revision.
Fixes
This was fixed in: https://github.com/vllm-project/vllm/pull/42616
Originally filed via huntr: https://huntr.com/bounties/3f1e24c0-87d2-4f6c-a705-820f380879ac.
The vLLM maintainer (Russell Bryant) redirected the report to the private GHSA channel. Offline proof bundle (vllmartifactpindecaybundleverify.py + bundle-verification-20260430T143506Z.json) is available upon request.
Summary
The extracthiddenstates speculative decoding proposer in vLLM returns a tensor with an incorrect shape after the first decode step, causing a RuntimeError that crashes the EngineCore process. The crash is triggered when any request in the batch uses sampling penalty parameters (repetitionpenalty, frequencypenalty, or presencepenalty).
A single request with a penalty parameter (e.g., "repetitionpenalty": 1.1) is sufficient to crash the server. The crash is deterministic and immediate — no concurrency, race condition, or special workload is required.
Details
In vLLM v0.17.0, the extracthiddenstates proposer's propose() method returned sampledtokenids.unsqueeze(-1), producing a tensor of shape (batchsize, 1).
In PR #37013 (first released in v0.18.0), the KV connector interface was refactored out of propose(). The return type changed from tuple[Tensor, KVConnectorOutput | None] to Tensor, and the .unsqueeze(-1) call was removed along with the KV connector output:
python Before (v0.17.0): return sampledtokenids.unsqueeze(-1), kvconnectoroutput # shape (batchsize, 1)
After (v0.18.0+): return sampledtokenids # shape (batchsize, 2) after first decode step
The refactor missed that sampledtokenids changed semantics between the first and subsequent decode steps. After the first decode step, the rejection sampler allocates its output as (batchsize, maxspeclen + 1). With numspeculativetokens=1, this produces shape (batchsize, 2) instead of the expected (batchsize, 1), causing a broadcast shape mismatch during penalty application.
Impact
Any vLLM deployment between v0.18.0 and v0.19.1 (inclusive) configured with extracthiddenstates speculative decoding is affected. A single API request containing any penalty parameter immediately and permanently crashes the EngineCore process, resulting in complete loss of service availability.
Patches
Fixed in PR #38610, first included in vLLM v0.20.0. The fix slices the return value to sampledtokenids[:, :1], ensuring the correct (batchsize, 1) shape regardless of the rejection sampler's output dimensions.
Workarounds
- Upgrade to vLLM v0.20.0 or later. - If upgrading is not possible, avoid using extracthiddenstates as the speculative decoding method on affected versions. - Alternatively, reject or strip penalty parameters (repetitionpenalty, frequencypenalty, presencepenalty) from incoming requests at an API gateway before they reach vLLM.
Summary When vLLM is configured to use Mooncake, unsafe deserialization exposed directly over ZMQ/TCP will allow attackers to execute remote code on distributed hosts.
Details 1. Pickle deserialization vulnerabilities are well documented. 2. The mooncake pipe is exposed over the network (by design to enable disaggregated prefilling across distributed environments) using ZMQ over TCP, greatly increasing exploitability. ~~Further, the mooncake integration opens these sockets listening on all interfaces on the host, meaning it can not be configured to only use a private, trusted network.~~
Only sendersocket and receiverack are allowed to be accessed publicly, while the data actually decompressed by pickle.loads() comes from recvbytes. Its interface is defined as self.receiversocket.connect(f\"tcp://{dhost}:{drankoffset + 1}\"), where dhost is decodehost, a locally defined address 192.168.0.139,from mooncake.json (https://github.com/kvcache-ai/Mooncake/blob/main/doc/en/vllm-integration-v0.2.md?plain=1#L36).
3. The root problem is recvtensor() calls recvimpl which passes the raw network bytes to pickle.loads(). Additionally, it does not appear that there are any controls (network, authentication, etc) to prevent arbitrary users from sending this payload to the affected service.
Impact This is a remote code execution vulnerability impacting any deployments using Mooncake to distribute KV across distributed hosts.
Remediation This issue is resolved by https://github.com/vllm-project/vllm/pull/14228