From 8b6de0eb9a09ef53f20cf06bd4d17ee264b9c2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20P=C3=A9rez=20de=20Algaba?= <124347725+jperezdealgaba@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:02:41 +0200 Subject: [PATCH] [Security] Cap GLMGA video sampling to prevent request-driven resource exhaustion (#54935) Signed-off-by: jperezde Signed-off-by: Isotr0py Co-authored-by: Isotr0py --- tests/multimodal/test_video.py | 54 ++++++++++++++++++++++++++++++++++ vllm/multimodal/video.py | 9 ++++-- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/tests/multimodal/test_video.py b/tests/multimodal/test_video.py index cd42c52b3f..9c24b5f969 100644 --- a/tests/multimodal/test_video.py +++ b/tests/multimodal/test_video.py @@ -22,6 +22,7 @@ from vllm.multimodal.video import ( DynamicVideoBackend, Glm5NextVideoBackend, GLM46VVideoBackend, + GLMGAVideoBackend, Molmo2VideoBackend, Qwen2VLVideoBackend, Qwen3VLVideoBackend, @@ -1473,6 +1474,59 @@ def test_glm46v_duration_estimation_from_fps(): assert all(0 <= idx < 90 for idx in indices) +class TestGLMGASamplingCaps: + """Regression tests for GHSA-58v5-2m8f-94pr: request-controlled fps + and max_frames must not create intermediate allocations larger than + the actual frame count.""" + + @staticmethod + def _source( + total_frames: int, fps: float = 30.0, duration: float = 0 + ) -> VideoSourceMetadata: + if duration == 0 and fps > 0 and total_frames > 1: + duration = round((total_frames - 1) / fps) + 1 + return VideoSourceMetadata(total_frames, fps, duration) + + def test_extreme_values_bounded_by_total_frames(self): + source = self._source(total_frames=2, fps=2.0, duration=1.0) + target = VideoTargetMetadata(num_frames=-1, fps=500_000, max_duration=-1) + indices = GLMGAVideoBackend.compute_frames_index_to_sample( + source, + target, + max_frames=500_000, + ) + assert len(indices) <= 2 + + def test_class_cap_overrides_kwargs_max_frames(self): + source = self._source(total_frames=10_000, fps=30.0) + target = VideoTargetMetadata(num_frames=-1, fps=30, max_duration=-1) + indices = GLMGAVideoBackend.compute_frames_index_to_sample( + source, + target, + max_frames=100_000, + ) + assert len(indices) <= GLMGAVideoBackend._MAX_FRAMES + + def test_class_cap_overrides_target_fps(self): + source = self._source(total_frames=10_000, fps=30.0) + target = VideoTargetMetadata(num_frames=-1, fps=500_000, max_duration=-1) + indices = GLMGAVideoBackend.compute_frames_index_to_sample( + source, + target, + ) + assert len(indices) <= GLMGAVideoBackend._MAX_FRAMES + + def test_normal_operation_unchanged(self): + source = self._source(total_frames=1000, fps=30.0, duration=33.0) + target = VideoTargetMetadata(num_frames=-1, fps=2, max_duration=-1) + indices = GLMGAVideoBackend.compute_frames_index_to_sample( + source, + target, + ) + assert 0 < len(indices) <= GLMGAVideoBackend._MAX_FRAMES + assert all(0 <= idx < 1000 for idx in indices) + + def test_glm5next_backend_selected_for_processor(): """Glm5NextVideoProcessor maps to the glm5next loader so only the sampled frames are decoded instead of the whole container. Both the diff --git a/vllm/multimodal/video.py b/vllm/multimodal/video.py index a02daa096e..2cc2aa9887 100644 --- a/vllm/multimodal/video.py +++ b/vllm/multimodal/video.py @@ -781,6 +781,9 @@ class Glm5NextVideoBackend(VideoBackend): video_processor="GlmgaVideoProcessor", ) class GLMGAVideoBackend(VideoBackend): + _MAX_FRAMES: ClassVar[int] = 640 + _MAX_FPS: ClassVar[int] = 30 + @classmethod def _prepare_source(cls, source: VideoSourceMetadata) -> VideoSourceMetadata: # Estimate duration from frame count and fps when the container @@ -806,14 +809,14 @@ class GLMGAVideoBackend(VideoBackend): total_frames_num = source.total_frames_num duration = source.duration original_fps = source.original_fps - target_fps = target.fps + target_fps = min(target.fps, cls._MAX_FPS) max_frame_idx = source.total_frames_num - 1 - max_frames = kwargs.get("max_frames", 640) + max_frames = min(kwargs.get("max_frames", cls._MAX_FRAMES), cls._MAX_FRAMES) duration = duration or round(max_frame_idx / original_fps) + 1 extract_t = int(duration * target_fps) - extract_t = min(extract_t, max_frames) + extract_t = min(extract_t, max_frames, total_frames_num) duration_per_frame = 1 / original_fps