Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 68 additions & 12 deletions onnxruntime/core/framework/tensorprotoutils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2097,6 +2097,33 @@ void MakeCpuTensorCopy(const Tensor& src_tensor, Tensor& dst_tensor) {
}

#if !defined(DISABLE_SPARSE_TENSORS)

// Validates that a TensorProto's external data path does not escape the model directory.
// Also validates that the file exists when filesystem access is available (skipped on WASM without a virtual FS).
// Returns Status::OK() (no-op) for tensors that do not use file-based external data.
static Status ValidateExternalDataPathForTensor(const ONNX_NAMESPACE::TensorProto& tensor_proto,
const std::filesystem::path& model_path) {
// Gates on data_location == EXTERNAL directly instead of using HasExternalData()/HasExternalDataInFile(),
// which also require data_type != UNDEFINED. That check is appropriate for data processing (can't unpack
// without a type), but too narrow for security validation: we must validate any declared external path
// regardless of data_type.
if (tensor_proto.data_location() != ONNX_NAMESPACE::TensorProto_DataLocation_EXTERNAL) {
return Status::OK();
}

std::unique_ptr<ExternalDataInfo> external_data_info;
ORT_RETURN_IF_ERROR(ExternalDataInfo::Create(tensor_proto.external_data(), external_data_info));
const auto& rel_path = external_data_info->GetRelPath();

// In-memory external data uses special marker locations — skip file path validation for those.
if (rel_path == kTensorProtoLittleEndianMemoryAddressTag ||
rel_path == kTensorProtoNativeEndianMemoryAddressTag) {
return Status::OK();
}

return utils::ValidateExternalDataPath(model_path, rel_path);
}

static Status CopySparseData(const std::string& name,
int64_t nnz_elements,
const ONNX_NAMESPACE::TensorProto& indices,
Expand All @@ -2115,10 +2142,18 @@ static Status CopySparseData(const std::string& name,
switch (indices.data_type()) {
case ONNX_NAMESPACE::TensorProto_DataType_INT64:
Comment thread
adrianlizarraga marked this conversation as resolved.
if (needs_unpack) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == SafeInt<size_t>(indices_elements) * sizeof(int64_t),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int64_t));
// For inline raw_data, validate size before unpacking to avoid a large allocation from a
// malformed tensor with small indices shape but oversized raw_data. For external data,
// raw_data is empty so we can only validate after unpacking.
if (!utils::HasExternalData(indices)) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == SafeInt<size_t>(indices_elements) * sizeof(int64_t),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int64_t));
}
ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer));
ORT_RETURN_IF_NOT(unpack_buffer.size() == SafeInt<size_t>(indices_elements) * sizeof(int64_t),
"Sparse tensor: ", name, " indices data size does not match expected: ",
indices_elements * sizeof(int64_t));
Comment thread
adrianlizarraga marked this conversation as resolved.
indices_data = ReinterpretAsSpan<const int64_t>(gsl::make_span(unpack_buffer));
} else {
ORT_RETURN_IF_NOT(indices.int64_data_size() == indices_elements,
Expand All @@ -2129,10 +2164,15 @@ static Status CopySparseData(const std::string& name,
break;
case ONNX_NAMESPACE::TensorProto_DataType_INT32: {
if (needs_unpack) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == SafeInt<size_t>(indices_elements) * sizeof(int32_t),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int32_t));
if (!utils::HasExternalData(indices)) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == SafeInt<size_t>(indices_elements) * sizeof(int32_t),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int32_t));
}
ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer));
ORT_RETURN_IF_NOT(unpack_buffer.size() == SafeInt<size_t>(indices_elements) * sizeof(int32_t),
"Sparse tensor: ", name, " indices data size does not match expected: ",
indices_elements * sizeof(int32_t));
auto int32_span = ReinterpretAsSpan<const int32_t>(gsl::make_span(unpack_buffer));
indices_values.insert(indices_values.cend(), int32_span.begin(), int32_span.end());
unpack_buffer.clear();
Expand All @@ -2148,10 +2188,15 @@ static Status CopySparseData(const std::string& name,
}
case ONNX_NAMESPACE::TensorProto_DataType_INT16: {
if (needs_unpack) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == SafeInt<size_t>(indices_elements) * sizeof(int16_t),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int16_t));
if (!utils::HasExternalData(indices)) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == SafeInt<size_t>(indices_elements) * sizeof(int16_t),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int16_t));
}
ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer));
ORT_RETURN_IF_NOT(unpack_buffer.size() == SafeInt<size_t>(indices_elements) * sizeof(int16_t),
"Sparse tensor: ", name, " indices data size does not match expected: ",
indices_elements * sizeof(int16_t));
auto int16_span = ReinterpretAsSpan<const int16_t>(gsl::make_span(unpack_buffer));
indices_values.insert(indices_values.cend(), int16_span.begin(), int16_span.end());
unpack_buffer.clear();
Expand All @@ -2167,10 +2212,15 @@ static Status CopySparseData(const std::string& name,
}
case ONNX_NAMESPACE::TensorProto_DataType_INT8: {
if (needs_unpack) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == narrow<size_t>(indices_elements),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int8_t));
if (!utils::HasExternalData(indices)) {
ORT_RETURN_IF_NOT(indices.raw_data().size() == narrow<size_t>(indices_elements),
"Sparse tensor: ", name, " indices raw data size does not match expected: ",
indices_elements * sizeof(int8_t));
}
ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer));
ORT_RETURN_IF_NOT(unpack_buffer.size() == narrow<size_t>(indices_elements),
"Sparse tensor: ", name, " indices data size does not match expected: ",
indices_elements * sizeof(int8_t));
auto int8_span = ReinterpretAsSpan<const int8_t>(gsl::make_span(unpack_buffer));
indices_values.insert(indices_values.cend(), int8_span.begin(), int8_span.end());
unpack_buffer.clear();
Expand Down Expand Up @@ -2318,6 +2368,12 @@ common::Status SparseTensorProtoToDenseTensorProto(const ONNX_NAMESPACE::SparseT
}
}

// Validate external data paths before any early returns or allocations.
// This ensures malicious paths are rejected even for zero-element tensors,
// and prevents large allocations before an invalid path is caught.
ORT_RETURN_IF_ERROR(ValidateExternalDataPathForTensor(sparse_values, model_path));
ORT_RETURN_IF_ERROR(ValidateExternalDataPathForTensor(indices, model_path));
Comment thread
adrianlizarraga marked this conversation as resolved.

if (dense_elements == 0) {
// if there are no elements in the dense tensor, we can return early with an empty tensor proto
return status;
Expand Down
Loading
Loading