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
82 changes: 75 additions & 7 deletions src/flb_pack.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@

#define try_to_write_str flb_utils_write_str

/*
* Maximum recursion depth allowed while converting a msgpack object into a
* JSON string (see msgpack2json() below). Msgpack arrays and maps can be
* nested arbitrarily deep, and msgpack2json() recurses once per nesting
* level. Without a bound, a deeply nested (malformed, corrupted or
* maliciously crafted) record can recurse deep enough to overflow the
* thread stack and crash the process. 512 levels is far beyond any
* reasonably structured log record while keeping stack usage negligible.
*/
#define FLB_PACK_JSON_MAX_DEPTH 512

static int convert_nan_to_null = FLB_FALSE;

static int flb_pack_set_null_as_nan(int b) {
Expand Down Expand Up @@ -981,14 +992,42 @@ static inline int key_exists_in_map(msgpack_object key, msgpack_object map, int
return FLB_FALSE;
}

/*
* Log the maximum-nesting-depth truncation warning at most once per
* top-level msgpack2json() conversion, regardless of how many separate
* branches in the structure end up being truncated.
*/
static void msgpack2json_depth_warn(int *warned)
{
if (warned != NULL && *warned == FLB_FALSE) {
flb_warn("[pack] msgpack to JSON conversion exceeded the maximum "
"nesting depth (%d), truncating remaining structure",
FLB_PACK_JSON_MAX_DEPTH);
*warned = FLB_TRUE;
}
}

static int msgpack2json(char *buf, int *off, size_t left,
const msgpack_object *o, int escape_unicode)
const msgpack_object *o, int escape_unicode,
int depth, int *warned)
{
int i;
int dup;
int ret = FLB_FALSE;
int loop;
int packed;
msgpack_object *p;

/*
* Stop descending once the maximum nesting depth is reached and encode
* the remaining structure as a JSON null instead of recursing further.
* This keeps the conversion bounded and avoids a stack overflow on
* pathologically nested input, see FLB_PACK_JSON_MAX_DEPTH above.
*/
if (depth > FLB_PACK_JSON_MAX_DEPTH) {
msgpack2json_depth_warn(warned);
return try_to_write(buf, off, left, "null", 4);
}

switch(o->type) {
case MSGPACK_OBJECT_NIL:
Expand Down Expand Up @@ -1078,17 +1117,30 @@ static int msgpack2json(char *buf, int *off, size_t left,
case MSGPACK_OBJECT_ARRAY:
loop = o->via.array.size;

if (loop != 0 && depth + 1 > FLB_PACK_JSON_MAX_DEPTH) {
/*
* The array is non-empty but its elements would exceed the
* maximum nesting depth. Render the whole array as null
* instead of opening it and only then truncating an element,
* keeping the output symmetric with the MSGPACK_OBJECT_MAP
* case below.
*/
msgpack2json_depth_warn(warned);
ret = try_to_write(buf, off, left, "null", 4);
break;
}

if (!try_to_write(buf, off, left, "[", 1)) {
goto msg2json_end;
}
if (loop != 0) {
msgpack_object* p = o->via.array.ptr;
if (!msgpack2json(buf, off, left, p, escape_unicode)) {
p = o->via.array.ptr;
if (!msgpack2json(buf, off, left, p, escape_unicode, depth + 1, warned)) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
goto msg2json_end;
}
for (i=1; i<loop; i++) {
if (!try_to_write(buf, off, left, ",", 1) ||
!msgpack2json(buf, off, left, p+i, escape_unicode)) {
!msgpack2json(buf, off, left, p+i, escape_unicode, depth + 1, warned)) {
goto msg2json_end;
}
}
Expand All @@ -1099,6 +1151,21 @@ static int msgpack2json(char *buf, int *off, size_t left,

case MSGPACK_OBJECT_MAP:
loop = o->via.map.size;

if (loop != 0 && depth + 1 > FLB_PACK_JSON_MAX_DEPTH) {
/*
* The map is non-empty but its keys/values would exceed the
* maximum nesting depth. A JSON object key must always be a
* quoted string; truncating an individual key to a bare
* "null" (as the generic depth guard above would do) produces
* invalid JSON such as {null:...}. Render the whole map as
* null instead of opening it.
*/
msgpack2json_depth_warn(warned);
ret = try_to_write(buf, off, left, "null", 4);
break;
}

if (!try_to_write(buf, off, left, "{", 1)) {
goto msg2json_end;
}
Expand All @@ -1124,9 +1191,9 @@ static int msgpack2json(char *buf, int *off, size_t left,
}

if (
!msgpack2json(buf, off, left, &(p+i)->key, escape_unicode) ||
!msgpack2json(buf, off, left, &(p+i)->key, escape_unicode, depth + 1, warned) ||
!try_to_write(buf, off, left, ":", 1) ||
!msgpack2json(buf, off, left, &(p+i)->val, escape_unicode) ) {
!msgpack2json(buf, off, left, &(p+i)->val, escape_unicode, depth + 1, warned) ) {
goto msg2json_end;
}
packed++;
Expand Down Expand Up @@ -1158,12 +1225,13 @@ int flb_msgpack_to_json(char *json_str, size_t json_size,
{
int ret = -1;
int off = 0;
int warned = FLB_FALSE;

if (json_str == NULL || obj == NULL) {
return -1;
}

ret = msgpack2json(json_str, &off, json_size - 1, obj, escape_unicode);
ret = msgpack2json(json_str, &off, json_size - 1, obj, escape_unicode, 0, &warned);
json_str[off] = '\0';
return ret ? off: ret;
}
Expand Down
156 changes: 156 additions & 0 deletions tests/internal/pack.c
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,160 @@ void test_json_pack_bug5336()
}

/* Ensure empty arrays inside nested objects are handled */
/*
* Must mirror FLB_PACK_JSON_MAX_DEPTH in src/flb_pack.c (not exposed via a
* public header since it is an internal implementation detail of
* msgpack2json()).
*/
#define TEST_PACK_MAX_DEPTH 512

/*
* Regression test: a non-empty msgpack map whose key/value pairs would be
* evaluated exactly one level past FLB_PACK_JSON_MAX_DEPTH must still be
* rendered as valid JSON. The whole map is expected to collapse to a JSON
* null literal instead of emitting a bare, unquoted null in place of a map
* key (which would produce invalid JSON such as {null:"v"}).
*/
void test_json_pack_deep_map_boundary()
{
int i;
char *out;
char *p;
msgpack_object *chain;
msgpack_object_kv kv;
size_t out_len;
size_t expected_len;

/*
* Build TEST_PACK_MAX_DEPTH nested single-element arrays with a
* non-empty map ({"k":"v"}) as the innermost element, constructed
* directly in memory. chain[0] is evaluated at depth 0, chain[i] at
* depth i, so the map at chain[TEST_PACK_MAX_DEPTH] is evaluated at
* depth == TEST_PACK_MAX_DEPTH: its own guard passes, but its key/value
* pair would be one level past the limit.
*/
chain = flb_malloc(sizeof(msgpack_object) * (TEST_PACK_MAX_DEPTH + 1));
if (!TEST_CHECK(chain != NULL)) {
TEST_MSG("could not allocate test msgpack_object chain");
return;
}

for (i = 0; i < TEST_PACK_MAX_DEPTH; i++) {
chain[i].type = MSGPACK_OBJECT_ARRAY;
chain[i].via.array.size = 1;
chain[i].via.array.ptr = &chain[i + 1];
}

kv.key.type = MSGPACK_OBJECT_STR;
kv.key.via.str.size = 1;
kv.key.via.str.ptr = "k";
kv.val.type = MSGPACK_OBJECT_STR;
kv.val.via.str.size = 1;
kv.val.via.str.ptr = "v";

chain[TEST_PACK_MAX_DEPTH].type = MSGPACK_OBJECT_MAP;
chain[TEST_PACK_MAX_DEPTH].via.map.size = 1;
chain[TEST_PACK_MAX_DEPTH].via.map.ptr = &kv;

out = flb_msgpack_to_json_str(1024, &chain[0], FLB_FALSE);
flb_free(chain);

if (!TEST_CHECK(out != NULL)) {
TEST_MSG("flb_msgpack_to_json_str returned NULL");
return;
}

/* a map key must never be truncated to an unquoted null */
p = strstr(out, "null:");
if (!TEST_CHECK(p == NULL)) {
TEST_MSG("map key was rendered as an unquoted null: %s", out);
}

/*
* Exact shape check: TEST_PACK_MAX_DEPTH opening brackets, then the
* truncated map as a bare "null", then TEST_PACK_MAX_DEPTH closing
* brackets. Verify both the total length and that the null literal
* begins exactly at offset TEST_PACK_MAX_DEPTH, rather than accepting
* "null" anywhere in the output.
*/
{
out_len = strlen(out);
expected_len = (size_t) TEST_PACK_MAX_DEPTH * 2 + 4;

if (!TEST_CHECK(out_len == expected_len)) {
TEST_MSG("unexpected output length: expected=%zu got=%zu out=%s",
expected_len, out_len, out);
}

if (!TEST_CHECK(out_len > (size_t) TEST_PACK_MAX_DEPTH + 4 &&
strncmp(out + TEST_PACK_MAX_DEPTH, "null", 4) == 0)) {
TEST_MSG("expected a null literal at offset %d: %s",
TEST_PACK_MAX_DEPTH, out);
}
}

/* the original key/value content must not appear: it was truncated */
p = strstr(out, "\"k\":\"v\"");
if (!TEST_CHECK(p == NULL)) {
TEST_MSG("map content should have been truncated: %s", out);
}

flb_free(out);
}

/*
* Companion check: the same map shape placed comfortably below the depth
* limit must still serialize its real content (i.e. the pre-check added for
* the boundary case above must not fire early for valid, shallower input).
*/
void test_json_pack_deep_map_below_boundary()
{
int i;
int shallow_depth = TEST_PACK_MAX_DEPTH - 5;
char *out;
char *p;
msgpack_object *chain;
msgpack_object_kv kv;

chain = flb_malloc(sizeof(msgpack_object) * (shallow_depth + 1));
if (!TEST_CHECK(chain != NULL)) {
TEST_MSG("could not allocate test msgpack_object chain");
return;
}

for (i = 0; i < shallow_depth; i++) {
chain[i].type = MSGPACK_OBJECT_ARRAY;
chain[i].via.array.size = 1;
chain[i].via.array.ptr = &chain[i + 1];
}

kv.key.type = MSGPACK_OBJECT_STR;
kv.key.via.str.size = 1;
kv.key.via.str.ptr = "k";
kv.val.type = MSGPACK_OBJECT_STR;
kv.val.via.str.size = 1;
kv.val.via.str.ptr = "v";

chain[shallow_depth].type = MSGPACK_OBJECT_MAP;
chain[shallow_depth].via.map.size = 1;
chain[shallow_depth].via.map.ptr = &kv;

out = flb_msgpack_to_json_str(1024, &chain[0], FLB_FALSE);
flb_free(chain);

if (!TEST_CHECK(out != NULL)) {
TEST_MSG("flb_msgpack_to_json_str returned NULL");
return;
}

p = strstr(out, "\"k\":\"v\"");
if (!TEST_CHECK(p != NULL)) {
TEST_MSG("map content below the depth limit should be preserved: %s", out);
}

flb_free(out);
}

void test_json_pack_empty_array()
{
int ret;
Expand Down Expand Up @@ -1305,6 +1459,8 @@ TEST_LIST = {
{ "json_pack_nan" , test_json_pack_nan},
{ "json_pack_bug5336" , test_json_pack_bug5336},
{ "json_pack_empty_array", test_json_pack_empty_array},
{ "json_pack_deep_map_boundary", test_json_pack_deep_map_boundary},
{ "json_pack_deep_map_below_boundary", test_json_pack_deep_map_below_boundary},
{ "json_date_iso8601" , test_json_date_iso8601},
{ "json_date_double" , test_json_date_double},
{ "json_date_java_sql" , test_json_date_java_sql},
Expand Down