[Security] Cap GLMGA video sampling to prevent request-driven resource exhaustion (#54935)

Signed-off-by: jperezde <jperezde@redhat.com> Signed-off-by: Isotr0py <Isotr0py@outlook.com> Co-authored-by: Isotr0py <Isotr0py@outlook.com>

Juan Pérez de Algaba committed Sep 4, 2026 at 14:02 UTC 8b6de0eb9a09ef53f20cf06bd4d17ee264b9c2a7
2 files changed +60 -3
tests/multimodal/test_video.py
+54
@@ -22,6 +22,7 @@ from vllm.multimodal.video import (
22 DynamicVideoBackend,
23 Glm5NextVideoBackend,
24 GLM46VVideoBackend,
25 + GLMGAVideoBackend,
26 Molmo2VideoBackend,
27 Qwen2VLVideoBackend,
28 Qwen3VLVideoBackend,
@@ -1473,6 +1474,59 @@ def test_glm46v_duration_estimation_from_fps():
1474 assert all(0 <= idx < 90 for idx in indices)
1475
1476
1477 +class TestGLMGASamplingCaps:
1478 + """Regression tests for GHSA-58v5-2m8f-94pr: request-controlled fps
1479 + and max_frames must not create intermediate allocations larger than
1480 + the actual frame count."""
1481 +
1482 + @staticmethod
1483 + def _source(
1484 + total_frames: int, fps: float = 30.0, duration: float = 0
1485 + ) -> VideoSourceMetadata:
1486 + if duration == 0 and fps > 0 and total_frames > 1:
1487 + duration = round((total_frames - 1) / fps) + 1
1488 + return VideoSourceMetadata(total_frames, fps, duration)
1489 +
1490 + def test_extreme_values_bounded_by_total_frames(self):
1491 + source = self._source(total_frames=2, fps=2.0, duration=1.0)
1492 + target = VideoTargetMetadata(num_frames=-1, fps=500_000, max_duration=-1)
1493 + indices = GLMGAVideoBackend.compute_frames_index_to_sample(
1494 + source,
1495 + target,
1496 + max_frames=500_000,
1497 + )
1498 + assert len(indices) <= 2
1499 +
1500 + def test_class_cap_overrides_kwargs_max_frames(self):
1501 + source = self._source(total_frames=10_000, fps=30.0)
1502 + target = VideoTargetMetadata(num_frames=-1, fps=30, max_duration=-1)
1503 + indices = GLMGAVideoBackend.compute_frames_index_to_sample(
1504 + source,
1505 + target,
1506 + max_frames=100_000,
1507 + )
1508 + assert len(indices) <= GLMGAVideoBackend._MAX_FRAMES
1509 +
1510 + def test_class_cap_overrides_target_fps(self):
1511 + source = self._source(total_frames=10_000, fps=30.0)
1512 + target = VideoTargetMetadata(num_frames=-1, fps=500_000, max_duration=-1)
1513 + indices = GLMGAVideoBackend.compute_frames_index_to_sample(
1514 + source,
1515 + target,
1516 + )
1517 + assert len(indices) <= GLMGAVideoBackend._MAX_FRAMES
1518 +
1519 + def test_normal_operation_unchanged(self):
1520 + source = self._source(total_frames=1000, fps=30.0, duration=33.0)
1521 + target = VideoTargetMetadata(num_frames=-1, fps=2, max_duration=-1)
1522 + indices = GLMGAVideoBackend.compute_frames_index_to_sample(
1523 + source,
1524 + target,
1525 + )
1526 + assert 0 < len(indices) <= GLMGAVideoBackend._MAX_FRAMES
1527 + assert all(0 <= idx < 1000 for idx in indices)
1528 +
1529 +
1530 def test_glm5next_backend_selected_for_processor():
1531 """Glm5NextVideoProcessor maps to the glm5next loader so only the
1532 sampled frames are decoded instead of the whole container. Both the
vllm/multimodal/video.py
+6 -3
@@ -781,6 +781,9 @@ class Glm5NextVideoBackend(VideoBackend):
781 video_processor="GlmgaVideoProcessor",
782 )
783 class GLMGAVideoBackend(VideoBackend):
784 + _MAX_FRAMES: ClassVar[int] = 640
785 + _MAX_FPS: ClassVar[int] = 30
786 +
787 @classmethod
788 def _prepare_source(cls, source: VideoSourceMetadata) -> VideoSourceMetadata:
789 # Estimate duration from frame count and fps when the container
@@ -806,14 +809,14 @@ class GLMGAVideoBackend(VideoBackend):
809 total_frames_num = source.total_frames_num
810 duration = source.duration
811 original_fps = source.original_fps
809 - target_fps = target.fps
812 + target_fps = min(target.fps, cls._MAX_FPS)
813 max_frame_idx = source.total_frames_num - 1
811 - max_frames = kwargs.get("max_frames", 640)
814 + max_frames = min(kwargs.get("max_frames", cls._MAX_FRAMES), cls._MAX_FRAMES)
815
816 duration = duration or round(max_frame_idx / original_fps) + 1
817
818 extract_t = int(duration * target_fps)
816 - extract_t = min(extract_t, max_frames)
819 + extract_t = min(extract_t, max_frames, total_frames_num)
820
821 duration_per_frame = 1 / original_fps
822