Skip to content

Commit ea1fa3d

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/pr5479-hybrid-model
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
2 parents a3aafb9 + 125695a commit ea1fa3d

9 files changed

Lines changed: 610 additions & 4 deletions

File tree

src/megatron/bridge/data/energon/base_energon_datamodule.py

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,156 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import importlib
1516
import logging
16-
from typing import Any, Literal, Optional
17+
from typing import Any, Callable, Literal, Optional
1718

1819
from megatron.core.process_groups_config import ProcessGroupCollection
19-
from megatron.energon import WorkerConfig, get_savable_loader, get_train_dataset
20+
from megatron.energon import Sample, WorkerConfig, get_savable_loader, get_train_dataset
21+
from megatron.energon import dataset_config as _energon_dataset_config
22+
from megatron.energon.epathlib import EPath
23+
from megatron.energon.flavors.webdataset.default_generic_webdataset import DefaultGenericWebdatasetFactory
24+
from megatron.energon.typed_converter import JsonParser
2025

2126

2227
logger = logging.getLogger(__name__)
2328

29+
_ORIGINAL_METHOD_ATTRIBUTE = "__megatron_bridge_original_method__"
30+
_TRUSTED_DATASET_FACTORY_MODULE_PREFIXES = (
31+
"megatron.bridge.data.energon",
32+
"megatron.energon",
33+
)
34+
_TRUSTED_DATASET_FACTORY_MODULE_ALIASES = frozenset(
35+
{
36+
"megatron.bridge.models.qwen_vl.data.energon",
37+
}
38+
)
39+
_energon_factory_init = getattr(
40+
DefaultGenericWebdatasetFactory.__init__,
41+
_ORIGINAL_METHOD_ATTRIBUTE,
42+
DefaultGenericWebdatasetFactory.__init__,
43+
)
44+
_energon_load_config = getattr(
45+
_energon_dataset_config.load_config,
46+
_ORIGINAL_METHOD_ATTRIBUTE,
47+
_energon_dataset_config.load_config,
48+
)
49+
50+
51+
def _validate_energon_dataset_metadata(value: Any, *, root: bool = True) -> None:
52+
"""Reject executable object references in untrusted dataset factory metadata."""
53+
if isinstance(value, list):
54+
for item in value:
55+
_validate_energon_dataset_metadata(item, root=False)
56+
return
57+
if not isinstance(value, dict):
58+
return
59+
60+
module_name = value.get("__module__")
61+
function_name = value.get("__function__")
62+
class_name = value.get("__class__")
63+
if function_name is not None:
64+
raise ValueError(
65+
"Energon dataset metadata cannot resolve serialized Python functions. "
66+
"Use declarative configuration or pass a Python callable from trusted application code."
67+
)
68+
if class_name is not None:
69+
trusted_class = (
70+
isinstance(module_name, str)
71+
and isinstance(class_name, str)
72+
and (
73+
module_name in _TRUSTED_DATASET_FACTORY_MODULE_ALIASES
74+
or any(
75+
module_name == prefix or module_name.startswith(f"{prefix}.")
76+
for prefix in _TRUSTED_DATASET_FACTORY_MODULE_PREFIXES
77+
)
78+
)
79+
)
80+
if trusted_class:
81+
module = importlib.import_module(module_name)
82+
referenced_type = getattr(module, class_name, None)
83+
required_base = DefaultGenericWebdatasetFactory if root else Sample
84+
trusted_class = isinstance(referenced_type, type) and issubclass(referenced_type, required_base)
85+
if not trusted_class:
86+
raise ValueError(
87+
"Energon dataset metadata cannot instantiate serialized Python classes. "
88+
"Only packaged Energon dataset factory and sample classes are allowed."
89+
)
90+
91+
for item in value.values():
92+
_validate_energon_dataset_metadata(item, root=False)
93+
94+
95+
def _secure_energon_load_config(
96+
path: EPath | dict[str, Any],
97+
*,
98+
default_type: type,
99+
default_kwargs: dict[str, Any] | None = None,
100+
parser: JsonParser = JsonParser(strict=True),
101+
) -> Any:
102+
"""Validate dataset metadata before Energon resolves serialized objects."""
103+
is_dataset_factory = isinstance(default_type, type) and issubclass(default_type, DefaultGenericWebdatasetFactory)
104+
if not is_dataset_factory:
105+
return _energon_load_config(
106+
path,
107+
default_type=default_type,
108+
default_kwargs=default_kwargs,
109+
parser=parser,
110+
)
111+
112+
if isinstance(path, dict):
113+
data = path
114+
else:
115+
with path.open("rb") as config_file:
116+
data = _energon_dataset_config.load_yaml(config_file)
117+
_validate_energon_dataset_metadata(data)
118+
return _energon_load_config(
119+
data,
120+
default_type=default_type,
121+
default_kwargs=default_kwargs,
122+
parser=parser,
123+
)
124+
125+
126+
def _secure_energon_factory_init(
127+
self: DefaultGenericWebdatasetFactory,
128+
path: EPath,
129+
*,
130+
subflavors: dict[str, Any] | None = None,
131+
field_map: dict[str, str] | None = None,
132+
sample_loader: str | Callable[[dict[str, Any]], dict[str, Any]] | None = None,
133+
part_filter: str | list[str] | Callable[[str], bool] | None = None,
134+
**kwargs: Any,
135+
) -> None:
136+
"""Reject dataset-local Python hooks before Energon resolves their files."""
137+
executable_fields = [
138+
name
139+
for name, value in (("sample_loader", sample_loader), ("part_filter", part_filter))
140+
if isinstance(value, str)
141+
]
142+
if executable_fields:
143+
raise ValueError(
144+
"Energon dataset metadata cannot load Python files through "
145+
f"{', '.join(executable_fields)}. Use a declarative field_map instead."
146+
)
147+
_energon_factory_init(
148+
self,
149+
path,
150+
subflavors=subflavors,
151+
field_map=field_map,
152+
sample_loader=sample_loader,
153+
part_filter=part_filter,
154+
**kwargs,
155+
)
156+
157+
158+
# Energon constructs this factory internally after reading dataset.yaml, so
159+
# Bridge must install the guard before calling get_train_dataset().
160+
setattr(_secure_energon_load_config, _ORIGINAL_METHOD_ATTRIBUTE, _energon_load_config)
161+
_energon_dataset_config.load_config = _secure_energon_load_config
162+
setattr(_secure_energon_factory_init, _ORIGINAL_METHOD_ATTRIBUTE, _energon_factory_init)
163+
DefaultGenericWebdatasetFactory.__init__ = _secure_energon_factory_init
164+
24165

