[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