CVE-2026-69147
vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation
vLLM is an inference and serving engine for large language models. Prior to 0.28.0, request bodies for Chat Completions and Responses can set media_io_kwargs.video.video_backend to pynvvideocodec, and MediaConnector.fetch_video forwards that choice to VideoMediaIO even when startup configuration selected a software decoder. The engine's _reserve_mm_ipc_gpu_memory logic budgets decoder memory only from static configuration, so the request-selected VIDEO_LOADER_REGISTRY backend can create a CUDA context, decoder surfaces, and decoded-frame allocations that were not removed from the engine's KV-cache budget. An attacker able to submit video requests to a video-capable GPU deployment with PyNvVideoCodec installed can exhaust shared GPU memory, causing request failures, worker crashes, or denial of service. The first release containing the fix is version 0.28.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
6.5
vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation
vLLM is an inference and serving engine for large language models. Prior to 0.28.0, request bodies for Chat Completions and Responses can set media_io_kwargs.video.video_backend to pynvvideocodec, and MediaConnector.fetch_video forwards that choice to VideoMediaIO even when startup configuration selected a software decoder. The engine's _reserve_mm_ipc_gpu_memory logic budgets decoder memory only from static configuration, so the request-selected VIDEO_LOADER_REGISTRY backend can create a CUDA context, decoder surfaces, and decoded-frame allocations that were not removed from the engine's KV-cache budget. An attacker able to submit video requests to a video-capable GPU deployment with PyNvVideoCodec installed can exhaust shared GPU memory, causing request failures, worker crashes, or denial of service. The first release containing the fix is version 0.28.0.
- Record
CVE-2026-69147- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-770
- Updated
- 2026-11-02
EUVDEUVD-2026-80935
6.5
vLLM is an inference and serving engine for large language models. Prior to 0.28.0, request bodies for Chat Completions and Responses can set media_io_kwargs.video.video_backend to pynvvideocodec, and MediaConnector.fetch_video forwards that choice to VideoMediaIO even when startup configuration selected a software decoder. The engine's _reserve_mm_ipc_gpu_memory logic budgets decoder memory only from static configuration, so the request-selected VIDEO_LOADER_REGISTRY backend can create a CUDA context, decoder surfaces, and decoded-frame allocations that were not removed from the engine's KV-cache budget. An attacker able to submit video requests to a video-capable GPU deployment with PyNvVideoCodec installed can exhaust shared GPU memory, causing request failures, worker crashes, or denial of service. The first release containing the fix is version 0.28.0.
- Record
EUVD-2026-80935- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-8pw2-6jv3-mj5j
6.5
vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation
Summary
Current vLLM main lets an inference request choose the PyNvVideoCodec GPU video decoder through media_io_kwargs.video.video_backend, but engine GPU memory reservation is computed only from static startup configuration and VLLM_VIDEO_LOADER_BACKEND. 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 media_io_kwargs, and those values are forwarded to the shared media connector. For video inputs, MediaConnector.fetch_video() copies self.media_io_kwargs["video"] into video_io_kwargs, only setting a model-derived backend when video_backend is absent. VideoMediaIO.__init__() then consumes video_backend from those kwargs and loads that backend from VIDEO_LOADER_REGISTRY.
The relevant request-side source path is:
video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))
if "video_backend" not in video_io_kwargs and (
video_backend := get_video_loader_backend_for_processor(video_processor)
):
video_io_kwargs["video_backend"] = video_backend
video_io = VideoMediaIO(image_io, **video_io_kwargs)
video_loader_backend = (
kwargs.pop("video_backend", None) or envs.VLLM_VIDEO_LOADER_BACKEND
)
self.video_loader = VIDEO_LOADER_REGISTRY.load(video_loader_backend)
VideoBackend.load_bytes() then dispatches backend == "pynvvideocodec" into decode_frames_pynvvideocodec(), 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:
def _uses_pynvvideocodec_video_backend(mm_config) -> bool:
video_kwargs = mm_config.media_io_kwargs.get("video", {})
video_loader_backend = (
video_kwargs.get("video_backend") or envs.VLLM_VIDEO_LOADER_BACKEND
)
codec_backend = video_kwargs.get("backend")
return (
video_loader_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
or codec_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
)
decoder_reserved_bytes = (
num_api_servers * per_server_decoder_bytes
if self._uses_pynvvideocodec_video_backend(mm_config)
else 0
)
reserved_bytes = raw_frame_reserved_bytes + decoder_reserved_bytes
if reserved_bytes <= 0:
return available_kv_cache_memory_bytes
With default static video configuration, mm_config.media_io_kwargs["video"] does not name PyNvVideoCodec and VLLM_VIDEO_LOADER_BACKEND defaults to OpenCV/software decoding. The worker therefore reserves no PyNv decoder/CUDA-context bytes. A later request can still set media_io_kwargs.video.video_backend="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:
{
"model": "served-vlm",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "summarize this clip"},
{"type": "video_url", "video_url": {"url": "data:video/mp4;base64,<small-mp4>"}}
]
}
],
"media_io_kwargs": {
"video": {
"video_backend": "pynvvideocodec"
}
}
}
The following bounded source-level check…
- Record
GHSA-8pw2-6jv3-mj5j- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-770
- Updated
- 2026-11-02
NVD
6.5
vLLM is an inference and serving engine for large language models. Prior to 0.28.0, request bodies for Chat Completions and Responses can set media_io_kwargs.video.video_backend to pynvvideocodec, and MediaConnector.fetch_video forwards that choice to VideoMediaIO even when startup configuration selected a software decoder. The engine's _reserve_mm_ipc_gpu_memory logic budgets decoder memory only from static configuration, so the request-selected VIDEO_LOADER_REGISTRY backend can create a CUDA context, decoder surfaces, and decoded-frame allocations that were not removed from the engine's KV-cache budget. An attacker able to submit video requests to a video-capable GPU deployment with PyNvVideoCodec installed can exhaust shared GPU memory, causing request failures, worker crashes, or denial of service. The first release containing the fix is version 0.28.0.
- Record
CVE-2026-69147- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-770
- Updated
- 2026-11-02
OSV
6.5
vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation
vLLM is an inference and serving engine for large language models. Prior to 0.28.0, request bodies for Chat Completions and Responses can set media_io_kwargs.video.video_backend to pynvvideocodec, and MediaConnector.fetch_video forwards that choice to VideoMediaIO even when startup configuration selected a software decoder. The engine's _reserve_mm_ipc_gpu_memory logic budgets decoder memory only from static configuration, so the request-selected VIDEO_LOADER_REGISTRY backend can create a CUDA context, decoder surfaces, and decoded-frame allocations that were not removed from the engine's KV-cache budget. An attacker able to submit video requests to a video-capable GPU deployment with PyNvVideoCodec installed can exhaust shared GPU memory, causing request failures, worker crashes, or denial of service. The first release containing the fix is version 0.28.0.
- Record
CVE-2026-69147- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-770
- Updated
- 2026-11-02
OSVGHSA-8pw2-6jv3-mj5j
6.5
vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation
Summary
Current vLLM main lets an inference request choose the PyNvVideoCodec GPU video decoder through media_io_kwargs.video.video_backend, but engine GPU memory reservation is computed only from static startup configuration and VLLM_VIDEO_LOADER_BACKEND. 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 media_io_kwargs, and those values are forwarded to the shared media connector. For video inputs, MediaConnector.fetch_video() copies self.media_io_kwargs["video"] into video_io_kwargs, only setting a model-derived backend when video_backend is absent. VideoMediaIO.__init__() then consumes video_backend from those kwargs and loads that backend from VIDEO_LOADER_REGISTRY.
The relevant request-side source path is:
video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))
if "video_backend" not in video_io_kwargs and (
video_backend := get_video_loader_backend_for_processor(video_processor)
):
video_io_kwargs["video_backend"] = video_backend
video_io = VideoMediaIO(image_io, **video_io_kwargs)
video_loader_backend = (
kwargs.pop("video_backend", None) or envs.VLLM_VIDEO_LOADER_BACKEND
)
self.video_loader = VIDEO_LOADER_REGISTRY.load(video_loader_backend)
VideoBackend.load_bytes() then dispatches backend == "pynvvideocodec" into decode_frames_pynvvideocodec(), 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:
def _uses_pynvvideocodec_video_backend(mm_config) -> bool:
video_kwargs = mm_config.media_io_kwargs.get("video", {})
video_loader_backend = (
video_kwargs.get("video_backend") or envs.VLLM_VIDEO_LOADER_BACKEND
)
codec_backend = video_kwargs.get("backend")
return (
video_loader_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
or codec_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
)
decoder_reserved_bytes = (
num_api_servers * per_server_decoder_bytes
if self._uses_pynvvideocodec_video_backend(mm_config)
else 0
)
reserved_bytes = raw_frame_reserved_bytes + decoder_reserved_bytes
if reserved_bytes <= 0:
return available_kv_cache_memory_bytes
With default static video configuration, mm_config.media_io_kwargs["video"] does not name PyNvVideoCodec and VLLM_VIDEO_LOADER_BACKEND defaults to OpenCV/software decoding. The worker therefore reserves no PyNv decoder/CUDA-context bytes. A later request can still set media_io_kwargs.video.video_backend="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:
{
"model": "served-vlm",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "summarize this clip"},
{"type": "video_url", "video_url": {"url": "data:video/mp4;base64,<small-mp4>"}}
]
}
],
"media_io_kwargs": {
"video": {
"video_backend": "pynvvideocodec"
}
}
}
The following bounded source-level check…
- Record
GHSA-8pw2-6jv3-mj5j- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-770
- Updated
- 2026-11-02
OSVPYSEC-2026-4178
6.5
vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation
Summary
Current vLLM main lets an inference request choose the PyNvVideoCodec GPU video decoder through media_io_kwargs.video.video_backend, but engine GPU memory reservation is computed only from static startup configuration and VLLM_VIDEO_LOADER_BACKEND. 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 media_io_kwargs, and those values are forwarded to the shared media connector. For video inputs, MediaConnector.fetch_video() copies self.media_io_kwargs["video"] into video_io_kwargs, only setting a model-derived backend when video_backend is absent. VideoMediaIO.__init__() then consumes video_backend from those kwargs and loads that backend from VIDEO_LOADER_REGISTRY.
The relevant request-side source path is:
video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))
if "video_backend" not in video_io_kwargs and (
video_backend := get_video_loader_backend_for_processor(video_processor)
):
video_io_kwargs["video_backend"] = video_backend
video_io = VideoMediaIO(image_io, **video_io_kwargs)
video_loader_backend = (
kwargs.pop("video_backend", None) or envs.VLLM_VIDEO_LOADER_BACKEND
)
self.video_loader = VIDEO_LOADER_REGISTRY.load(video_loader_backend)
VideoBackend.load_bytes() then dispatches backend == "pynvvideocodec" into decode_frames_pynvvideocodec(), 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:
def _uses_pynvvideocodec_video_backend(mm_config) -> bool:
video_kwargs = mm_config.media_io_kwargs.get("video", {})
video_loader_backend = (
video_kwargs.get("video_backend") or envs.VLLM_VIDEO_LOADER_BACKEND
)
codec_backend = video_kwargs.get("backend")
return (
video_loader_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
or codec_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
)
decoder_reserved_bytes = (
num_api_servers * per_server_decoder_bytes
if self._uses_pynvvideocodec_video_backend(mm_config)
else 0
)
reserved_bytes = raw_frame_reserved_bytes + decoder_reserved_bytes
if reserved_bytes <= 0:
return available_kv_cache_memory_bytes
With default static video configuration, mm_config.media_io_kwargs["video"] does not name PyNvVideoCodec and VLLM_VIDEO_LOADER_BACKEND defaults to OpenCV/software decoding. The worker therefore reserves no PyNv decoder/CUDA-context bytes. A later request can still set media_io_kwargs.video.video_backend="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:
{
"model": "served-vlm",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "summarize this clip"},
{"type": "video_url", "video_url": {"url": "data:video/mp4;base64,<small-mp4>"}}
]
}
],
"media_io_kwargs": {
"video": {
"video_backend": "pynvvideocodec"
}
}
}
The following bounded source-level check…
- Record
PYSEC-2026-4178- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
vllmPyPI |
before 0.28.0 | 0.28.0 |
vllmPyPI |
before 0.28.0, plus listed versions 0.0.10.1.00.1.10.1.20.1.3 Show 45 moreShow less0.1.40.1.50.1.60.1.70.10.00.10.10.10.1.10.10.20.11.00.11.10.11.20.12.00.13.00.14.00.14.10.15.00.15.10.16.00.17.00.17.10.18.00.18.10.19.00.19.10.2.00.2.10.2.1.post10.2.20.2.30.2.40.2.50.2.60.2.70.20.00.20.10.20.20.21.00.22.00.22.10.23.00.24.00.25.00.25.10.26.00.27.0 |
0.28.0 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-69147 is a medium vulnerability (CVSS 6.5) in Vllm, published September 16, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.58%, the 48th percentile. Fixed in vllm 0.28.0.
References
github.com/vllm-project/vllm/security/advisories/GHSA-8pw2-6jv3-mj5j
github.com/vllm-project/vllm/pull/47259
github.com/vllm-project/vllm/commit/283893c72292ede38d277e3cd2b9b64c3e4f1dda
github.com/vllm-project/vllm/commit/ba22152096b2484faa3579624a253d54804d876d
nvd.nist.gov/vuln/detail/CVE-2026-69147
github.com/vllm-project/vllm/releases/tag/v0.25.0
Left out here: 45 versions and the rest of 3 source descriptions. The JSON record has them.