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(