Files
wehub-resource-sync ba4be087d5
CICD NeMo / cicd-main-unit-tests (push) Blocked by required conditions
CICD NeMo / cicd-main-speech (push) Blocked by required conditions
CICD NeMo / cicd-test-container-build (push) Blocked by required conditions
CICD NeMo / cicd-import-tests (push) Blocked by required conditions
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Blocked by required conditions
CICD NeMo / Nemo_CICD_Test (push) Blocked by required conditions
CICD NeMo / Coverage (e2e) (push) Blocked by required conditions
CICD NeMo / Coverage (unit-test) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
CICD NeMo / cicd-wait-in-queue (push) Waiting to run
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:58 +08:00

155 lines
5.6 KiB
Python

# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ast
import io
import subprocess
import tarfile
from pathlib import Path
from types import SimpleNamespace
import pytest
from omegaconf import OmegaConf
from nemo.collections.asr.parts.mixins.mixins import ASRBPEMixin
from nemo.utils.app_state import AppState
from nemo.utils.model_utils import load_config, save_artifacts
from nemo.utils.notebook_utils import download_an4
from nemo.utils.tar_utils import TarPathTraversalError
REPO_ROOT = Path(__file__).parents[2]
RAW_TAR_EXTRACTION_ALLOWLIST = {REPO_ROOT / "nemo/utils/tar_utils.py"}
def _is_tarfile_open_call(node):
return (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "open"
and isinstance(node.func.value, ast.Name)
and node.func.value.id == "tarfile"
)
def _add_tar_file(tar, name, data=b""):
info = tarfile.TarInfo(name=name)
info.size = len(data)
tar.addfile(info, io.BytesIO(data))
def test_load_config_rejects_traversal_model_config(tmp_path):
nemo_path = tmp_path / "malicious.nemo"
with tarfile.open(nemo_path, "w:") as tar:
_add_tar_file(tar, "../model_config.yaml", b"model:\n value: 1\n")
with pytest.raises(TarPathTraversalError):
load_config(str(nemo_path))
def test_save_artifacts_rejects_traversal_artifact_path(tmp_path):
nemo_path = tmp_path / "malicious.nemo"
with tarfile.open(nemo_path, "w:") as tar:
_add_tar_file(tar, "model_config.yaml", b"model:\n value: 1\n")
_add_tar_file(tar, "../tokenizer.model", b"payload")
model = SimpleNamespace(
cfg=OmegaConf.create({"tokenizer": {"library": "sentencepiece"}}),
artifacts={"tokenizer.model": SimpleNamespace(path="nemo:../tokenizer.model")},
)
AppState().model_restore_path = str(nemo_path)
with pytest.raises(TarPathTraversalError):
save_artifacts(model, str(tmp_path / "output"))
def test_save_artifacts_rejects_absolute_artifact_path_before_rename(tmp_path):
secret_path = tmp_path / "secret.txt"
secret_path.write_text("sensitive")
nemo_path = tmp_path / "malicious.nemo"
with tarfile.open(nemo_path, "w:") as tar:
_add_tar_file(tar, "nested/model_config.yaml", b"model:\n value: 1\n")
_add_tar_file(tar, f"nested/{secret_path.as_posix()}", b"payload")
model = SimpleNamespace(
cfg=OmegaConf.create({"tokenizer": {"library": "sentencepiece"}}),
artifacts={"tokenizer.model": SimpleNamespace(path=f"nemo:{secret_path.as_posix()}")},
)
output_dir = tmp_path / "output"
AppState().model_restore_path = str(nemo_path)
with pytest.raises(TarPathTraversalError):
save_artifacts(model, str(output_dir))
assert secret_path.read_text() == "sensitive"
assert not (output_dir / "tokenizer.model").exists()
def test_asr_tokenizer_rename_uses_member_basename():
assert ASRBPEMixin._get_extracted_tokenizer_name("prefix_/tmp/tokenizer_vocab") == "vocab"
def test_download_an4_rejects_traversal_archive(tmp_path):
data_dir = tmp_path / "data"
data_dir.mkdir()
an4_path = data_dir / "an4_sphere.tar.gz"
with tarfile.open(an4_path, "w:gz") as tar:
_add_tar_file(tar, "an4/etc/an4_train.transcription")
_add_tar_file(tar, "an4/etc/an4_test.transcription")
_add_tar_file(tar, "../nemo_poc_traversal_test", b"payload")
with pytest.raises(TarPathTraversalError):
download_an4(str(data_dir))
assert not (tmp_path / "nemo_poc_traversal_test").exists()
def test_no_raw_tar_extraction_outside_safe_helper():
raw_extractions = []
python_files = subprocess.run(
["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "*.py"],
cwd=REPO_ROOT,
check=True,
capture_output=True,
).stdout.split(b"\0")
for python_file in python_files:
if not python_file:
continue
path = REPO_ROOT / python_file.decode()
if path in RAW_TAR_EXTRACTION_ALLOWLIST:
continue
tar_vars = set()
tree = ast.parse(path.read_text(), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.Assign) and _is_tarfile_open_call(node.value):
for target in node.targets:
if isinstance(target, ast.Name):
tar_vars.add(target.id)
elif isinstance(node, ast.With):
for item in node.items:
if _is_tarfile_open_call(item.context_expr) and isinstance(item.optional_vars, ast.Name):
tar_vars.add(item.optional_vars.id)
for node in ast.walk(tree):
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
continue
if (
node.func.attr in {"extract", "extractall"}
and isinstance(node.func.value, ast.Name)
and node.func.value.id in tar_vars
):
raw_extractions.append(f"{path.relative_to(REPO_ROOT)}:{node.lineno}")
assert raw_extractions == []