Address external path references in loaded models#28709
Conversation
There was a problem hiding this comment.
Pull request overview
Hardens external data loading against malicious models by (1) adding a backstop path-validation call in GetExtDataFromTensorProto/LoadExtDataToTensorFromTensorProto so direct callers outside Graph::Resolve are also protected, and (2) explicitly rejecting ORT in-memory address markers (*/_ORT_MEM_ADDR_/*, */_ORT_NATIVE_ENDIAN_MEM_ADDR_/*) when they appear in dense initializers, sparse initializers, or sparse Constant-node attributes loaded from a protobuf. The in-memory markers are ORT-internal sentinels that cause reinterpret_cast<void*>(offset)-based dereferences; allowing them from an .onnx protobuf is a memory-safety hazard. ORT-format flatbuffer loading and the ModelEditor API use different Graph constructors and are unaffected.
Changes:
- Introduce
ValidateExternalFilePathForTensorand call it fromGetExtDataFromTensorProto/LoadExtDataToTensorFromTensorProto; rename/refactor the sparse-only validator toValidateSparseSubTensorExternalDataPath. - In
Graph::Graph(GraphProto ctor),ORT_ENFORCEagainst in-memory address markers on dense initializers, sparse initializers (values + indices), and Constant-node sparse-tensor attributes. - Add direct loader tests for absolute and directory-escaping paths, plus model-load tests that reject in-memory markers on dense and sparse initializers; update one optimizer test to expect
OnnxRuntimeExceptioninstead offilesystem_error.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/core/framework/tensorprotoutils.cc | Adds ValidateExternalFilePathForTensor backstop and renames the sparse helper to ValidateSparseSubTensorExternalDataPath. |
| onnxruntime/core/graph/graph.cc | Rejects in-memory address markers on dense initializers, sparse initializers, and sparse Constant attributes in the GraphProto ctor. |
| onnxruntime/test/framework/tensorutils_test.cc | Adds direct GetExtDataFromTensorProto tests for absolute and escaping external paths. |
| onnxruntime/test/ir/graph_test.cc | Adds model-load tests verifying in-memory markers are rejected on dense and sparse (values/indices) initializers. |
| onnxruntime/test/optimizer/initializer_test.cc | Updates expected exception type for an invalid external directory to OnnxRuntimeException. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tianleiwu
left a comment
There was a problem hiding this comment.
Security Hardening Review
Strong defense-in-depth PR. The layered approach — Graph ctor as the gatekeeper for untrusted protobufs, with validation helpers enforcing path constraints at data-loading callsites — is well-designed.
Positives:
- Adding path validation inside
GetExtDataFromTensorProtoandLoadExtDataToTensorFromTensorProtoensures callers outsideGraph::Resolve(training checkpoints, custom-op initializers) are protected. - Rejecting in-memory markers at the protobuf entry point (Graph ctor) is the correct architectural choice — prevents attacker-controlled pointers from ever reaching data-loading code.
- Renaming to
ValidateSparseSubTensorExternalDataPathmakes scope and intent clearer. - The trust-boundary comments (ORT-format flatbuffer = trusted, protobuf = untrusted) are valuable for future maintainers.
- Test coverage is thorough: sparse values/indices markers, dense initializer markers, absolute paths, and escaping paths.
One minor suggestion inline.
…t_address ORT marks initializers whose data lives in an in-process memory buffer with one of two location sentinels: the original little-endian tag "*/_ORT_MEM_ADDR_/*" (kTensorProtoLittleEndianMemoryAddressTag) and the newer native-endian tag "*/_ORT_NATIVE_ENDIAN_MEM_ADDR_/*" (kTensorProtoNativeEndianMemoryAddressTag). Since microsoft#27404, TensorToTensorProto() emits the native-endian tag by default, but the VitisAI EP's process_ext_address only matched the little-endian tag. As a result, in-memory initializers (e.g. quantized weights moved out of the TensorProto during graph optimization) are no longer detected by model_clone, fall through to the verbatim-copy branch, and carry the in-memory marker into the cloned model protobuf. After microsoft#28709 added an explicit ORT_ENFORCE in Graph::Graph rejecting in-memory address markers in deserialized protobufs, this now aborts model compilation with: Initializer '...' references an ORT in-memory address marker, which is not allowed in a model protobuf. Both tags encode the buffer address in the 'offset' field; on little-endian hosts (the only platform this EP targets) the byte layout is identical, so they are decoded the same way. Recognizing both tags restores the pre-1.27 behavior of converting these initializers to a lightweight reference during model_clone.
…t_address (#29248) ### Description `vaip::process_ext_address` (in the VitisAI EP) decodes the in-memory "external data" address marker that ORT plants on initializers whose data lives in an in-process buffer. It only matched the little-endian tag `"*/_ORT_MEM_ADDR_/*"` (`kTensorProtoLittleEndianMemoryAddressTag`). This PR makes it also recognize the native-endian tag `"*/_ORT_NATIVE_ENDIAN_MEM_ADDR_/*"` (`kTensorProtoNativeEndianMemoryAddressTag`). ```cpp if (file == "*/_ORT_MEM_ADDR_/*" || file == "*/_ORT_NATIVE_ENDIAN_MEM_ADDR_/*") { auto addr = reinterpret_cast<const char*>(offset); return {addr, size}; } ``` ### Motivation and Context Two existing changes combined to break VitisAI model compilation: 1. #27404 split the single in-memory marker into a little-endian and a native-endian variant, and switched `TensorToTensorProto(use_tensor_buffer=true)` to emit the **native-endian** tag by default. Both `HasExternalDataInMemory` and the data readers treat the two tags equivalently. 2. #28709 added an explicit `ORT_ENFORCE(!utils::HasExternalDataInMemory(tensor), ...)` in `Graph::Graph`, rejecting any in-memory address marker found in a deserialized model protobuf. Because `process_ext_address` still only recognized the little-endian tag, in-memory initializers (e.g. quantized weights moved out of the `TensorProto` during graph optimization) are no longer detected in `vaip::model_clone`. They fall through to the verbatim-copy branch and the native-endian marker is carried into the cloned model proto. When ORT then constructs the cloned `Graph`, the new enforce fires and aborts compilation: ``` Initializer 'onnx::Conv_1575_quantized' references an ORT in-memory address marker, which is not allowed in a model protobuf. ``` This regression is observed on 1.27.0, where both changes are present (1.25.x / 1.26.x have the native-endian tag but not the enforce, so the marker leaked silently without crashing). Both tags encode the buffer address in the `offset` field; on little-endian hosts (the only platform this EP targets) the byte layout is identical, so they are decoded the same way. Recognizing both tags restores the previous behavior of converting these initializers into a lightweight reference inside `model_clone`, so the in-memory marker never reaches `Graph::Graph`.
…t_address (#29248) ### Description `vaip::process_ext_address` (in the VitisAI EP) decodes the in-memory "external data" address marker that ORT plants on initializers whose data lives in an in-process buffer. It only matched the little-endian tag `"*/_ORT_MEM_ADDR_/*"` (`kTensorProtoLittleEndianMemoryAddressTag`). This PR makes it also recognize the native-endian tag `"*/_ORT_NATIVE_ENDIAN_MEM_ADDR_/*"` (`kTensorProtoNativeEndianMemoryAddressTag`). ```cpp if (file == "*/_ORT_MEM_ADDR_/*" || file == "*/_ORT_NATIVE_ENDIAN_MEM_ADDR_/*") { auto addr = reinterpret_cast<const char*>(offset); return {addr, size}; } ``` ### Motivation and Context Two existing changes combined to break VitisAI model compilation: 1. #27404 split the single in-memory marker into a little-endian and a native-endian variant, and switched `TensorToTensorProto(use_tensor_buffer=true)` to emit the **native-endian** tag by default. Both `HasExternalDataInMemory` and the data readers treat the two tags equivalently. 2. #28709 added an explicit `ORT_ENFORCE(!utils::HasExternalDataInMemory(tensor), ...)` in `Graph::Graph`, rejecting any in-memory address marker found in a deserialized model protobuf. Because `process_ext_address` still only recognized the little-endian tag, in-memory initializers (e.g. quantized weights moved out of the `TensorProto` during graph optimization) are no longer detected in `vaip::model_clone`. They fall through to the verbatim-copy branch and the native-endian marker is carried into the cloned model proto. When ORT then constructs the cloned `Graph`, the new enforce fires and aborts compilation: ``` Initializer 'onnx::Conv_1575_quantized' references an ORT in-memory address marker, which is not allowed in a model protobuf. ``` This regression is observed on 1.27.0, where both changes are present (1.25.x / 1.26.x have the native-endian tag but not the enforce, so the marker leaked silently without crashing). Both tags encode the buffer address in the `offset` field; on little-endian hosts (the only platform this EP targets) the byte layout is identical, so they are decoded the same way. Recognizing both tags restores the previous behavior of converting these initializers into a lightweight reference inside `model_clone`, so the in-memory marker never reaches `Graph::Graph`.
This pull request strengthens security checks around loading external tensor data in ONNX Runtime, particularly to prevent malicious models from referencing unsafe file paths or in-memory address markers that could lead to arbitrary file access or unsafe memory dereferencing. The changes introduce stricter validation for external data paths and add explicit rejections for ORT in-memory address markers found in model protobufs, along with new and improved regression tests to verify this behavior.
Security hardening for external data loading:
ValidateExternalFilePathForTensorto enforce that external data paths are validated for all code paths loading external data (including those outsideGraph::Resolve), rejecting absolute or directory-escaping paths and passing through only trusted in-memory markers. This is now called inGetExtDataFromTensorProtoandLoadExtDataToTensorFromTensorPrototo ensure defense-in-depth. [1] [2]ValidateSparseSubTensorExternalDataPath, clarifying the handling of in-memory markers and ensuring only legitimate file paths are accepted.SparseTensorProtoToDenseTensorPrototo use the new sparse sub-tensor validation for both values and indices.Model loading and graph construction protections:
Graph::Graph, added explicit rejection of ORT in-memory address markers in sparse tensor attributes and initializers when loading from a protobuf, preventing attackers from crafting models that could cause unsafe memory access during sparse-to-dense conversion or initializer resolution. [1] [2] [3]Expanded and improved testing: