Prhub

#50755 fix(security): classify DeepStream as GPU backend and enforce pixel limits

原始 PR 作者 jperezdealgaba 合并时间 2026-08-03 16:23 文件变更 3 提交数 2 评论 2 代码增减 +35 / -0

执行摘要

DeepStream 归类为 GPU 后端,补像素限制封堵请求绕过

PR body 明确指出三重安全缺口:DeepStream was not registered as a GPU-requiring codec, allowing request-level activation of NVDEC decoding without startup configuration or VRAM reservation;The decode path also skipped VLLM_MAX_IMAGE_PIXELS enforcement;以及 accepted request-controlled pool_size for the process-wide DecodePool singleton。即请求方可通过运行时 media_io_kwargs 指定 backend=deepstream,既不经过启动时的 VRAM 预留与校验,也不受像素上限限制,还能操控进程级解码池规模,构成资源滥用与潜在 DoS 面。修复目标是把 3 类资源敏感行为全部收敛到启动阶段。

值得精读。这是一份'小改动、大影响'的安全修复:3 个文件、35 行新增即闭合了请求级绕过 GPU 配置与像素限制的漏洞。核心看点有两个:一是 register_gpu_codecregister 装饰器并存的设计,如何在零依赖前提下扩展注册表语义;二是提交历史中 fail-closed 默认值与显式注册方案的往复,展示了安全默认值与向后兼容之间的真实权衡(最终选择显式注册以保护 pyav/torchcodec 等软件解码路径)。建议后续为 GPU codec 增加注册一致性断言,并补充 DeepStream 分支的集成测试(含可选依赖标记)。

讨论亮点

核心讨论集中在 backend_requires_gpu 默认值的取舍上。第一版 commit 采用 fail-closed(未知名称默认 True),DarkLight1337 评论质疑:Is it intended that you set the default to True? Why not decorate the class explicitly? 作者随即在第二个 commit 中回退默认值为 False,改为显式 register_gpu_codec('deepstream'):原因是 pyavtorchcodec 等软件 codec 从未注册进 loader registry,若未知名称默认 True 会误拦截这些合法的请求级后端选择。最终 Isotr0py 与 DarkLight1337 均批准合并。未解决的疑虑:DeepStream 分支的像素限制依赖 probe_metadata 返回的宽高,单测未直接覆盖该可选依赖路径。

实现拆解

  1. 注册机制扩展(vllm/multimodal/video.py)VideoLoaderRegistry 新增 register_gpu_codec(name) 方法,仅将名称写入 _requires_gpu 标记字典而不注册 loader 类;模块加载期立即执行 VIDEO_LOADER_REGISTRY.register_gpu_codec('deepstream')。这样 merge_kwargs 中既有的 GPU 后端拦截逻辑即可对 DeepStream 生效——请求级指定且静态配置(default_kwargs)不一致时剥离并告警一次。
  2. 运行时资源键收敛(vllm/multimodal/media/video.py)merge_kwargs 在剥离 hw_decoders 的同一位置新增 runtime_kwargs.pop('pool_size', None)。理由在 DeepStream 分支注释中已写明:DecodePool 是进程级单例,首个解码请求的 pool_size 值全局生效,因此该键只能由启动值控制。
  3. 像素上限补强(vllm/multimodal/video.py):DeepStream 分支在 probe_metadata 返回帧宽高后新增 _check_frame_pixel_limit(_w, _h),使该路径与 pynvvideocodec 等其他后端一致受 VLLM_MAX_IMAGE_PIXELS 约束,超大帧在进入解码/显存分配前即被拒绝。
  4. 测试配套(tests/multimodal/media/test_video.py):新增 5 个单测,覆盖 GPU 标记生效(test_deepstream_requires_gpu)、未静态配置时请求级指定被剥离(test_strips_backend_deepstream_when_not_static)、静态配置时保留并合并其他键(test_preserves_backend_deepstream_when_static)、pool_size 被剥离(test_strips_pool_size_from_runtime)、未知后端默认不视为 GPU(test_unknown_backend_not_treated_as_gpu)。