25166
class EnergonMultiModalDataModule:
26167
"""

src/megatron/bridge/diffusion/data/common/diffusion_task_encoder_with_sp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from megatron.bridge.data.energon.metadata import sample_metadata_kwargs
2525
from megatron.bridge.data.packing.algorithms import first_fit_decreasing
2626
from megatron.bridge.diffusion.data.common.diffusion_sample import DiffusionSample
27+
from megatron.bridge.diffusion.data.common.safe_decoder import SafeDiffusionSampleDecoder
2728
from megatron.bridge.diffusion.data.common.sequence_packing_utils import packing_length
2829

2930

@@ -51,6 +52,8 @@ def cook(sample: dict) -> dict:
5152

5253

5354
class DiffusionTaskEncoderWithSequencePacking(DefaultTaskEncoder, ABC): # noqa: D101
55+
decoder = SafeDiffusionSampleDecoder()
56+
5457
cookers = [
5558
Cooker(cook),
5659
]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Safe decoding for Bridge diffusion WebDataset fields."""
16+
17+
import io
18+
import re
19+
from typing import Any
20+
21+
import torch
22+
from megatron.energon.flavors.webdataset.sample_decoder import SampleDecoder
23+
from webdataset.autodecode import basichandlers
24+
25+
from megatron.bridge.utils.safe_pickle import safe_torch_tensor_pickle_loads
26+
27+
28+
_PICKLE_EXTENSIONS = frozenset({"pickle", "pkl", "pyd"})
29+
30+
31+
def _safe_basic_handler(key: str, data: bytes) -> Any:
32+
"""Decode tensor fields safely and delegate non-executable formats."""
33+
extension = re.sub(r".*[.]", "", key).lower()
34+
if extension in _PICKLE_EXTENSIONS:
35+
return safe_torch_tensor_pickle_loads(data)
36+
if extension == "pth":
37+
return torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
38+
return basichandlers(key, data)
39+
40+
41+
class SafeDiffusionSampleDecoder(SampleDecoder):
42+
"""Energon decoder that never uses WebDataset's unrestricted pickle handlers."""
43+
44+
def __init__(self) -> None:
45+
super().__init__()
46+
replaced = False
47+
for index, handler in enumerate(self._decoder.handlers):
48+
if handler is basichandlers:
49+
self._decoder.handlers[index] = _safe_basic_handler
50+
replaced = True
51+
if not replaced:
52+
raise RuntimeError("Energon SampleDecoder no longer exposes the expected WebDataset basic handler.")

