fix(security): enforce audio decode duration limit in NanoNemotronVL (#50221)

Signed-off-by: jperezde <jperezde@redhat.com>

Juan Pérez de Algaba committed Aug 13, 2026 at 12:31 UTC 37c3bdf5a7ef379ac4cc9678a46b6bc26479be12
2 files changed +63 -2
tests/models/multimodal/test_nano_nemotron_vl.py
+56 -1
@@ -1,9 +1,17 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +from unittest.mock import patch
5 +
6 +import numpy as np
7 import pytest
8
6 -from vllm.model_executor.models.nano_nemotron_vl import NemotronH_Nano_VL_V2
9 +from vllm import envs
10 +from vllm.model_executor.models.nano_nemotron_vl import (
11 + NanoNemotronVLMultiModalProcessor,
12 + NemotronH_Nano_VL_V2,
13 +)
14 +from vllm.multimodal.parse import MultiModalDataItems, VideoProcessorItems
15
16
17 class _TextOnlyMultiModalConfig:
@@ -125,3 +133,50 @@ def test_nano_nemotron_vl_requires_sound_encoder_for_sound_weights():
133
134 with pytest.raises(AssertionError):
135 model.load_weights([("sound_encoder.encoder.weight", object())])
136 +
137 +
138 +def _make_mm_items_with_video_bytes(
139 + video_bytes: bytes,
140 +) -> MultiModalDataItems:
141 + """Build a minimal MultiModalDataItems with one video entry."""
142 + items = MultiModalDataItems()
143 + items["video"] = VideoProcessorItems(
144 + data=[None],
145 + metadata=[{"original_video_bytes": video_bytes}],
146 + )
147 + return items
148 +
149 +
150 +def test_extract_audio_from_videos_passes_max_duration():
151 + """_extract_audio_from_videos must forward VLLM_MAX_AUDIO_DECODE_DURATION_S
152 + to load_audio_pyav so decompression-bomb audio is rejected."""
153 + dummy_audio = (np.zeros(16000, dtype=np.float32), 16000.0)
154 + mm_items = _make_mm_items_with_video_bytes(b"\x00" * 64)
155 +
156 + processor = object.__new__(NanoNemotronVLMultiModalProcessor)
157 +
158 + target = "vllm.model_executor.models.nano_nemotron_vl.load_audio_pyav"
159 + with patch(target, return_value=dummy_audio) as mock_load:
160 + processor._extract_audio_from_videos(mm_items)
161 +
162 + mock_load.assert_called_once()
163 + _, kwargs = mock_load.call_args
164 + assert kwargs["max_duration_s"] == envs.VLLM_MAX_AUDIO_DECODE_DURATION_S
165 +
166 +
167 +def test_extract_audio_from_videos_rejects_oversized_audio():
168 + """When load_audio_pyav raises due to duration limit the video is
169 + marked as having no audio instead of crashing the server."""
170 + mm_items = _make_mm_items_with_video_bytes(b"\x00" * 64)
171 +
172 + processor = object.__new__(NanoNemotronVLMultiModalProcessor)
173 +
174 + target = "vllm.model_executor.models.nano_nemotron_vl.load_audio_pyav"
175 + with patch(
176 + target,
177 + side_effect=ValueError("Audio exceeds maximum allowed duration"),
178 + ):
179 + _, audio_items, has_audio = processor._extract_audio_from_videos(mm_items)
180 +
181 + assert audio_items == []
182 + assert has_audio == [False]
vllm/model_executor/models/nano_nemotron_vl.py
+7 -1
@@ -18,6 +18,7 @@ import torch
18 import torch.nn as nn
19 from transformers import BatchFeature, PretrainedConfig
20
21 +from vllm import envs
22 from vllm.config import VllmConfig
23 from vllm.config.multimodal import BaseDummyOptions, VideoDummyOptions
24 from vllm.inputs import MultiModalDataDict, MultiModalInput
@@ -627,7 +628,12 @@ class NanoNemotronVLMultiModalProcessor(
628 "the chat API with a model that sets use_audio_in_video)."
629 )
630 try:
630 - audio_items.append(load_audio_pyav(BytesIO(video_bytes)))
631 + audio_items.append(
632 + load_audio_pyav(
633 + BytesIO(video_bytes),
634 + max_duration_s=envs.VLLM_MAX_AUDIO_DECODE_DURATION_S,
635 + )
636 + )
637 has_audio.append(True)
638 except Exception:
639 logger.debug(