mirror of
https://github.com/Icinga/icinga2.git
synced 2026-07-09 00:50:59 -04:00
The recursion depth limit added to JsonDecode() in 2.16.2 gave the C++
function a second parameter with a default value. Function pointers do not
carry default arguments, so the DSL function binding deduced an arity of 2
via boost::function_types::function_arity and required two arguments. As a
result `Json.decode("...")` failed with "Too few arguments for function",
an undocumented breaking change in a patch release.
Wrap JsonDecode() in a single-argument shim (mirroring the existing
JsonEncodeShim) so the registered function keeps its one-parameter contract
while still applying the default depth limit internally.
refs #10913
347 lines
11 KiB
C++
347 lines
11 KiB
C++
// SPDX-FileCopyrightText: 2012 Icinga GmbH <https://icinga.com>
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "base/convert.hpp"
|
|
#include "base/dictionary.hpp"
|
|
#include "base/function.hpp"
|
|
#include "base/namespace.hpp"
|
|
#include "base/array.hpp"
|
|
#include "base/generator.hpp"
|
|
#include "base/io-engine.hpp"
|
|
#include "base/objectlock.hpp"
|
|
#include "base/json.hpp"
|
|
#include "base/scriptglobal.hpp"
|
|
#include "test/utils.hpp"
|
|
#include <boost/algorithm/string/replace.hpp>
|
|
#include <BoostTestTargetConfig.h>
|
|
#include <limits>
|
|
#include <cmath>
|
|
|
|
using namespace icinga;
|
|
|
|
BOOST_AUTO_TEST_SUITE(base_json)
|
|
|
|
BOOST_AUTO_TEST_CASE(encode)
|
|
{
|
|
int emptyGenCounter = 0;
|
|
std::vector<int> empty;
|
|
std::vector<int> vec{1, 2, 3};
|
|
auto generate = [](int count) -> Value { return Value(count); };
|
|
|
|
Dictionary::Ptr input (new Dictionary({
|
|
{ "array", new Array({ new Namespace() }) },
|
|
{ "false", false },
|
|
// Use double max value to test JSON encoding of large numbers and trigger boost numeric_cast exceptions
|
|
{ "max_double", std::numeric_limits<double>::max() },
|
|
// Test the largest uint64_t value that has an exact double representation (2^64-2048).
|
|
{ "max_int_in_double", std::nextafter(std::pow(2, 64), 0.0) },
|
|
{ "float", -1.25f },
|
|
{ "float_without_fraction", 23.0f },
|
|
{ "fx", new Function("<test>", []() {}) },
|
|
{ "int", -42 },
|
|
{ "null", Value() },
|
|
{ "string", "LF\nTAB\tAUml\xC3\xA4Ill\xC3" },
|
|
{ "true", true },
|
|
{ "uint", 23u },
|
|
{ "generator", new ValueGenerator{vec, generate} },
|
|
{
|
|
"empty_generator",
|
|
new ValueGenerator{
|
|
empty,
|
|
[&emptyGenCounter](int) -> Value {
|
|
emptyGenCounter++;
|
|
return Empty;
|
|
}
|
|
}
|
|
},
|
|
}));
|
|
|
|
String output (R"EOF({
|
|
"array": [
|
|
{}
|
|
],
|
|
"empty_generator": [],
|
|
"false": false,
|
|
"float": -1.25,
|
|
"float_without_fraction": 23,
|
|
"fx": "Object of type 'Function'",
|
|
"generator": [
|
|
1,
|
|
2,
|
|
3
|
|
],
|
|
"int": -42,
|
|
"max_double": 1.7976931348623157e+308,
|
|
"max_int_in_double": 18446744073709549568,
|
|
"null": null,
|
|
"string": "LF\nTAB\tAUml\u00e4Ill\ufffd",
|
|
"true": true,
|
|
"uint": 23
|
|
}
|
|
)EOF");
|
|
|
|
auto got(JsonEncode(input, true));
|
|
BOOST_CHECK_EQUAL(output, got);
|
|
BOOST_CHECK_EQUAL(emptyGenCounter, 0); // Ensure the transformation function was never invoked.
|
|
input->Set("generator", new ValueGenerator{vec, generate});
|
|
|
|
std::ostringstream oss;
|
|
JsonEncode(input, oss, true);
|
|
BOOST_CHECK_EQUAL(emptyGenCounter, 0); // Ensure the transformation function was never invoked.
|
|
BOOST_CHECK_EQUAL(output, oss.str());
|
|
|
|
boost::algorithm::replace_all(output, " ", "");
|
|
boost::algorithm::replace_all(output, "Objectoftype'Function'", "Object of type 'Function'");
|
|
boost::algorithm::replace_all(output, "\n", "");
|
|
|
|
input->Set("generator", new ValueGenerator{vec, generate});
|
|
BOOST_CHECK(JsonEncode(input, false) == output);
|
|
BOOST_CHECK_EQUAL(emptyGenCounter, 0); // Ensure the transformation function was never invoked.
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(decode)
|
|
{
|
|
String input (R"EOF({
|
|
"array": [
|
|
{}
|
|
],
|
|
"false": false,
|
|
"float": -1.25,
|
|
"int": -42,
|
|
"null": null,
|
|
"string": "LF\nTAB\tAUmlIll",
|
|
"true": true,
|
|
"uint": 23
|
|
}
|
|
)EOF");
|
|
|
|
boost::algorithm::replace_all(input, "AUml", "AUml\xC3\xA4");
|
|
boost::algorithm::replace_all(input, "Ill", "Ill\xC3");
|
|
|
|
auto output ((Dictionary::Ptr)JsonDecode(input));
|
|
BOOST_CHECK(output->GetKeys() == std::vector<String>({"array", "false", "float", "int", "null", "string", "true", "uint"}));
|
|
|
|
auto array ((Array::Ptr)output->Get("array"));
|
|
BOOST_CHECK(array->GetLength() == 1u);
|
|
|
|
auto array0 ((Dictionary::Ptr)array->Get(0));
|
|
BOOST_CHECK(array0->GetKeys() == std::vector<String>());
|
|
|
|
auto fAlse (output->Get("false"));
|
|
BOOST_CHECK(fAlse.IsBoolean() && !fAlse.ToBool());
|
|
|
|
auto fLoat (output->Get("float"));
|
|
BOOST_CHECK(fLoat.IsNumber() && fLoat.Get<double>() == -1.25);
|
|
|
|
auto iNt (output->Get("int"));
|
|
BOOST_CHECK(iNt.IsNumber() && iNt.Get<double>() == -42.0);
|
|
|
|
BOOST_CHECK(output->Get("null").IsEmpty());
|
|
|
|
auto string (output->Get("string"));
|
|
BOOST_CHECK(string.IsString() && string.Get<String>() == "LF\nTAB\tAUml\xC3\xA4Ill\xEF\xBF\xBD");
|
|
|
|
auto tRue (output->Get("true"));
|
|
BOOST_CHECK(tRue.IsBoolean() && tRue.ToBool());
|
|
|
|
auto uint (output->Get("uint"));
|
|
BOOST_CHECK(uint.IsNumber() && uint.Get<double>() == 23.0);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(decode_dsl_single_argument)
|
|
{
|
|
/* Regression test for #10913: the Json.decode() DSL function must accept a
|
|
* single argument. JsonDecode()'s optional depthLimit parameter must not
|
|
* leak into the function binding as a required argument.
|
|
*/
|
|
Namespace::Ptr systemNS = ScriptGlobal::Get("System");
|
|
Namespace::Ptr jsonNS = systemNS->Get("Json");
|
|
Function::Ptr decode = jsonNS->Get("decode");
|
|
|
|
BOOST_REQUIRE(decode);
|
|
BOOST_CHECK_EQUAL(decode->Invoke({ "2" }), Value(2));
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(invalid1)
|
|
{
|
|
BOOST_CHECK_THROW(JsonDecode("\"1.7"), std::exception);
|
|
BOOST_CHECK_THROW(JsonDecode("{8: \"test\"}"), std::exception);
|
|
BOOST_CHECK_THROW(JsonDecode("{\"test\": \"test\""), std::exception);
|
|
}
|
|
|
|
static std::string MakeNestedJsonArray(size_t depth)
|
|
{
|
|
return std::string(depth, '[') + std::string(depth, ']');
|
|
}
|
|
|
|
static std::string MakeNestedJsonObject(size_t depth)
|
|
{
|
|
std::ostringstream buf;
|
|
for (size_t i = 0; i < depth; ++i) {
|
|
buf << "{\"" << i << "\":";
|
|
}
|
|
buf << "null";
|
|
for (size_t i = 0; i < depth; ++i) {
|
|
buf << "}";
|
|
}
|
|
return buf.str();
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(decode_depth_limit)
|
|
{
|
|
auto ExpectLimitExceeded = [](std::string_view input, size_t limit, std::string_view path) {
|
|
try {
|
|
JsonDecode(std::string(input), limit);
|
|
|
|
boost::test_tools::assertion_result result{false};
|
|
result.message() << "Decoding '" << input << "' with limit " << limit << " did not throw an exception";
|
|
return result;
|
|
} catch (const std::exception& ex) {
|
|
std::ostringstream expected;
|
|
expected << "JSON decoding recursion limit reached (path: " << path << ")";
|
|
|
|
boost::test_tools::assertion_result result{ex.what() == expected.str()};
|
|
result.message() << "Decoding '" << input << "' with limit " << limit << ":\n"
|
|
<< " got exception: " << ex.what() << "\n"
|
|
<< " expected exception: " << expected.str() << "\n";
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// Scalars parse even with depth limit 0.
|
|
BOOST_CHECK_EQUAL(JsonDecode("42", 0), Value(42));
|
|
BOOST_CHECK_EQUAL(JsonDecode("true", 0), Value(true));
|
|
BOOST_CHECK_EQUAL(JsonDecode("\"test\"", 0), Value("test"));
|
|
|
|
// Arrays and objects require at least depth limit 1.
|
|
BOOST_CHECK(ExpectLimitExceeded("[]", 0, "root"));
|
|
BOOST_CHECK(ExpectLimitExceeded("{}", 0, "root"));
|
|
BOOST_CHECK(ExpectLimitExceeded("[42]", 0, "root"));
|
|
BOOST_CHECK(ExpectLimitExceeded("{\"foo\": 23}", 0, "root"));
|
|
|
|
// Array in array and object in object require at least depth limit 2.
|
|
BOOST_CHECK(ExpectLimitExceeded("[[]]", 1, "root[0]"));
|
|
BOOST_CHECK_NO_THROW(JsonDecode("[[]]", 2));
|
|
BOOST_CHECK(ExpectLimitExceeded(R"({"a":{}})", 1, R"(root["a"])"));
|
|
BOOST_CHECK_NO_THROW(JsonDecode(R"({"a":{}})", 2));
|
|
|
|
// Mixed nesting of arrays and objects is both counted towards the same limit.
|
|
BOOST_CHECK(ExpectLimitExceeded("[{}]", 1, "root[0]"));
|
|
BOOST_CHECK_NO_THROW(JsonDecode("[{}]", 2));
|
|
BOOST_CHECK(ExpectLimitExceeded(R"({"a":[]})", 1, R"(root["a"])"));
|
|
BOOST_CHECK_NO_THROW(JsonDecode(R"({"a":[]})", 2));
|
|
|
|
// Siblings are not added up for the depth check.
|
|
BOOST_CHECK_NO_THROW(JsonDecode("[[], [], [], {}, [], [], {}, {}, [], {}, {}, {}]", 2));
|
|
BOOST_CHECK_NO_THROW(JsonDecode(R"({"a": [], "b": {}, "c": {}, "d": [], "e": [], "f": {}})", 2));
|
|
|
|
// Some deeper nested array.
|
|
std::string arrayWithNesting42 = MakeNestedJsonArray(42);
|
|
std::ostringstream arrayPathWithNesting41;
|
|
arrayPathWithNesting41 << "root";
|
|
for (size_t i = 0; i < 41; ++i) {
|
|
arrayPathWithNesting41 << "[0]";
|
|
}
|
|
BOOST_CHECK(ExpectLimitExceeded(arrayWithNesting42, 41, arrayPathWithNesting41.str()));
|
|
BOOST_CHECK_NO_THROW(JsonDecode(arrayWithNesting42, 42));
|
|
|
|
// Some deeper nested object.
|
|
std::string objectWithNesting42 = MakeNestedJsonObject(42);
|
|
std::ostringstream objectPathWithNesting41;
|
|
objectPathWithNesting41 << "root";
|
|
for (size_t i = 0; i < 41; ++i) {
|
|
objectPathWithNesting41 << "[\"" << i << "\"]";
|
|
}
|
|
BOOST_CHECK(ExpectLimitExceeded(objectWithNesting42, 41, objectPathWithNesting41.str()));
|
|
BOOST_CHECK_NO_THROW(JsonDecode(objectWithNesting42, 42));
|
|
|
|
// And some deeper nested mixed containers.
|
|
std::string deeperMixedNesting = R"({
|
|
"1st-level": [
|
|
true,
|
|
"2nd-level",
|
|
{
|
|
"dummy": 42,
|
|
"3rd-level": {
|
|
"4th-level": [
|
|
"5th-level",
|
|
{
|
|
"6th-level": {
|
|
"7th-level": {
|
|
"8th-level": [
|
|
"9th-level",
|
|
[
|
|
"10th-level"
|
|
]
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
})";
|
|
auto deeperPath = R"(root["1st-level"][2]["3rd-level"]["4th-level"][1]["6th-level"]["7th-level"]["8th-level"][1])";
|
|
BOOST_CHECK(ExpectLimitExceeded(deeperMixedNesting, 9, deeperPath));
|
|
BOOST_CHECK_NO_THROW(JsonDecode(deeperMixedNesting, 10));
|
|
}
|
|
|
|
/* This test case decodes JSON nested much deeper (see safetyFactor) than the default depth limit and performs some
|
|
* operations on the resulting values. This is done with its limited stack size on order to verify that the default
|
|
* limit is low enough to be safe. Note that this isn't an exact science unfortunately: other recursive operations may
|
|
* have larger stack frames and these operations aren't the only thing on a stack (could be done inside an HTTP or
|
|
* JSON-RPC connection for example). Therefor, aim for a sufficiently large safety factor. The test function may be
|
|
* executed twice, once in a coroutine (which may not have a guard page and reliably detect an overflow depending on
|
|
* the Boost version), once in a pthread thread (which may not be available everywhere).
|
|
*/
|
|
static void TestJsonStackSize()
|
|
{
|
|
constexpr size_t safetyFactor = 10;
|
|
constexpr size_t depth = JsonDecodeDefaultDepthLimit * safetyFactor;
|
|
|
|
Value val;
|
|
|
|
BOOST_REQUIRE_NO_THROW(val = JsonDecode(MakeNestedJsonArray(depth), depth));
|
|
BOOST_REQUIRE_NO_THROW(val.Clone());
|
|
BOOST_REQUIRE_NO_THROW(Convert::ToString(val));
|
|
BOOST_REQUIRE_NO_THROW(JsonDecode(JsonEncode(val), depth));
|
|
|
|
BOOST_REQUIRE_NO_THROW(val = JsonDecode(MakeNestedJsonObject(depth), depth));
|
|
BOOST_REQUIRE_NO_THROW(val.Clone());
|
|
BOOST_REQUIRE_NO_THROW(Convert::ToString(val));
|
|
BOOST_REQUIRE_NO_THROW(JsonDecode(JsonEncode(val), depth));
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(stack_size_coroutine)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([](boost::asio::yield_context) {
|
|
TestJsonStackSize();
|
|
});
|
|
future.get();
|
|
}
|
|
|
|
#ifdef HAVE_PTHREAD_CREATE
|
|
BOOST_AUTO_TEST_CASE(stack_size_pthread)
|
|
{
|
|
auto pagesize = sysconf(_SC_PAGESIZE);
|
|
BOOST_REQUIRE_GT(pagesize, 0);
|
|
|
|
pthread_attr_t attr;
|
|
BOOST_REQUIRE_EQUAL(0, pthread_attr_init(&attr));
|
|
BOOST_REQUIRE_EQUAL(0, pthread_attr_setstacksize(&attr, IoEngine::GetCoroutineStackSize()));
|
|
BOOST_REQUIRE_EQUAL(0, pthread_attr_setguardsize(&attr, pagesize));
|
|
|
|
pthread_t thread;
|
|
BOOST_REQUIRE_EQUAL(0, pthread_create(&thread, &attr, [](void*) -> void* {
|
|
TestJsonStackSize();
|
|
return nullptr;
|
|
}, nullptr));
|
|
|
|
BOOST_REQUIRE_EQUAL(0, pthread_join(thread, nullptr));
|
|
BOOST_REQUIRE_EQUAL(0, pthread_attr_destroy(&attr));
|
|
|
|
}
|
|
#endif /* HAVE_PTHREAD_CREATE */
|
|
|
|
BOOST_AUTO_TEST_SUITE_END()
|