[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>
This commit is contained in:
Juan Pérez de Algaba
2026-09-04 12:02:41 +00:00
committed by GitHub
co-authored by Isotr0py
parent c615b1fd67
commit 8b6de0eb9a
2 changed files with 60 additions and 3 deletions
+54
View File
@@ -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
+6 -3
View File
@@ -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