Skip to content

Commit d7a4417

Browse files
chore: Mitigate vulnerability in diskcache (#520)
# Summary Mitigate CVE-2025-69872 in diskcache, a transitive dep for safe synthesizer. Since the consumers (vllm and outlines) do not provide ways to control the deserialization, we address the vulnerability via env vars for explicit cache location and file permissions. If any of vllm, outlines, diskcache directly resolve the vulnerability in the future, we should revert this PR. Signed-off-by: Kendrick Boyd <kendrickb@nvidia.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent e9495c7 commit d7a4417

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

src/nemo_safe_synthesizer/generation/vllm_backend.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
import logging
99
import os
10+
import tempfile
1011
import time
1112
from functools import partial
13+
from pathlib import Path
1214
from typing import Any, cast
1315

1416
import torch
@@ -48,6 +50,70 @@
4850
# in by exporting VLLM_USE_DEEP_GEMM=1.
4951
os.environ.setdefault("VLLM_USE_DEEP_GEMM", "0")
5052

53+
# CVE-2025-69872: diskcache (pulled in transitively by outlines and used by
54+
# vLLM's optional on-disk outlines cache) deserializes cached values with
55+
# pickle/cloudpickle and is therefore RCE-vulnerable if another principal can
56+
# write into the cache directory. Neither library exposes a way to swap the
57+
# serializer, so we mitigate at the boundary:
58+
# 1. Keep vLLM's opt-in diskcache off (its default is an in-memory LRUCache).
59+
# Hard-set (not setdefault) so a user env can't silently flip on a
60+
# pickle-deserializing code path.
61+
# 2. Pin OUTLINES_CACHE_DIR to a per-user path and chmod it to 0700, since
62+
# outlines always uses diskcache for its FSM/index cache.
63+
os.environ["VLLM_V1_USE_OUTLINES_CACHE"] = "0"
64+
65+
66+
def _secure_outlines_cache_dir() -> None:
67+
"""Pin ``OUTLINES_CACHE_DIR`` to a per-user path and tighten permissions.
68+
69+
Respects an explicit ``OUTLINES_CACHE_DIR`` set by the operator (so CI and
70+
multi-tenant deployments can choose their own private location), but always
71+
creates the directory with 0700 permissions to prevent co-tenants from
72+
poisoning the diskcache (CVE-2025-69872).
73+
74+
When unset, picks a per-user path under ``$XDG_CACHE_HOME`` or
75+
``$HOME/.cache`` and falls back to a UID-scoped subdir of the system temp
76+
dir for distroless/rootless containers where ``$HOME`` is ``/``.
77+
"""
78+
cache_dir_env = os.environ.get("OUTLINES_CACHE_DIR")
79+
if cache_dir_env:
80+
cache_dir = Path(cache_dir_env)
81+
else:
82+
xdg_cache_home = os.environ.get("XDG_CACHE_HOME")
83+
home_dir = os.path.normpath(os.path.expanduser("~"))
84+
if xdg_cache_home:
85+
cache_root = Path(xdg_cache_home)
86+
elif home_dir != "/" and Path(home_dir).is_dir():
87+
cache_root = Path(home_dir) / ".cache"
88+
else:
89+
uid = getattr(os, "getuid", lambda: "default")()
90+
cache_root = Path(tempfile.gettempdir()) / f".cache-{uid}"
91+
cache_dir = cache_root / "nemo-safe-synthesizer" / "outlines"
92+
os.environ["OUTLINES_CACHE_DIR"] = str(cache_dir)
93+
94+
try:
95+
# Set the umask to 077 to prevent other principals from writing to the
96+
# cache directory between the mkdir and chmod calls.
97+
old_umask = os.umask(0o077)
98+
try:
99+
cache_dir.mkdir(parents=True, exist_ok=True)
100+
finally:
101+
os.umask(old_umask)
102+
# Also explicitly set permissions to 0700 for the situation where the
103+
# directory already exists and is not 0700.
104+
cache_dir.chmod(0o700)
105+
except OSError as exc:
106+
logger.warning(
107+
"Could not enforce 0700 permissions on outlines cache dir %s: %s. "
108+
"If this path is shared with other principals, set OUTLINES_CACHE_DIR "
109+
"to a private location (CVE-2025-69872).",
110+
cache_dir,
111+
exc,
112+
)
113+
114+
115+
_secure_outlines_cache_dir()
116+
51117

52118
def _is_redis_available() -> bool:
53119
"""Return True if the ``redis`` package is importable."""

tests/generation/test_vllm_backend.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
"""Unit tests for the VllmBackend class private methods and module-level side effects."""
55

6+
import os
67
from unittest.mock import MagicMock, patch
78

89
import pytest
@@ -552,6 +553,39 @@ def test_is_redis_available_returns_true_when_present(self):
552553
assert _is_redis_available() is True
553554

554555

556+
class TestSecureOutlinesCacheDir:
557+
"""Tests for the CVE-2025-69872 outlines diskcache hardening."""
558+
559+
def test_chmods_existing_cache_dir_to_0700(self, tmp_path, monkeypatch):
560+
"""``_secure_outlines_cache_dir`` tightens permissions on a permissive dir.
561+
562+
Exercises the explicit-OUTLINES_CACHE_DIR branch: simulates a co-tenant-
563+
writable cache directory (mode 0777) and asserts the helper locks it down
564+
to 0700, which is the precondition CVE-2025-69872 needs to fail.
565+
"""
566+
import stat
567+
568+
from nemo_safe_synthesizer.generation.vllm_backend import _secure_outlines_cache_dir
569+
570+
cache_dir = tmp_path / "outlines-cache"
571+
cache_dir.mkdir()
572+
cache_dir.chmod(0o777)
573+
assert stat.S_IMODE(cache_dir.stat().st_mode) == 0o777, "precondition: dir starts world-writable"
574+
575+
monkeypatch.setenv("OUTLINES_CACHE_DIR", str(cache_dir))
576+
577+
_secure_outlines_cache_dir()
578+
579+
assert stat.S_IMODE(cache_dir.stat().st_mode) == 0o700
580+
assert os.environ["OUTLINES_CACHE_DIR"] == str(cache_dir)
581+
582+
def test_vllm_outlines_diskcache_is_disabled(self):
583+
"""Module import must hard-disable the vLLM opt-in diskcache."""
584+
from nemo_safe_synthesizer.generation import vllm_backend # noqa: F401 -- ensure module is imported
585+
586+
assert os.environ.get("VLLM_V1_USE_OUTLINES_CACHE") == "0"
587+
588+
555589
class TestGroupedGenerationStopKwargs:
556590
"""Tests that grouped generation relies on native EOS stopping (ignore_eos=False)
557591
rather than explicit stop/stop_token_ids kwargs.

0 commit comments

Comments
 (0)