src/megatron/bridge/utils/instantiate_utils.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@
105105
},
106106
}
107107

108+
# Resolve aliases before enforcing these entries. For example,
109+
# ``numpy.lib.npyio.load`` is the same callable as ``numpy.load`` and
110+
# ``torch.serialization.load`` is the same callable as ``torch.load``.
111+
_DISALLOWED_CANONICAL_TARGETS: set[str] = {
112+
"numpy.load",
113+
"torch.serialization.load",
114+
"transformers.dynamic_module_utils.get_class_in_module",
115+
"transformers.dynamic_module_utils.get_class_from_dynamic_module",
116+
}
117+
108118
_DISALLOWED_CALLABLE_FIELD_NAMES: set[str] = {
109119
"collate_impl",
110120
"hf_filter_lambda",
@@ -182,7 +192,19 @@ def _resolve_target(
182192
"""Resolve target string, type, or callable after Bridge validation."""
183193
if isinstance(target, str):
184194
_validate_target_prefix(target=target, full_key=full_key)
185-
return _mcore_resolve_target(target, full_key, check_callable)
195+
resolved_target = _mcore_resolve_target(target, full_key, check_callable)
196+
if isinstance(target, str):
197+
module = getattr(resolved_target, "__module__", None)
198+
qualname = getattr(resolved_target, "__qualname__", None)
199+
canonical_target = f"{module}.{qualname}" if isinstance(module, str) and isinstance(qualname, str) else None
200+
if canonical_target in _DISALLOWED_CANONICAL_TARGETS:
201+
raise InstantiationException(
202+
f"Instantiation of '{target}' is not allowed because it resolves to the unsafe target "
203+
f"'{canonical_target}'." + (f"\nfull_key: {full_key}" if full_key else "")
204+
)
205+
if canonical_target is not None:
206+
_validate_target_prefix(target=canonical_target, full_key=full_key)
207+
return resolved_target
186208

187209

188210
_mcore_instantiate_utils._resolve_target = _resolve_target

src/megatron/bridge/utils/safe_pickle.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,32 @@ def find_class(self, module: str, name: str) -> object:
239239
)
240240

241241

242+
def _safe_torch_load_from_bytes(data: bytes) -> object:
243+
"""Reconstruct tensor storage bytes without enabling arbitrary pickle globals."""
244+
import torch
245+
246+
return torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
247+
248+
249+
class _TorchTensorRestrictedUnpickler(_RestrictedUnpickler):
250+
"""Restricted unpickler for plain tensors and containers of tensors."""
251+
252+
_SAFE_MODULES = MappingProxyType(
253+
{
254+
**_RestrictedUnpickler._SAFE_MODULES,
255+
"torch._utils": frozenset({"_rebuild_tensor", "_rebuild_tensor_v2"}),
256+
}
257+
)
258+
259+
def find_class(self, module: str, name: str) -> object:
260+
if (module, name) in {
261+
("megatron.core.safe_globals", "safe_load_from_bytes"),
262+
("torch.storage", "_load_from_bytes"),
263+
}:
264+
return _safe_torch_load_from_bytes
265+
return super().find_class(module, name)
266+
267+
242268
class _NumpyRestrictedUnpickler(pickle.Unpickler):
243269
"""Unpickler that allows safe builtins and the narrow set of numpy types needed for object array reconstruction.
244270
@@ -428,6 +454,11 @@ def safe_pickle_loads(data: bytes) -> object:
428454
return _RestrictedUnpickler(io.BytesIO(data)).load()
429455

430456

457+
def safe_torch_tensor_pickle_loads(data: bytes) -> object:
458+
"""Deserialize raw pickle data containing only safe containers and plain torch tensors."""
459+
return _TorchTensorRestrictedUnpickler(io.BytesIO(data)).load()
460+
461+
431462
def safe_load_npy(data: bytes):
432463
"""Load a ``.npy`` file from raw bytes without enabling unrestricted pickle.
433464

0 commit comments

Comments
 (0)