文件 模块 状态 重要度
vllm/multimodal/video.py 视频解码 modified 5.81
vllm/multimodal/media/video.py 视频加载 modified 4.58
tests/multimodal/media/test_video.py 视频测试 modified 6.09

关键符号

register_gpu_codec backend_requires_gpu merge_kwargs _check_frame_pixel_limit load_bytes

关键源码片段

vllm/multimodal/video.py core-logic

核心安全修复所在:新增 register_gpu_codec 方法并显式注册 deepstream 为 GPU codec,DeepStream 解码分支补充像素上限检查。

# vllm/multimodal/video.py —— VideoLoaderRegistry 与 load_bytes 的 DeepStream 分支(节选)class VideoLoaderRegistry:
    """视频加载后端注册表:管理后端 → loader 类/处理器映射与 GPU 依赖标记。"""
​
    def register_gpu_codec(self, name: str) -> None:
        """标记某 codec 需要 GPU,但不注册具体 loader 类。        DeepStream 的 loader 来自可选依赖包 nvidia.deepstream_videodecode,
        无法在模块加载期用装饰器注册,因此提供该方法让注册表在零依赖
        前提下完成 GPU 标记,供 merge_kwargs 做请求级拦截。
        """
        self._requires_gpu[name] = True
​
    def backend_requires_gpu(self, name: str) -> bool:
        # 默认返回 False(fail-open):pyav、torchcodec 等软件 codec
        # 从未进入注册表,若对未知名称默认 True 会误拦截它们合法的
        # 请求级选择(这是提交历史中曾尝试又回退的改动)。
        return self._requires_gpu.get(name, False)
​
​
# 显式安全注册:请求级指定 backend=deepstream 时,merge_kwargs
# 会将其剥离,除非该后端已在启动时静态配置。
VIDEO_LOADER_REGISTRY = VideoLoaderRegistry()
VIDEO_LOADER_REGISTRY.register_gpu_codec('deepstream')
​
​
# —— load_bytes 中的 DeepStream 分支 ——
elif backend == 'deepstream':
    assert not frame_recovery, (
        'frame_recovery is only available for `opencv` backend'
    )
    # DecodePool 是进程级单例:首个请求的 pool_size 会固定全局,
    # 因此从运行时 kwargs 取出,避免请求方控制解码池规模。
    pool_size = kwargs.pop('pool_size', None)
​
    # 通过 GStreamer 探测容器元数据(来自 deepstream 视频解码 wheel)
    from nvidia.deepstream_videodecode import probe_metadata
​
    total_frames, original_fps, duration, _w, _h, codec = probe_metadata(data)
    # 与其他 codec 路径保持一致:解码前先校验帧像素上限
    # (VLLM_MAX_IMAGE_PIXELS),防止超大帧耗尽显存与内存。
    _check_frame_pixel_limit(_w, _h)
    source = cls._prepare_source(
        VideoSourceMetadata(
            total_frames_num=total_frames,
            original_fps=original_fps,
            duration=duration,
        )
    )
    frame_idx = cls.compute_frames_index_to_sample(source=source, target=target, **kwargs)
    frames, valid = cls.decode_indices(data, frame_idx, source, codec=codec, pool_size=pool_size)
vllm/multimodal/media/video.py core-logic

merge_kwargs 新增剥离请求级 pool_size,防止请求控制进程级 DecodePool 单例。

# vllm/multimodal/media/video.py —— VideoMediaIO.merge_kwargs(节选)@classmethod
def merge_kwargs(
    cls,
    default_kwargs: dict[str, Any] | None,
    runtime_kwargs: dict[str, Any] | None,
) -> dict[str, Any]:
    if runtime_kwargs:
        # 拷贝一份,避免污染调用方传入的原始 dict
        runtime_kwargs = dict(runtime_kwargs)
