Skip to content

Commit 9268c57

Browse files
authored
Merge branch 'dev' into perf/swinunetr-flash-attention
2 parents 969d474 + 605611b commit 9268c57

26 files changed

Lines changed: 859 additions & 103 deletions

.github/workflows/weekly-preview.yml

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ permissions:
66
on:
77
schedule:
88
- cron: "0 2 * * 0" # 02:00 of every Sunday
9+
pull_request:
10+
branches:
11+
- dev
12+
13+
env:
14+
PYTHON_VER: '3.10'
15+
PYTORCH_VER: '2.8.0'
16+
PIP_EXTRA_INDEX_URL: "https://download.pytorch.org/whl/cpu" # forces CPU PyTorch installation, should be faster
917

1018
jobs:
1119
static-checks:
20+
if: github.event_name == 'schedule' # only check on cron run, these checks are redundant in a PR
1221
runs-on: ubuntu-latest
1322
strategy:
1423
matrix:
@@ -25,10 +34,10 @@ jobs:
2534
- uses: actions/checkout@v7
2635
with:
2736
persist-credentials: false
28-
- name: Set up Python 3.10
37+
- name: Set up Python ${{ env.PYTHON_VER }}
2938
uses: actions/setup-python@v6
3039
with:
31-
python-version: '3.10'
40+
python-version: ${{ env.PYTHON_VER }}
3241
cache: 'pip'
3342
- name: Install dependencies
3443
run: |
@@ -40,22 +49,24 @@ jobs:
4049
$(pwd)/runtests.sh --build --clean
4150
$(pwd)/runtests.sh --build --${{ matrix.opt }}
4251
43-
packaging:
52+
publish:
4453
if: github.repository == 'Project-MONAI/MONAI'
4554
runs-on: ubuntu-latest
4655
steps:
4756
- uses: actions/checkout@v7
4857
with:
49-
ref: dev
58+
# get the ref for the PR branch or dev if this is a cron job
59+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'dev' }}
5060
fetch-depth: 0
5161
persist-credentials: false
52-
- name: Set up Python 3.10
62+
- name: Set up Python ${{ env.PYTHON_VER }}
5363
uses: actions/setup-python@v6
5464
with:
55-
python-version: '3.10'
56-
- name: Install setuptools
65+
python-version: ${{ env.PYTHON_VER }}
66+
cache: 'pip'
67+
- name: Install tools
5768
run: |
58-
python -m pip install --user --upgrade setuptools wheel packaging
69+
python -m pip install -U pip build
5970
- name: Build distribution
6071
run: |
6172
export HEAD_COMMIT_ID=$(git rev-parse HEAD)
@@ -72,9 +83,15 @@ jobs:
7283
git tag "1.7.dev${YEAR_WEEK}"
7384
git log -1
7485
git tag --list
75-
python setup.py sdist bdist_wheel
76-
86+
python -m build
87+
ls -lh dist
88+
- name: Test Installation
89+
run: |
90+
pip install dist/*.whl
91+
pip list
92+
(cd "$(mktemp -d)" && python -c 'import monai; print(monai.__version__)')
7793
- name: Publish to PyPI
94+
if: github.event_name == 'schedule' # only publish on cron run
7895
uses: pypa/gh-action-pypi-publish@release/v1
7996
with:
8097
user: __token__

docs/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ sphinxcontrib-serializinghtml
2020
sphinx-autodoc-typehints==1.11.1
2121
pandas
2222
einops
23-
transformers>=4.53.0
24-
mlflow>=2.12.2,<3.13
23+
transformers>=5.5.0
24+
mlflow>=3.15.2
2525
clearml>=1.10.0rc0
2626
tensorboardX
2727
imagecodecs; platform_system == "Linux" or platform_system == "Darwin"

monai/bundle/config_parser.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,14 @@ def __getattr__(self, key: str) -> Any:
162162
try:
163163
return self._chain(key)
164164
except KeyError:
165-
return getattr(self._value, key)
165+
pass
166+
if isinstance(self._value, dict) and key in self._value:
167+
# the chained id is absent from the resolver (for example when this proxy is
168+
# backed by a `$@ref`, whose children have no ids of their own), but the key
169+
# does exist in the container: resolve it like `__getitem__` does, so dot- and
170+
# bracket-notation agree and config keys keep precedence over dict methods.
171+
return self._value[key]
172+
return getattr(self._value, key)
166173

167174
def __getitem__(self, key: str | int) -> Any:
168175
try:

monai/bundle/scripts.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
ValidationError, _ = optional_import("jsonschema.exceptions", name="ValidationError")
6060
Checkpoint, has_ignite = optional_import("ignite.handlers", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Checkpoint")
6161
requests, has_requests = optional_import("requests")
62-
onnx, _ = optional_import("onnx")
6362
huggingface_hub, _ = optional_import("huggingface_hub")
6463

6564
logger = get_logger(module_name=__name__)
@@ -998,7 +997,8 @@ def run(
998997
common parameters shown below will be added and can be passed through the `override` parameter of this method.
999998
1000999
- ``"output_dir"``: the path to save mlflow tracking outputs locally, default to "<bundle root>/eval".
1001-
- ``"tracking_uri"``: uri to save mlflow tracking outputs, default to "/output_dir/mlruns".
1000+
- ``"tracking_uri"``: uri to save mlflow tracking outputs, default to a local SQLite database
1001+
at "<output_dir>/mlruns.db" with run artifacts kept under "<output_dir>/mlruns".
10021002
- ``"experiment_name"``: experiment name for this run, default to "monai_experiment".
10031003
- ``"run_name"``: the name of current run.
10041004
- ``"save_execute_config"``: whether to save the executed config files. It can be `False`, `/path/to/artifacts`
@@ -1437,6 +1437,7 @@ def onnx_export(
14371437
converter_kwargs_.update({"inputs": inputs_, "use_trace": use_trace_})
14381438

14391439
def save_onnx(onnx_obj: Any, filename_prefix_or_stream: str, **kwargs: Any) -> None:
1440+
onnx, _ = optional_import("onnx")
14401441
onnx.save(onnx_obj, filename_prefix_or_stream)
14411442

14421443
_export(

monai/bundle/utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@
116116
"configs": {
117117
# if no "output_dir" in the bundle config, default to "<bundle root>/eval"
118118
"output_dir": "$@bundle_root + '/eval'",
119-
# use URI to support linux, mac and windows os
120-
"tracking_uri": "$monai.utils.path_to_uri(@output_dir) + '/mlruns'",
119+
# MLflow 3.13+ rejects the filesystem (file store) tracking backend, so default tracking
120+
# to a local SQLite database. The handler keeps run artifacts under "<output_dir>/mlruns"
121+
# (next to the db). A URI is used so the path is valid on linux, mac and windows os.
122+
"tracking_uri": "$monai.utils.path_to_sqlite_uri(@output_dir + '/mlruns.db')",
121123
"experiment_name": "monai_experiment",
122124
"run_name": None,
123125
# may fill it at runtime

monai/bundle/workflows.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import os
1616
import sys
1717
import time
18+
import warnings
1819
from abc import ABC, abstractmethod
1920
from collections.abc import Sequence
2021
from copy import copy
@@ -34,6 +35,23 @@
3435
logger = get_logger(module_name=__name__)
3536

3637

38+
def _warn_logging_file_execution(logging_file: str) -> None:
39+
"""
40+
Warn that ``logging_file`` is about to be executed by `logging.config.fileConfig`.
41+
42+
Called immediately before every `fileConfig` invocation in this module, so the warning is only
43+
raised when the file is really executed -- not when it is missing or logging is disabled.
44+
"""
45+
warnings.warn(
46+
f"applying logging config {logging_file}: `logging.config.fileConfig` passes the `class=` and "
47+
"`args=` fields of the INI's handler and formatter sections to Python `eval()`, so this file "
48+
"runs as code. A bundle ships its own `configs/logging.conf` and it is applied by default, "
49+
"before any of the bundle's config is parsed. Only proceed if this file is from a source you "
50+
"trust (see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3).",
51+
stacklevel=3,
52+
)
53+
54+
3755
class BundleWorkflow(ABC):
3856
"""
3957
Base class for the workflow specification in bundle, it can be a training, evaluation or inference workflow.
@@ -55,6 +73,10 @@ class BundleWorkflow(ABC):
5573
meta_file: filepath of the metadata file, if this is a list of file paths, their contents will be merged in order.
5674
logging_file: config file for `logging` module in the program. for more details:
5775
https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig.
76+
Security note: `fileConfig` passes the INI's `class=` and `args=` fields to Python
77+
`eval()`, so this file runs as code and applying it raises a warning -- once per call
78+
site, as Python's default warning filter suppresses repeats
79+
(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3).
5880
5981
"""
6082

@@ -72,6 +94,7 @@ def __init__(
7294
if not os.path.isfile(logging_file):
7395
raise FileNotFoundError(f"Cannot find the logging config file: {logging_file}.")
7496
logger.info(f"Setting logging properties based on config: {logging_file}.")
97+
_warn_logging_file_execution(logging_file)
7598
fileConfig(logging_file, disable_existing_loggers=False)
7699

77100
if meta_file is not None:
@@ -273,6 +296,10 @@ class PythonicWorkflow(BundleWorkflow):
273296
meta_file: filepath of the metadata file, if this is a list of file paths, their contents will be merged in order.
274297
logging_file: config file for `logging` module in the program. for more details:
275298
https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig.
299+
Security note: `fileConfig` passes the INI's `class=` and `args=` fields to Python
300+
`eval()`, so this file runs as code and applying it raises a warning -- once per call
301+
site, as Python's default warning filter suppresses repeats
302+
(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3).
276303
277304
"""
278305

@@ -375,6 +402,10 @@ class ConfigWorkflow(BundleWorkflow):
375402
https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig.
376403
If None, default to "configs/logging.conf", which is commonly used for bundles in MONAI model zoo.
377404
If False, the logging logic for the bundle will not be modified.
405+
Security note: `fileConfig` passes the INI's `class=` and `args=` fields to Python
406+
`eval()`, so this file runs as code and applying it raises a warning -- once per call
407+
site, as Python's default warning filter suppresses repeats
408+
(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3).
378409
init_id: ID name of the expected config expression to initialize before running, default to "initialize".
379410
allow a config to have no `initialize` logic and the ID.
380411
run_id: ID name of the expected config expression to run, default to "run".
@@ -444,6 +475,7 @@ def __init__(
444475
else:
445476
raise FileNotFoundError(f"Cannot find the logging config file: {logging_file}.")
446477
else:
478+
_warn_logging_file_execution(str(logging_file))
447479
fileConfig(str(logging_file), disable_existing_loggers=False)
448480
logger.info(f"Setting logging properties based on config: {logging_file}.")
449481

monai/fl/client/monai_algo.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import os
1515
import time
16+
import warnings
1617
from collections.abc import Mapping, MutableMapping
1718
from typing import Any, cast
1819

@@ -34,6 +35,26 @@
3435
logger = get_logger(__name__)
3536

3637

38+
def _warn_provisioned_config_execution(bundle_root: str) -> None:
39+
"""
40+
Warn that the bundle under ``bundle_root`` is about to be executed.
41+
42+
In federated learning the whole app directory -- configs included -- is provisioned by the FL
43+
system, and the aggregation server dispatches `initialize`/`train` tasks that the client runs
44+
on its own, so there is no per-round human interaction to catch a poisoned config.
45+
"""
46+
warnings.warn(
47+
f"executing the bundle config under {bundle_root}, which is provisioned by the FL system: "
48+
'any `"_target_"` value in it is resolved to an importable callable and invoked with no '
49+
'allow list, and any `"$"`-prefixed value is passed to Python `eval()`. A malicious or '
50+
"compromised aggregation server therefore gets code execution on this client, without any "
51+
"per-round human interaction. Only join a federation whose server and app-provisioning "
52+
"channel you trust (see "
53+
"https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-x6pr-233j-x5cw).",
54+
stacklevel=3,
55+
)
56+
57+
3758
def convert_global_weights(global_weights: Mapping, local_var_dict: MutableMapping) -> tuple[MutableMapping, int]:
3859
"""Helper function to convert global weights to local weights format"""
3960
# Before loading weights, tensors might need to be reshaped to support HE for secure aggregation.
@@ -86,6 +107,15 @@ class MonaiAlgoStats(ClientAlgoStats):
86107
"""
87108
Implementation of ``ClientAlgoStats`` to allow federated learning with MONAI bundle configurations.
88109
110+
Security note: the bundle under `bundle_root` is provisioned by the FL system -- `initialize()`
111+
resolves it against `extra[ExtraItems.APP_ROOT]`, which the aggregation server supplies -- and
112+
executing it runs whatever its config contains: any `"_target_"` value is resolved to an
113+
importable callable and invoked with no allow list, and any `"$"`-prefixed value is passed to
114+
Python `eval()`. A malicious or compromised server therefore gets code execution on this client,
115+
with no per-round human interaction. Executing a config raises a warning -- once per call site,
116+
as Python's default warning filter suppresses repeats
117+
(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-x6pr-233j-x5cw).
118+
89119
Args:
90120
bundle_root: directory path of the bundle.
91121
config_train_filename: bundle training config path relative to bundle_root. Can be a list of files;
@@ -135,18 +165,29 @@ def initialize(self, extra=None):
135165
Args:
136166
extra: Dict with additional information that should be provided by FL system,
137167
i.e., `ExtraItems.CLIENT_NAME`, `ExtraItems.APP_ROOT` and `ExtraItems.LOGGING_FILE`.
138-
You can diable the logging logic in the monai bundle by setting {ExtraItems.LOGGING_FILE} to False.
168+
`{ExtraItems.LOGGING_FILE}` defaults to False here, and an explicit `None` is
169+
treated the same way, so the bundle's own "configs/logging.conf" is not applied:
170+
it is provisioned by the FL system and `logging.config.fileConfig` runs the INI's
171+
`class=`/`args=` fields through `eval()`
172+
(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3).
173+
Set it to a logging config file path to opt back in to configuring logging.
139174
140175
"""
141176
if extra is None:
142177
extra = {}
143178
self.client_name = extra.get(ExtraItems.CLIENT_NAME, "noname")
144-
logging_file = extra.get(ExtraItems.LOGGING_FILE, None)
179+
logging_file = extra.get(ExtraItems.LOGGING_FILE, False)
180+
if logging_file is None:
181+
# `ConfigWorkflow` reads `None` as "fall back to the bundle's own configs/logging.conf",
182+
# the FL-provisioned file this default exists to keep away from `fileConfig`. Passing the
183+
# key explicitly as `None` has to mean the same as leaving it out.
184+
logging_file = False
145185
self.logger.info(f"Initializing {self.client_name} ...")
146186

147187
# FL platform needs to provide filepath to configuration files
148188
self.app_root = extra.get(ExtraItems.APP_ROOT, "")
149189
self.bundle_root = os.path.join(self.app_root, self.bundle_root)
190+
_warn_provisioned_config_execution(self.bundle_root)
150191

151192
if self.workflow is None:
152193
config_train_files = self._add_config_files(self.config_train_filename)
@@ -313,6 +354,15 @@ class MonaiAlgo(ClientAlgo, MonaiAlgoStats):
313354
"""
314355
Implementation of ``ClientAlgo`` to allow federated learning with MONAI bundle configurations.
315356
357+
Security note: the bundle under `bundle_root` is provisioned by the FL system -- `initialize()`
358+
resolves it against `extra[ExtraItems.APP_ROOT]`, which the aggregation server supplies -- and
359+
executing it runs whatever its config contains: any `"_target_"` value is resolved to an
360+
importable callable and invoked with no allow list, and any `"$"`-prefixed value is passed to
361+
Python `eval()`. A malicious or compromised server therefore gets code execution on this client,
362+
with no per-round human interaction. Executing a config raises a warning -- once per call site,
363+
as Python's default warning filter suppresses repeats
364+
(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-x6pr-233j-x5cw).
365+
316366
Args:
317367
bundle_root: directory path of the bundle.
318368
local_epochs: number of local epochs to execute during each round of local training; defaults to 1.
@@ -416,19 +466,30 @@ def initialize(self, extra=None):
416466
Args:
417467
extra: Dict with additional information that should be provided by FL system,
418468
i.e., `ExtraItems.CLIENT_NAME`, `ExtraItems.APP_ROOT` and `ExtraItems.LOGGING_FILE`.
419-
You can diable the logging logic in the monai bundle by setting {ExtraItems.LOGGING_FILE} to False.
469+
`{ExtraItems.LOGGING_FILE}` defaults to False here, and an explicit `None` is
470+
treated the same way, so the bundle's own "configs/logging.conf" is not applied:
471+
it is provisioned by the FL system and `logging.config.fileConfig` runs the INI's
472+
`class=`/`args=` fields through `eval()`
473+
(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3).
474+
Set it to a logging config file path to opt back in to configuring logging.
420475
421476
"""
422477
self._set_cuda_device()
423478
if extra is None:
424479
extra = {}
425480
self.client_name = extra.get(ExtraItems.CLIENT_NAME, "noname")
426-
logging_file = extra.get(ExtraItems.LOGGING_FILE, None)
481+
logging_file = extra.get(ExtraItems.LOGGING_FILE, False)
482+
if logging_file is None:
483+
# `ConfigWorkflow` reads `None` as "fall back to the bundle's own configs/logging.conf",
484+
# the FL-provisioned file this default exists to keep away from `fileConfig`. Passing the
485+
# key explicitly as `None` has to mean the same as leaving it out.
486+
logging_file = False
427487
timestamp = time.strftime("%Y%m%d_%H%M%S")
428488
self.logger.info(f"Initializing {self.client_name} ...")
429489
# FL platform needs to provide filepath to configuration files
430490
self.app_root = extra.get(ExtraItems.APP_ROOT, "")
431491
self.bundle_root = os.path.join(self.app_root, self.bundle_root)
492+
_warn_provisioned_config_execution(self.bundle_root)
432493

433494
if self.train_workflow is None and self.config_train_filename is not None:
434495
config_train_files = self._add_config_files(self.config_train_filename)

0 commit comments

Comments
 (0)