Files
openbmb--voxcpm/tests/test_torch_load_safety.py
Rafael Figuereo 09fce8e9db Harden checkpoint loading: set weights_only=True on all torch.load calls
The fine-tuning resume path in scripts/train_voxcpm_finetune.py called
torch.load() without weights_only=True for the LoRA checkpoint, full-model
checkpoint, optimizer, and scheduler. Loading an attacker-supplied checkpoint
directory would therefore execute arbitrary code during unpickling.

Every inference-time loader (model/voxcpm.py, model/voxcpm2.py, LoRA loading)
already passes weights_only=True, and the project ships a test asserting LoRA
loading rejects malicious pickle payloads. This closes the remaining gap so the
resume path matches that posture.

Also:
- app.py: add a --host flag so the Gradio server can bind to 127.0.0.1 instead
  of being hardwired to 0.0.0.0 (default unchanged to preserve current behavior).
  The UI exposes an unauthenticated api_name="generate" endpoint.
- tests/test_torch_load_safety.py: AST guard asserting every torch.load across
  src/, scripts/, app.py and lora_ft_webui.py sets weights_only=True, plus a
  behavioral check that a malicious pickle is blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:16:14 -04:00

86 lines
3.0 KiB
Python

"""Regression guard: every ``torch.load`` call must set ``weights_only=True``.
VoxCPM deliberately loads checkpoints with ``weights_only=True`` so that a
crafted ``.ckpt``/``.pth``/``.bin`` file cannot execute arbitrary code via
pickle during unpickling (see
``tests/test_lora_checkpoint_loading.py::test_load_lora_weights_rejects_malicious_pickle_payloads``).
The fine-tuning resume path in ``scripts/train_voxcpm_finetune.py`` originally
called ``torch.load`` without that flag, leaving an arbitrary-code-execution
gap when resuming from an attacker-supplied checkpoint directory. This test
statically asserts the flag is present on every ``torch.load`` call across the
package and scripts so the gap cannot silently reappear.
"""
import ast
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
# Directories whose Python files load checkpoints at runtime / on resume.
SCANNED_DIRS = [REPO_ROOT / "src", REPO_ROOT / "scripts", REPO_ROOT / "app.py", REPO_ROOT / "lora_ft_webui.py"]
def _python_files():
for entry in SCANNED_DIRS:
if entry.is_file() and entry.suffix == ".py":
yield entry
elif entry.is_dir():
yield from entry.rglob("*.py")
def _is_torch_load(node: ast.Call) -> bool:
func = node.func
# Matches ``torch.load(...)`` and ``load(...)`` aliased from torch.
if isinstance(func, ast.Attribute) and func.attr == "load":
return isinstance(func.value, ast.Name) and func.value.id == "torch"
return False
def _has_weights_only_true(node: ast.Call) -> bool:
for kw in node.keywords:
if kw.arg == "weights_only":
return isinstance(kw.value, ast.Constant) and kw.value.value is True
return False
def test_every_torch_load_sets_weights_only_true():
offenders = []
checked = 0
for path in _python_files():
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.Call) and _is_torch_load(node):
checked += 1
if not _has_weights_only_true(node):
offenders.append(f"{path.relative_to(REPO_ROOT)}:{node.lineno}")
assert checked > 0, "expected to find at least one torch.load call to verify"
assert not offenders, (
"torch.load without weights_only=True (pickle RCE risk):\n "
+ "\n ".join(offenders)
)
def test_torch_load_weights_only_blocks_malicious_pickle(tmp_path):
"""Behavioral check that weights_only=True actually rejects a code-exec payload."""
torch = pytest.importorskip("torch")
marker = tmp_path / "pwned.txt"
class Exploit:
def __reduce__(self):
import pathlib
return (pathlib.Path.write_text, (marker, "executed\n"))
ckpt = tmp_path / "optimizer.pth"
torch.save({"state_dict": {"w": torch.zeros(1)}, "boom": Exploit()}, ckpt)
with pytest.raises(Exception):
torch.load(ckpt, map_location="cpu", weights_only=True)
assert not marker.exists(), "malicious pickle executed despite weights_only=True"