Skip to content
Open
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
21 changes: 21 additions & 0 deletions onnxruntime/core/providers/openvino/ov_bin_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,22 @@ void BinManager::DeserializeImpl(std::istream& stream, const std::shared_ptr<Sha
ORT_ENFORCE(header.version == to_underlying(BinVersion::current), "Error: Unsupported file version: ", header.version);
ORT_ENFORCE(header.header_size == sizeof(header_t), "Error: Header size mismatch.");

// Determine the actual size of the stream so that all offsets/sizes read from
// the (untrusted) file can be validated before being used as allocation sizes.
stream.seekg(0, std::ios::end);
ORT_ENFORCE(stream.good(), "Error: Failed to seek to end of stream.");
const uint64_t stream_size = static_cast<uint64_t>(stream.tellg());

Comment on lines +352 to +355
// Overflow-safe check that [offset, offset + size) fits within the stream.
auto range_within_stream = [stream_size](uint64_t offset, uint64_t size) {
return offset <= stream_size && size <= stream_size - offset;
};

// Validate the BSON region lies fully within the file before allocating for it.
ORT_ENFORCE(range_within_stream(header.bson_start_offset, header.bson_size),
"Error: BSON region out of bounds. Offset: ", header.bson_start_offset,
" Size: ", header.bson_size, " File size: ", stream_size);

// Seek to BSON metadata and read it
stream.seekg(header.bson_start_offset);
ORT_ENFORCE(stream.good(), "Error: Failed to seek to BSON metadata.");
Expand Down Expand Up @@ -417,6 +433,11 @@ void BinManager::DeserializeImpl(std::istream& stream, const std::shared_ptr<Sha

// If no external file, extract blob data into vector
if (!has_external_file) {
// Validate the blob range lies fully within the file before allocating for it.
ORT_ENFORCE(range_within_stream(blob_offset, blob_size),
"Error: Blob range out of bounds for ", blob_name,
". Offset: ", blob_offset, " Size: ", blob_size, " File size: ", stream_size);

// Seek to blob offset and read data into vector
auto current_pos = stream.tellg();
stream.seekg(blob_offset);
Expand Down
132 changes: 132 additions & 0 deletions onnxruntime/test/providers/openvino/openvino_ep_context_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,137 @@ TEST_F(OVEPEPContextTests, OVEPEPContextFolderPath) {
}
}

namespace {

// Layout must match header_t in core/providers/openvino/ov_bin_manager.cc.
struct OvBinHeader {
uint64_t magic;
uint64_t version;
uint64_t header_size;
uint64_t bson_start_offset;
uint64_t bson_size;
};

// "OVEP_BIN" in little-endian. Must match kMagicNumber in ov_bin_manager.cc.
constexpr uint64_t kOvBinMagic = 0x4E49425F5045564FULL;

// Builds the byte payload of an OVEP_BIN blob containing only a header. The
// header is valid (correct magic/version/size) but advertises a BSON region
// whose size is far larger than the actual payload, simulating a corrupted or
// malicious EP-context cache.
std::string MakeBinBlobWithOversizedBson(uint64_t bson_size) {
OvBinHeader header{};
header.magic = kOvBinMagic;
header.version = 1; // BinVersion::current
header.header_size = sizeof(OvBinHeader);
header.bson_start_offset = sizeof(OvBinHeader);
header.bson_size = bson_size;

return std::string(reinterpret_cast<const char*>(&header), sizeof(header));
}

// Serializes a synthetic model containing a single embedded-mode EPContext node
// whose "source" is the OpenVINO EP and whose "ep_cache_context" carries the
// supplied OVEP_BIN payload. Loading this model drives BinManager::Deserialize.
std::string MakeEmbeddedEPContextModel(const std::string& ep_cache_context) {
ModelProto model;
model.set_ir_version(ONNX_NAMESPACE::Version::IR_VERSION);
auto* opset = model.add_opset_import();
opset->set_domain("");
opset->set_version(13);
auto* ms_opset = model.add_opset_import();
ms_opset->set_domain(kMSDomain);
ms_opset->set_version(1);

auto* graph = model.mutable_graph();
graph->set_name("OVEP_BinDeserialize_Test");

auto* input = graph->add_input();
input->set_name("input");
auto* input_type = input->mutable_type()->mutable_tensor_type();
input_type->set_elem_type(TensorProto_DataType_FLOAT);
input_type->mutable_shape()->add_dim()->set_dim_value(1);
input_type->mutable_shape()->add_dim()->set_dim_value(3);

auto* output = graph->add_output();
output->set_name("output");
auto* output_type = output->mutable_type()->mutable_tensor_type();
output_type->set_elem_type(TensorProto_DataType_FLOAT);
output_type->mutable_shape()->add_dim()->set_dim_value(1);
output_type->mutable_shape()->add_dim()->set_dim_value(3);

auto* node = graph->add_node();
node->set_op_type("EPContext");
node->set_domain(kMSDomain);
node->set_name("ep_context_node");
node->add_input("input");
node->add_output("output");

auto* attr_embed = node->add_attribute();
attr_embed->set_name("embed_mode");
attr_embed->set_type(AttributeProto_AttributeType_INT);
attr_embed->set_i(1);

auto* attr_main = node->add_attribute();
attr_main->set_name("main_context");
attr_main->set_type(AttributeProto_AttributeType_INT);
attr_main->set_i(1);

auto* attr_cache = node->add_attribute();
attr_cache->set_name("ep_cache_context");
attr_cache->set_type(AttributeProto_AttributeType_STRING);
attr_cache->set_s(ep_cache_context);

auto* attr_source = node->add_attribute();
attr_source->set_name("source");
attr_source->set_type(AttributeProto_AttributeType_STRING);
attr_source->set_s("OpenVINOExecutionProvider");

auto* attr_partition = node->add_attribute();
attr_partition->set_name("partition_name");
attr_partition->set_type(AttributeProto_AttributeType_STRING);
attr_partition->set_s("OVEP_BinDeserialize_Test");

std::string model_data;
model.SerializeToString(&model_data);
return model_data;
Comment on lines +166 to +168
}

} // namespace

// Regression test for the unbounded-allocation hardening in
// BinManager::DeserializeImpl. A crafted EP-context blob advertises a BSON
// region whose size dwarfs the actual payload. Deserialization must reject it
// with a bounded, descriptive error instead of attempting a huge allocation.
TEST_F(OVEPEPContextTests, OVEPBinDeserializeRejectsOversizedBson) {
Ort::SessionOptions session_options;
std::unordered_map<std::string, std::string> ov_options;
// Empty options -> use the device the OVEP build targets (mirrors other tests
// in this file). Skip the test entirely if no OpenVINO device is available.
try {
session_options.AppendExecutionProvider_OpenVINO_V2(ov_options);
} catch (const Ort::Exception&) {
GTEST_SKIP() << "OpenVINO device not available on this machine";
}

auto& logging_manager = DefaultLoggingManager();
logging_manager.SetDefaultLoggerSeverity(logging::Severity::kERROR);

Comment on lines +188 to +190
// bson_start_offset (40) + bson_size (1 TiB) is far beyond the ~40 byte blob.
const std::string malicious_blob = MakeBinBlobWithOversizedBson(uint64_t{1} << 40);
const std::string model_data = MakeEmbeddedEPContextModel(malicious_blob);

try {
Ort::Session session(*ort_env, model_data.data(), model_data.size(), session_options);
FAIL() << "Expected deserialization of an oversized-BSON blob to throw.";
} catch (const Ort::Exception& excpt) {
// The new bounds check produces "BSON region out of bounds ...", which
// BinManager::Deserialize wraps with a "Could not deserialize" message.
// Asserting on "out of bounds" confirms the bounds check fired (rather than
// a std::bad_alloc/length_error from an unbounded allocation attempt).
ASSERT_THAT(excpt.what(), testing::HasSubstr("out of bounds"));
}
}

} // namespace test
} // namespace onnxruntime
Loading