Prhub

#44549 [Security] Replace diskcache to eliminate pickle deserialization

原始 PR 作者 russellb 合并时间 2026-07-15 11:29 文件变更 7 提交数 8 评论 5 代码增减 +162 / -19

执行摘要

使用 SQLite 替换 diskcache,消除 pickle 反序列化风险

Pickle deserialization allows arbitrary code execution, posing a security risk. CVE-2025-69872 identifies diskcache's use of pickle as a potential arbitrary code execution vector (CVSS 9.8 Critical). While vLLM is not vulnerable in practice (the disk cache is disabled by default and gated behind VLLM_V1_USE_OUTLINES_CACHE, and we document that the cache directory must not be shared with any untrusted users or content), the presence of diskcache as an installed dependency causes it to appear on security scans of vLLM environments. Removing the dependency eliminates the finding.

该 PR 值得精读,尤其是学习如何用标准库替换存在安全风险的第三方依赖并保持 API 兼容。OutlinesDiskCache 类实现简洁直观,展示了 sqlite3 + outlines_core 原生序列化的替代方案,对于类似 pickle 安全问题的修复有参考价值。建议关注其中 __reduce__ 的使用方式。

讨论亮点
  1. 类常量未使用问题:审核人 hmellor 指出 _TYPE_INDEX_TYPE_STRING 定义为 b"I" / b"S" 但未在方法中使用(Magic strings 被直接使用)。作者 russellb 立即确认并修复,最终代码将这些常量改为字符串形式 "I" / "S" 并在 __getitem____setitem__ 中正确引用,已解决。
  2. 缓存目录权限安全建议:depthfirst-app[bot] 建议在 os.makedirs 添加 mode=0o700 以限制目录权限,防止本地其他用户读取缓存。该建议未被采纳,可能由于缓存默认禁用且文档已明确要求目录不可共享,属于低优先级增强。状态未解决。