​
        # 资源敏感配置只允许来自启动配置:hw_decoders 决定显存预留,
        # pool_size 决定进程级 DecodePool 单例规模,均不接受请求覆盖。
        runtime_kwargs.pop('hw_decoders', None)
        runtime_kwargs.pop('pool_size', None)
​
        # 拦截请求级选择的 GPU 后端:仅当静态配置(default_kwargs)
        # 与请求值一致时才放行,否则剥离该键并记录一次告警。
        for key in ('video_backend', 'backend'):
            requested = runtime_kwargs.get(key)
            if requested and VIDEO_LOADER_REGISTRY.backend_requires_gpu(requested):
                static_val = (default_kwargs or {}).get(key)
                if static_val != requested:
                    logger.warning_once(
                        'Stripping request-level %s=%r: GPU video '
                        'backend not configured at startup.',
                        key,
                        requested,
                    )
                    runtime_kwargs = {
                        k: v for k, v in runtime_kwargs.items() if k != key
                    }
​
    merged = super().merge_kwargs(default_kwargs, runtime_kwargs)
​
    # fps 与 num_frames 相互影响:请求只覆盖其一,则从默认值中
    # 清除另一个,避免产生意外的跨字段组合。
    if runtime_kwargs:
        if 'num_frames' in runtime_kwargs and 'fps' not in runtime_kwargs:
            merged.pop('fps', None)
        elif 'fps' in runtime_kwargs and 'num_frames' not in runtime_kwargs:
            merged.pop('num_frames', None)
    return merged

评论区精华

backend_requires_gpu 默认值的 fail-closed 与向后兼容取舍 设计

DarkLight1337 质疑将未知 codec 的 backend_requires_gpu 默认值改为 True 是否有意,并建议显式装饰类而非全局默认;作者调整方向时发现 pyav、torchcodec 等软件 codec 未进入 loader registry,默认 True 会误拦截这些合法的请求级后端选择。

结论:回退默认值为 False(fail-open),改用显式 register_gpu_codec('deepstream') 注册闭合漏洞,兼顾安全与兼容。 · 已解决

风险与影响

1)行为兼容性:凡此前依赖请求级 backend=deepstream 的调用方,现在必须显式在启动配置中声明,否则该键被剥离并产生 warning_once——属于有意为之的 breaking change,需要在发布说明与文档中明确。2)回归风险:pool_size 从运行时 kwargs 移除后,若上游文档或示例仍展示请求级用法,将出现静默失效(剥离后不报错),需要同步检查 media-io-kwargs 相关文档。3)测试盲区:_check_frame_pixel_limit(_w, _h) 仅在 probe_metadata 返回非零宽高时有效,且 DeepStream 为可选依赖(nvidia.deepstream_videodecode),CI 中若未安装该包则对应路径无执行覆盖。4)注册通道并存隐患:register 装饰器与 register_gpu_codec 两条写入 _requires_gpu 的通道并存,未来新增 GPU codec 时若只走 register 且漏传 requires_gpu=True,会重新出现本 PR 修复的漏洞——值得加一条注册表一致性断言。5)性能影响:仅新增一次乘比较,可忽略。

影响范围集中在多模态视频解码入口的请求配置管道:merge_kwargsbackend / video_backend / pool_size 的运行时处理,以及 DeepStream 解码前的像素校验。对用户而言,DeepStream 必须在服务启动时通过媒体 IO 配置显式启用,请求方无法再任意激活 NVDEC 解码或控制全局 DecodePool 规模;对部署方而言,显存/内存资源使用更可预期,降低了超大视频帧引发的 DoS 面。对团队而言,register_gpu_codec 提供了一种零依赖注册 GPU codec 的范式,后续新后端可遵循同一模式;相关文档与示例需同步检查。

安全修复 核心路径变更 行为兼容性变化 测试依赖可选包

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论