This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new 9a863e8 Robustify FromJSONGraph (#619)
9a863e8 is described below
commit 9a863e8a9349cb8be47da31a6c9ae05c2fb92653
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Jun 10 13:59:09 2026 -0400
Robustify FromJSONGraph (#619)
Bounds-check the node index and add small arity checks so malformed JSON
raises ValueError instead of reading out of bounds. Covered by a C++
malformed-input test.
---
src/ffi/extra/serialization.cc | 18 +++++++++++++
tests/cpp/extra/test_serialization.cc | 50 +++++++++++++++++++++++++++++++++++
2 files changed, 68 insertions(+)
diff --git a/src/ffi/extra/serialization.cc b/src/ffi/extra/serialization.cc
index e32d647..a4b820d 100644
--- a/src/ffi/extra/serialization.cc
+++ b/src/ffi/extra/serialization.cc
@@ -270,6 +270,12 @@ class ObjectGraphDeserializer {
}
Any GetOrDecodeNode(int64_t node_index) {
+ // node_index comes from the input (root_index and child references), so
+ // validate it before indexing into decoded_nodes_ / nodes_, which would
+ // otherwise read out of bounds.
+ if (node_index < 0 || node_index >= static_cast<int64_t>(nodes_.size())) {
+ TVM_FFI_THROW(ValueError) << "Invalid JSON Object Graph: invalid node
index " << node_index;
+ }
// already decoded null index
if (node_index == decoded_null_index_) {
return Any(nullptr);
@@ -312,6 +318,11 @@ class ObjectGraphDeserializer {
}
case TypeIndex::kTVMFFIDevice: {
Array<int32_t> data = node["data"].cast<Array<int32_t>>();
+ if (data.size() != 2) {
+ TVM_FFI_THROW(ValueError)
+ << "Invalid JSON Object Graph: Device data must be an array of "
+ << "[device_type, device_id], got " << data.size() << "
element(s)";
+ }
return DLDevice{static_cast<DLDeviceType>(data[0]), data[1]};
}
case TypeIndex::kTVMFFIStr: {
@@ -356,6 +367,13 @@ class ObjectGraphDeserializer {
MapType DecodeMapLikeData(const json::Array& data) {
MapType result;
const int64_t n = static_cast<int64_t>(data.size());
+ // Map/Dict data is a flat array of alternating [key, value] indices, so
the
+ // length must be even; an odd length means the input is malformed and
would
+ // otherwise read data[i + 1] past the end on the final iteration.
+ if (n % 2 != 0) {
+ TVM_FFI_THROW(ValueError) << "Invalid JSON Object Graph: Map/Dict data
must contain an even "
+ << "number of [key, value] entries, got " << n;
+ }
for (int64_t i = 0; i < n; i += 2) {
int64_t key_index = data[i].cast<int64_t>();
int64_t value_index = data[i + 1].cast<int64_t>();
diff --git a/tests/cpp/extra/test_serialization.cc
b/tests/cpp/extra/test_serialization.cc
index 664c055..8d29f5b 100644
--- a/tests/cpp/extra/test_serialization.cc
+++ b/tests/cpp/extra/test_serialization.cc
@@ -819,6 +819,56 @@ TEST(Serialization, ErrorMissingNodes) {
EXPECT_ANY_THROW(FromJSONGraph(graph));
}
+// ---------------------------------------------------------------------------
+// Malformed-input validation: every case below must THROW an ffi::Error rather
+// than read out of bounds when deserializing an object graph.
+// ---------------------------------------------------------------------------
+TEST(Serialization, MalformedInput) {
+ // NOTE: use EXPECT_ANY_THROW rather than EXPECT_THROW(..., tvm::ffi::Error).
+ // FromJSONGraph is compiled into the shared library, so the tvm::ffi::Error
it
+ // throws carries the library's typeinfo. On macOS (hidden-visibility
typeinfo)
+ // that does not match the test executable's typeinfo, so an exact-type match
+ // spuriously fails even though the error is thrown correctly. This matches
the
+ // other Serialization.Error* tests in this file, which also use
EXPECT_ANY_THROW.
+ auto expect_throws = [](const json::Object& graph) {
EXPECT_ANY_THROW(FromJSONGraph(graph)); };
+
+ // root_index points past the end of the nodes array.
+ expect_throws({{"root_index", 99}, {"nodes",
json::Array{json::Object{{"type", "None"}}}}});
+
+ // root_index is negative.
+ expect_throws({{"root_index", -5}, {"nodes",
json::Array{json::Object{{"type", "None"}}}}});
+
+ // A child reference inside an array node is out of range.
+ expect_throws(
+ {{"root_index", 0},
+ {"nodes", json::Array{json::Object{{"type", "ffi.Array"}, {"data",
json::Array{42}}}}}});
+
+ // A key/value reference inside a map node is out of range.
+ expect_throws(
+ {{"root_index", 0},
+ {"nodes", json::Array{json::Object{{"type", "ffi.Map"}, {"data",
json::Array{5, 6}}}}}});
+
+ // Map data has an odd number of entries (would read one past the end).
+ expect_throws({{"root_index", 0},
+ {"nodes", json::Array{json::Object{{"type", "ffi.Map"},
{"data", json::Array{0}}},
+ json::Object{{"type", "int"}, {"data",
1}}}}});
+
+ // Device data has the wrong number of elements.
+ expect_throws(
+ {{"root_index", 0},
+ {"nodes", json::Array{json::Object{{"type", "Device"}, {"data",
json::Array{1}}}}}});
+
+ // A node is missing the required "type" key.
+ expect_throws({{"root_index", 0}, {"nodes",
json::Array{json::Object{{"data", 1}}}}});
+
+ // A node has the wrong value type for a child reference (string where an int
+ // index is expected).
+ expect_throws(
+ {{"root_index", 0},
+ {"nodes", json::Array{json::Object{{"type", "ffi.Array"},
+ {"data",
json::Array{String("not-an-index")}}}}}});
+}
+
// ---------------------------------------------------------------------------
// String serialization roundtrip (json::Stringify / json::Parse)
// ---------------------------------------------------------------------------