Skip to content

Commit 126aa03

Browse files
authored
Merge branch 'dev' into weekly_preview2
2 parents e7f0be5 + c0d1ec1 commit 126aa03

14 files changed

Lines changed: 354 additions & 33 deletions

File tree

monai/apps/nnunet/nnunetv2_runner.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import re
1818
import shlex
1919
import subprocess
20+
import warnings
21+
from concurrent.futures import ThreadPoolExecutor
2022
from typing import Any
2123

2224
import monai
@@ -710,7 +712,11 @@ def train_parallel(
710712
**kwargs: Any,
711713
) -> None:
712714
"""
713-
Create the line command for subprocess call for parallel training.
715+
Launch subprocesses for parallel training.
716+
717+
The commands for each GPU run sequentially on that device, while different devices run in
718+
parallel. Each stage waits for all of its devices to finish before the next stage starts.
719+
714720
Note: to set the number of GPUs to use, use ``gpu_id_for_all`` instead of the `CUDA_VISIBLE_DEVICES`
715721
environment variable.
716722
@@ -741,17 +747,19 @@ def train_parallel(
741747
f"log '.txt' inside '{os.path.join(self.nnunet_results, self.dataset_name)}'"
742748
)
743749
for stage in all_cmds:
744-
processes = []
745-
for device_id in stage:
746-
if not stage[device_id]:
747-
continue
748-
cmd_str = "; ".join(shlex.join(cmd) for cmd, _ in stage[device_id])
749-
env = stage[device_id][0][1]
750-
logger.info(f"Current running command on GPU device {device_id}:\n{cmd_str}\n")
751-
processes.append(subprocess.Popen(cmd_str, shell=True, env=env, stdout=subprocess.DEVNULL))
752-
# finish this stage first
753-
for p in processes:
754-
p.wait()
750+
device_cmds = [(device_id, gpu_cmds) for device_id, gpu_cmds in stage.items() if gpu_cmds]
751+
if not device_cmds:
752+
continue
753+
754+
def _run_device_commands(item):
755+
device_id, gpu_cmds = item
756+
for cmd, env in gpu_cmds:
757+
cmd_str = shlex.join(cmd)
758+
logger.info(f"Current running command on GPU device {device_id}:\n{cmd_str}\n")
759+
subprocess.Popen(cmd, shell=False, env=env, stdout=subprocess.DEVNULL).wait()
760+
761+
with ThreadPoolExecutor(max_workers=len(device_cmds)) as executor:
762+
list(executor.map(_run_device_commands, device_cmds))
755763

756764
def validate_single_model(self, config: str, fold: int, **kwargs: Any) -> None:
757765
"""
@@ -996,7 +1004,16 @@ def predict_ensemble_postprocessing(
9961004

9971005
# apply postprocessing
9981006
if run_postprocessing:
999-
pp_fns, pp_fn_kwargs = load_pickle(self.best_configuration["best_model_or_ensemble"]["postprocessing_file"])
1007+
postprocessing_file = self.best_configuration["best_model_or_ensemble"]["postprocessing_file"]
1008+
warnings.warn(
1009+
f"unpickling postprocessing_file {postprocessing_file}: this path is read from "
1010+
"inference_information.json and is loaded with Python pickle without any allow list, "
1011+
"which gives whoever controls that file arbitrary code execution. Only proceed if the "
1012+
"inference_information.json is from a source you trust "
1013+
"(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-8f32-8649-rv87).",
1014+
stacklevel=2,
1015+
)
1016+
pp_fns, pp_fn_kwargs = load_pickle(postprocessing_file)
10001017
apply_postprocessing_to_folder(
10011018
folder_for_pp,
10021019
join(target_dir_base, "ensemble_predictions_postprocessed"),

monai/apps/utils.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,20 +166,20 @@ def safe_extract_member(member, extract_to):
166166
return full_path
167167

168168

169-
def check_hash(filepath: PathLike, val: str | None = None, hash_type: str = "md5") -> bool:
169+
def check_hash(filepath: PathLike, val: str | None = None, hash_type: str = "sha256") -> bool:
170170
"""
171171
Verify hash signature of specified file.
172172
173173
Args:
174174
filepath: path of source file to verify hash value.
175175
val: expected hash value of the file.
176-
hash_type: type of hash algorithm to use, default is `"md5"`.
176+
hash_type: type of hash algorithm to use, default is `"sha256"`.
177177
The supported hash types are `"md5"`, `"sha1"`, `"sha256"`, `"sha512"`.
178178
See also: :py:data:`monai.apps.utils.SUPPORTED_HASH_TYPES`.
179179
180180
"""
181181
if val is None:
182-
logger.info(f"Expected {hash_type} is None, skip {hash_type} check for file {filepath}.")
182+
warnings.warn(f"No hash value provided for {filepath}; file integrity is NOT verified.", stacklevel=2)
183183
return True
184184
actual_hash_func = look_up_option(hash_type.lower(), SUPPORTED_HASH_TYPES)
185185

@@ -204,7 +204,7 @@ def download_url(
204204
url: str,
205205
filepath: PathLike = "",
206206
hash_val: str | None = None,
207-
hash_type: str = "md5",
207+
hash_type: str = "sha256",
208208
progress: bool = True,
209209
**gdown_kwargs: Any,
210210
) -> None:
@@ -217,7 +217,8 @@ def download_url(
217217
If undefined, `os.path.basename(url)` will be used.
218218
hash_val: expected hash value to validate the downloaded file.
219219
if None, skip hash validation.
220-
hash_type: 'md5' or 'sha1', defaults to 'md5'.
220+
hash_type: type of hash algorithm to use, default is `"sha256"`.
221+
The supported hash types are `"md5"`, `"sha1"`, `"sha256"`, `"sha512"`.
221222
progress: whether to display a progress bar.
222223
gdown_kwargs: other args for `gdown` except for the `url`, `output` and `quiet`.
223224
these args will only be used if download from google drive.
@@ -315,7 +316,7 @@ def extractall(
315316
filepath: PathLike,
316317
output_dir: PathLike = ".",
317318
hash_val: str | None = None,
318-
hash_type: str = "md5",
319+
hash_type: str = "sha256",
319320
file_type: str = "",
320321
has_base: bool = True,
321322
) -> None:
@@ -328,7 +329,7 @@ def extractall(
328329
output_dir: target directory to save extracted files.
329330
hash_val: expected hash value to validate the compressed file.
330331
if None, skip hash validation.
331-
hash_type: 'md5' or 'sha1', defaults to 'md5'.
332+
hash_type: type of hash algorithm to use, default is `"sha256"`.
332333
file_type: string of file type for decompressing. Leave it empty to infer the type from the filepath basename.
333334
has_base: whether the extracted files have a base folder. This flag is used when checking if the existing
334335
folder is a result of `extractall`, if it is, the extraction is skipped. For example, if A.zip is unzipped
@@ -394,7 +395,7 @@ def download_and_extract(
394395
filepath: PathLike = "",
395396
output_dir: PathLike = ".",
396397
hash_val: str | None = None,
397-
hash_type: str = "md5",
398+
hash_type: str = "sha256",
398399
file_type: str = "",
399400
has_base: bool = True,
400401
progress: bool = True,
@@ -410,7 +411,7 @@ def download_and_extract(
410411
default is the current directory.
411412
hash_val: expected hash value to validate the downloaded file.
412413
if None, skip hash validation.
413-
hash_type: 'md5' or 'sha1', defaults to 'md5'.
414+
hash_type: type of hash algorithm to use, default is `"sha256"`.
414415
file_type: string of file type for decompressing. Leave it empty to infer the type from url's base file name.
415416
has_base: whether the extracted files have a base folder. This flag is used when checking if the existing
416417
folder is a result of `extractall`, if it is, the extraction is skipped. For example, if A.zip is unzipped

monai/auto3dseg/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,14 @@ def algo_from_json(filename: str, template_path: PathLike | None = None, **kwarg
493493
if state_template_path:
494494
algo_config["template_path"] = state_template_path
495495

496+
warnings.warn(
497+
f"Loading {filename}: the file's `_target_` value is resolved to an imported callable and "
498+
"invoked, and template directories from the file may be added to `sys.path`; only load "
499+
"algo_object.json files from a source you trust "
500+
"(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-2wx3-8x3w-r8qv).",
501+
stacklevel=2,
502+
)
503+
496504
parser = ConfigParser(algo_config)
497505
algo = parser.get_parsed_content()
498506
used_template_path = path

monai/bundle/scripts.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,17 @@ def create_workflow(
20082008
return workflow_
20092009

20102010

2011+
def _safe_large_file_path(bundle_path: PathLike, filepath: str) -> str:
2012+
"""Securely resolve a large-file target path to prevent traversal outside the bundle directory."""
2013+
bundle_root = os.path.realpath(bundle_path)
2014+
target = os.path.normpath(os.path.join(bundle_path, filepath))
2015+
target_real = os.path.realpath(target)
2016+
# Ensure the resolved path stays within the bundle root
2017+
if os.path.commonpath([bundle_root, target_real]) != bundle_root:
2018+
raise ValueError(f"Unsafe path: path traversal {filepath} for bundle_path {bundle_path}")
2019+
return target
2020+
2021+
20112022
def download_large_files(bundle_path: str | None = None, large_file_name: str | None = None) -> None:
20122023
"""
20132024
This utility allows you to download large files from a bundle. It supports file suffixes like ".yml", ".yaml", and ".json".
@@ -2042,6 +2053,6 @@ def download_large_files(bundle_path: str | None = None, large_file_name: str |
20422053
lf_data.pop("hash_val")
20432054
if "hash_type" in lf_data and lf_data.get("hash_type", "") == "":
20442055
lf_data.pop("hash_type")
2045-
lf_data["filepath"] = os.path.join(bundle_path, lf_data["path"])
2056+
lf_data["filepath"] = _safe_large_file_path(bundle_path, lf_data["path"])
20462057
lf_data.pop("path")
20472058
download_url(**lf_data)

monai/data/image_reader.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ def _get_affine(self, img, lps_to_ras: bool = True):
347347
affine: np.ndarray = np.eye(sr + 1)
348348
affine[:sr, :sr] = direction[:sr, :sr] @ np.diag(spacing[:sr])
349349
affine[:sr, -1] = origin[:sr]
350+
350351
if lps_to_ras:
351352
affine = orientation_ras_lps(affine)
352353
return affine
@@ -752,13 +753,25 @@ def _get_affine(self, metadata: dict, lps_to_ras: bool = True):
752753
stacklevel=2,
753754
)
754755
return affine
756+
757+
def _raise_if_not_finite(values: Sequence[Any], tag: str) -> None:
758+
if not np.isfinite(tuple(values)).all():
759+
raise ValueError(
760+
f"PydicomReader: cannot derive affine matrix because DICOM tag {tag} "
761+
f"has a non-finite value: {values}."
762+
)
763+
755764
# "00200037" is the tag of `ImageOrientationPatient`
756765
rx, ry, rz, cx, cy, cz = metadata["00200037"]["Value"]
766+
_raise_if_not_finite((rx, ry, rz, cx, cy, cz), "ImageOrientationPatient (0020,0037)")
757767
# "00200032" is the tag of `ImagePositionPatient`
758768
sx, sy, sz = metadata["00200032"]["Value"]
769+
_raise_if_not_finite((sx, sy, sz), "ImagePositionPatient (0020,0032)")
759770
# "00280030" is the tag of `PixelSpacing`
760771
spacing = metadata["00280030"]["Value"] if "00280030" in metadata else (1.0, 1.0)
772+
_raise_if_not_finite(tuple(spacing), "PixelSpacing (0028,0030)")
761773
dr, dc = metadata.get("spacing", spacing)[:2]
774+
_raise_if_not_finite((dr, dc), "spacing")
762775
affine[0, 0] = cx * dr
763776
affine[0, 1] = rx * dc
764777
affine[0, 3] = sx
@@ -773,12 +786,16 @@ def _get_affine(self, metadata: dict, lps_to_ras: bool = True):
773786
# 3d
774787
if "lastImagePositionPatient" in metadata:
775788
t1n, t2n, t3n = metadata["lastImagePositionPatient"]
789+
_raise_if_not_finite((t1n, t2n, t3n), "lastImagePositionPatient")
776790
n = metadata[MetaKeys.SPATIAL_SHAPE][-1]
777791
if n > 1:
778792
affine[0, 2] = (t1n - sx) / (n - 1)
779793
affine[1, 2] = (t2n - sy) / (n - 1)
780794
affine[2, 2] = (t3n - sz) / (n - 1)
781795

796+
if not np.isfinite(affine).all():
797+
raise ValueError("PydicomReader: affine matrix not finite after composition.")
798+
782799
if lps_to_ras:
783800
affine = orientation_ras_lps(affine)
784801
return affine

monai/networks/blocks/text_embedding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __init__(
6767

6868
if pretrained:
6969
model_url = url_map[self.encoding]
70-
pretrain_state_dict = model_zoo.load_url(model_url, map_location="cpu")
70+
pretrain_state_dict = model_zoo.load_url(model_url, map_location="cpu", weights_only=True)
7171
self.text_embedding.data = pretrain_state_dict.float() # type: ignore
7272
else:
7373
print(f"{self.encoding} is not implemented, and can not be downloaded, please load your own")

monai/networks/nets/densenet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def _load_state_dict(model: nn.Module, arch: str, progress: bool):
277277
r"^(.*denselayer\d+)(\.(?:norm|relu|conv))\.((?:[12])\.(?:weight|bias|running_mean|running_var))$"
278278
)
279279

280-
state_dict = load_state_dict_from_url(model_url, progress=progress)
280+
state_dict = load_state_dict_from_url(model_url, progress=progress, weights_only=True)
281281
for key in list(state_dict.keys()):
282282
res = pattern.match(key)
283283
if res:

monai/networks/nets/efficientnet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ def _load_state_dict(model: nn.Module, arch: str, progress: bool, adv_prop: bool
793793
else:
794794
# load state dict from url
795795
model_url = url_map[arch]
796-
pretrain_state_dict = model_zoo.load_url(model_url, progress=progress)
796+
pretrain_state_dict = model_zoo.load_url(model_url, progress=progress, weights_only=True)
797797
model_state_dict = model.state_dict()
798798

799799
pattern = re.compile(r"(.+)\.\d+(\.\d+\..+)")

monai/networks/nets/senet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def _load_state_dict(model: nn.Module, arch: str, progress: bool):
304304
download_url(model_url["url"], filepath=model_url["filename"])
305305
state_dict = torch.load(model_url["filename"], map_location=None, weights_only=True)
306306
else:
307-
state_dict = load_state_dict_from_url(model_url, progress=progress)
307+
state_dict = load_state_dict_from_url(model_url, progress=progress, weights_only=True)
308308
for key in list(state_dict.keys()):
309309
new_key = None
310310
if pattern_conv.match(key):

0 commit comments

Comments
 (0)