From 6ddac0130450b39b1e7c25d4bf66e46b73f7c6e4 Mon Sep 17 00:00:00 2001 From: Daniel Baston Date: Fri, 6 Dec 2024 11:19:25 -0500 Subject: [PATCH] Avoid crash when converting dict with circular reference Fixes https://github.com/pybind/pybind11_json/issues/73 --- include/pybind11_json/pybind11_json.hpp | 17 ++++++++++++++--- test/test_pybind11_json.cpp | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/include/pybind11_json/pybind11_json.hpp b/include/pybind11_json/pybind11_json.hpp index 2b7d9b2..c97c5aa 100644 --- a/include/pybind11_json/pybind11_json.hpp +++ b/include/pybind11_json/pybind11_json.hpp @@ -67,8 +67,12 @@ namespace pyjson } } - inline nl::json to_json(const py::handle& obj) + inline nl::json to_json_inner(const py::handle& obj, std::set& refs) { + if (auto [_, unique] = refs.insert(obj.ptr()); !unique) { + throw std::runtime_error("Circular reference detected"); + } + if (obj.ptr() == nullptr || obj.is_none()) { return nullptr; @@ -121,7 +125,7 @@ namespace pyjson auto out = nl::json::array(); for (const py::handle value : obj) { - out.push_back(to_json(value)); + out.push_back(to_json_inner(value, refs)); } return out; } @@ -130,12 +134,19 @@ namespace pyjson auto out = nl::json::object(); for (const py::handle key : obj) { - out[py::str(key).cast()] = to_json(obj[key]); + out[py::str(key).cast()] = to_json_inner(obj[key], refs); } return out; } throw std::runtime_error("to_json not implemented for this type of object: " + py::repr(obj).cast()); } + + inline nl::json to_json(const py::handle& obj) + { + std::set refs; + return to_json_inner(obj, refs); + } + } // nlohmann_json serializers diff --git a/test/test_pybind11_json.cpp b/test/test_pybind11_json.cpp index 2d9cc82..b9ad588 100644 --- a/test/test_pybind11_json.cpp +++ b/test/test_pybind11_json.cpp @@ -481,3 +481,17 @@ TEST(pybind11_caster_fromjson, dict) ASSERT_EQ(j["number"].cast(), 1234); ASSERT_EQ(j["hello"].cast(), "world"); } + +TEST(pybind11_caster_tojson, recursive_dict) +{ + py::scoped_interpreter guard; + py::module m = create_module("test"); + + m.def("to_json", &test_fromtojson); + + // Simulate calling this binding from Python with a dictionary as argument + py::dict obj("number"_a=1234, "hello"_a="world"); + obj["recur"] = obj; + + ASSERT_ANY_THROW(m.attr("to_json")(obj)); +}