实现拆解

  1. 实现 OutlinesDiskCache 类vllm/v1/structured_output/utils.py):核心类使用 Python 标准库 sqlite3,在初始化时创建 outlines_cache.db 文件并启用 WAL 模式。类内部通过类型标签 _TYPE_INDEX / _TYPE_STRING 区分存储对象:字符串直接 utf-8 编解码,outlines_core.Index 对象则使用 __reduce__ 获取其 Rust serde 二进制数据(调用 Index.from_binary() 反序列化)。
  2. 替换 get_outlines_cache 工厂函数(同一文件):当环境变量 VLLM_V1_USE_OUTLINES_CACHE 启用时,将原来的 diskcache.Cache 替换为 OutlinesDiskCache(cache_dir),保持相同的 get/set/contains 调用约定和版本失效逻辑。
  3. 移除 diskcache 依赖requirements/common.txtrequirements/test/*.txt):从顶层 common.txt 和三个测试平台依赖文件(cuda/rocm/xpu/cpu)中删除 diskcache==5.6.3 条目,消除安全扫描中的告警。
  4. 新增测试套件tests/v1/structured_output/test_outlines_cache.py):覆盖 OutlinesDiskCache 的存/取 Index 对象与字符串、__contains__get 的默认值行为、KeyErrorclear、覆盖写入、跨实例持久化、目录创建、版本失效流程等 9 个场景,确保缓存正确性与兼容性。
文件 模块 状态 重要度
vllm/v1/structured_output/utils.py 缓存 modified 8.21
tests/v1/structured_output/test_outlines_cache.py 缓存测试 added 7.15
requirements/common.txt 依赖配置 modified 1.89
requirements/test/cuda.txt 依赖配置 modified 2.07
requirements/test/rocm.txt 依赖配置 modified 2.07
requirements/test/xpu.txt 依赖配置 modified 2.07
requirements/test/cpu.txt 依赖配置 modified 1.89

关键符号

OutlinesDiskCache __init__ __contains__ __getitem__ __setitem__ get set clear get_outlines_cache

关键源码片段

vllm/v1/structured_output/utils.py dependency-wiring

核心变更文件:新增 OutlinesDiskCache 类替换 diskcache.Cache,修改 get_outlines_cache 工厂函数,消除 pickle 安全风险。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import os
import sqlite3from outlines_core import Index as oc_Index
​
​
class OutlinesDiskCache:
    """SQLite-backed cache for outlines_core.Index objects.    Uses outlines_core's native binary serialization (via Rust serde)
    instead of pickle, eliminating arbitrary code execution risk on
    deserialization.
    """
​
    # 类型标签:区分字符串值和 Index 对象
    _TYPE_INDEX = "I"
    _TYPE_STRING = "S"
​
    def __init__(self, path: str):
        os.makedirs(path, exist_ok=True)
        db_path = os.path.join(path, "outlines_cache.db")
        self._db = sqlite3.connect(db_path, check_same_thread=False)
        self._db.execute("PRAGMA journal_mode=WAL")
        self._db.execute(
            "CREATE TABLE IF NOT EXISTS cache "
            "(key TEXT PRIMARY KEY, type_tag TEXT NOT NULL, value BLOB NOT NULL)"
        )
        self._db.commit()
​
    def __contains__(self, key: str) -> bool:
        row = self._db.execute("SELECT 1 FROM cache WHERE key=?", (key,)).fetchone()
        return row is not None
​
    def __getitem__(self, key: str):
        row = self._db.execute(
            "SELECT type_tag, value FROM cache WHERE key=?", (key,)
        ).fetchone()
        if row is None:
            raise KeyError(key)
        type_tag, data = row
        if type_tag == self._TYPE_STRING:
            return data.decode("utf-8")
        # Index 对象使用 outlines_core 原生序列化恢复
        return oc_Index.from_binary(data)
​
    def __setitem__(self, key: str, value):
        if isinstance(value, str):
            type_tag = self._TYPE_STRING
            data = value.encode("utf-8")
        else:
            type_tag = self._TYPE_INDEX
            # 通过 __reduce__ 获取二进制数据(Rust serde 序列化结果)
            data = value.__reduce__()[1][0]
        self._db.execute(
            "INSERT OR REPLACE INTO cache (key, type_tag, value) VALUES (?, ?, ?)",
            (key, type_tag, data),
        )
        self._db.commit()
​
    def get(self, key: str, default=None):
        try:
            return self[key]
        except KeyError:
            return default
​
    def set(self, key: str, value):
        self[key] = value
​
    def clear(self):
        self._db.execute("DELETE FROM cache")
        self._db.commit()
tests/v1/structured_output/test_outlines_cache.py test-coverage

新增完整测试套件,覆盖 OutlinesDiskCache 所有公有方法及边界场景,保障替换正确性。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import tempfile
import outlines_core as oc
import pytest
from vllm.v1.structured_output.utils import OutlinesDiskCachepytestmark = pytest.mark.cpu_test
​
​
@pytest.fixture
def vocab():
    # 构造一个简单的词汇表,用于生成 Index
    return oc.Vocabulary(3, {b"a": [0], b"b": [1], b"ab": [2]})
​
​
@pytest.fixture
def index(vocab):
    # 构造一个 Index 对象(LLM 语法解析器)
    return oc.Index("ab", vocab)
​
​
@pytest.fixture
def cache(tmp_path):
    return OutlinesDiskCache(str(tmp_path))
​
​
class TestOutlinesDiskCache:
    def test_store_and_retrieve_index(self, cache, index):
        # 写入并读取 Index 对象,验证序列化完整性
        cache["test_key"] = index
        restored = cache["test_key"]
        assert restored.get_initial_state() == index.get_initial_state()
        assert restored.get_transitions() == index.get_transitions()
        assert restored.get_final_states() == index.get_final_states()
​
    def test_store_and_retrieve_string(self, cache):
        # 字符串存取(用于存储版本号等元数据)
        cache.set("__version__", "0.2.14")
        assert cache.get("__version__") == "0.2.14"
​
    def test_clear(self, cache, index):
        # 清空缓存
        cache["key"] = index
        cache.clear()
        assert "key" not in cache
​
    def test_persistence_across_instances(self, tmp_path, index):
        # 不同实例间共享同一个数据库文件,验证持久化
        cache1 = OutlinesDiskCache(str(tmp_path))
        cache1["key"] = index
        cache2 = OutlinesDiskCache(str(tmp_path))
        restored = cache2["key"]
        assert restored.get_transitions() == index.get_transitions()
​
    def test_version_invalidation_flow(self, cache, index):
        # 模拟版本失效流程:版本不一致则清空缓存
        cache.set("__version__", "0.2.13")
        cache["key"] = index
        cached_version = cache.get("__version__")
        new_version = "0.2.14"
        if cached_version != new_version:
            cache.clear()
        cache.set("__version__", new_version)
        assert cache.get("__version__") == "0.2.14"
        assert "key" not in cache

评论区精华

未使用的 _TYPE_INDEX / _TYPE_STRING 常量 设计

Hmellor 指出类常量 _TYPE_INDEX = b"I" 和 _TYPE_STRING = b"S" 未被实际使用,方法中直接写入了 Magic string。

结论:作者 russellb 确认并修复,最终将常量改为字符串并在方法中正确引用。 · 已解决

缓存目录权限安全建议 安全

depthfirst-app[bot] 建议在 os.makedirs 添加 mode=0o700 以限制目录权限,防止信息泄露。

结论:该建议未被采纳(未收到批复),可能因为缓存默认禁用且文档已要求目录不共享。 · unresolved

风险与影响

  1. 序列化兼容性:使用 Index.__reduce__ 获取二进制数据,该接口依赖 outlines_core 的 Rust serde 稳定格式,若升级 outlines_core 后二进制格式不兼容,可能导致缓存失效(通过版本键 __version__ 自动清空,风险可控)。
  2. 并发安全:SQLite 连接使用 check_same_thread=False 及 WAL 模式允许跨线程读写,但同一进程内多个 OutlinesDiskCache 实例指向同一数据库时仍需注意;多进程同时写入同一数据库文件(如共享缓存目录)可能存在竞态,但该场景极少见且官方文档已警告不应共享。
  3. 资源消耗:SQLite 缓存是无界缓存,与原来 diskcache 的 eviction_policy="none" 行为一致,可能占用大量磁盘空间,维持原有的日志警告。
  4. 测试覆盖:新增测试较为全面,但缺少并发写入的回归测试。

影响范围:仅限于开启 VLLM_V1_USE_OUTLINES_CACHE 环境变量的用户(默认禁闭)。生产环境通常不启用,因此绝大多数用户无感知。依赖变化diskcache 不再作为 vLLM 的依赖,CI/CD 环境需要移除已安装的包。安全收益:消除 CVE-2025-69872 的检出项,安全扫描可顺利通过。向后兼容:新的 OutlinesDiskCache 类完全兼容原有 diskcache 的 API(get/set/__contains__/clear),升级过程对调用方透明。

Pickle 反序列化消除 SQLite 并发处理 依赖移除影响 CI outlines_core 序列化兼容

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论