Skip to content

Commit d5de262

Browse files
committed
Bound eetf number and container-header reads against end of input
term_to_json_value dispatches on the tag byte (guaranteed present by the function-top invalid_end), but then hands the raw pointer to ei_decode_long/ ei_decode_double and decode_list/tuple/map_header, which read a fixed payload past the tag before any bounds check -- a read past end when the payload is truncated (verified with a guard page: SIGSEGV on a tag-at-end buffer for each numeric and container tag). Since the tag is known at the call site, gate each decode with check_invalid_offset for that tag's fixed width: 2/5 bytes for SMALL_INTEGER/INTEGER, 9/32 for NEW_FLOAT/legacy FLOAT, 5 for LIST/MAP/ LARGE_TUPLE arity, 2 for SMALL_TUPLE. Also stop the write_sequence loop on error so a header declaring up to 2^32 elements with a truncated body errors out at once instead of spinning the declared arity. Add regression tests for each truncated scalar, truncated container header, and the oversized-arity case.
1 parent 5668dd5 commit d5de262

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

include/glaze/eetf/eetf_to_json.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ namespace glz
8585
dump('[', out, ix);
8686
while (arity--) {
8787
term_to_json_value<Opts>(ctx, it, end, out, ix, recursive_depth);
88+
// Stop on error instead of spinning the remaining (attacker-declared, up to 2^32)
89+
// iterations once the input is exhausted.
90+
if (bool(ctx.error)) [[unlikely]] {
91+
return;
92+
}
8893
if (arity) {
8994
dump(',', out, ix);
9095
if constexpr (Opts.prettify) {
@@ -99,6 +104,9 @@ namespace glz
99104
switch (type) {
100105
case ERL_SMALL_INTEGER_EXT:
101106
case ERL_INTEGER_EXT: {
107+
// ei_decode_long reads the tag plus a fixed payload (1 byte for SMALL_INTEGER_EXT,
108+
// 4 for INTEGER_EXT) off the raw pointer; bound it before the decode.
109+
if (check_invalid_offset(ctx, it, end, type == ERL_SMALL_INTEGER_EXT ? 2u : 5u)) return;
102110
term_to_json_number<Opts>(std::int64_t{}, ctx, it, end, out, ix);
103111
if (bool(ctx.error)) return;
104112
break;
@@ -112,6 +120,9 @@ namespace glz
112120

113121
case ERL_FLOAT_EXT:
114122
case NEW_FLOAT_EXT: {
123+
// ei_decode_double reads the tag plus a fixed payload (8 bytes for NEW_FLOAT_EXT, the
124+
// 31-byte ASCII form for the legacy FLOAT_EXT) off the raw pointer; bound it first.
125+
if (check_invalid_offset(ctx, it, end, type == NEW_FLOAT_EXT ? 9u : 32u)) return;
115126
term_to_json_number<Opts>(double{}, ctx, it, end, out, ix);
116127
if (bool(ctx.error)) return;
117128
break;
@@ -151,6 +162,8 @@ namespace glz
151162
}
152163

153164
case ERL_LIST_EXT: {
165+
// decode_list_header reads the tag plus a 4-byte arity off the raw pointer; bound it.
166+
if (check_invalid_offset(ctx, it, end, 5u)) return;
154167
[[maybe_unused]] auto [arity, idx] = decode_list_header(ctx, it);
155168
if (bool(ctx.error)) {
156169
return;
@@ -183,6 +196,9 @@ namespace glz
183196

184197
case ERL_SMALL_TUPLE_EXT:
185198
case ERL_LARGE_TUPLE_EXT: {
199+
// decode_tuple_header reads the tag plus the arity (1 byte for SMALL_TUPLE_EXT, 4 for
200+
// LARGE_TUPLE_EXT) off the raw pointer; bound it.
201+
if (check_invalid_offset(ctx, it, end, type == ERL_SMALL_TUPLE_EXT ? 2u : 5u)) return;
186202
[[maybe_unused]] auto [arity, idx] = decode_tuple_header(ctx, it);
187203
if (bool(ctx.error)) {
188204
return;
@@ -192,6 +208,8 @@ namespace glz
192208
}
193209

194210
case ERL_MAP_EXT: {
211+
// decode_map_header reads the tag plus a 4-byte arity off the raw pointer; bound it.
212+
if (check_invalid_offset(ctx, it, end, 5u)) return;
195213
[[maybe_unused]] auto [arity, idx] = decode_map_header(ctx, it);
196214
if (bool(ctx.error)) {
197215
return;

tests/eetf_test/eetf_test.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,67 @@ suite eetf_to_json_tests = [] {
523523
expect(ec.ec == glz::error_code::array_element_not_found);
524524
};
525525

526+
"eetf_to_json truncated scalar"_test = [] {
527+
auto expect_unexpected_end = [](const auto& buffer) {
528+
std::string json{};
529+
const auto ec = glz::eetf_to_json(buffer, json);
530+
expect(ec.ec == glz::error_code::unexpected_end);
531+
};
532+
533+
// Numeric tags whose fixed payload is cut off after the tag: ei_decode_long/double must not
534+
// read it off the raw pointer before the bounds check.
535+
const std::array<std::uint8_t, 2> small_int{uint8_t(glz::eetf_magic_version), uint8_t(ERL_SMALL_INTEGER_EXT)};
536+
const std::array<std::uint8_t, 2> integer{uint8_t(glz::eetf_magic_version), uint8_t(ERL_INTEGER_EXT)};
537+
const std::array<std::uint8_t, 4> integer_partial{uint8_t(glz::eetf_magic_version), uint8_t(ERL_INTEGER_EXT), 0,
538+
0};
539+
const std::array<std::uint8_t, 2> new_float{uint8_t(glz::eetf_magic_version), uint8_t(NEW_FLOAT_EXT)};
540+
const std::array<std::uint8_t, 2> old_float{uint8_t(glz::eetf_magic_version), uint8_t(ERL_FLOAT_EXT)};
541+
542+
expect_unexpected_end(small_int);
543+
expect_unexpected_end(integer);
544+
expect_unexpected_end(integer_partial);
545+
expect_unexpected_end(new_float);
546+
expect_unexpected_end(old_float);
547+
};
548+
549+
"eetf_to_json truncated container header"_test = [] {
550+
auto expect_unexpected_end = [](const auto& buffer) {
551+
std::string json{};
552+
const auto ec = glz::eetf_to_json(buffer, json);
553+
expect(ec.ec == glz::error_code::unexpected_end);
554+
};
555+
556+
// Container tags whose 1- or 4-byte arity is cut off after the tag: decode_*_header must not
557+
// read it off the raw pointer before the bounds check.
558+
const std::array<std::uint8_t, 2> list{uint8_t(glz::eetf_magic_version), uint8_t(ERL_LIST_EXT)};
559+
const std::array<std::uint8_t, 4> list_partial{uint8_t(glz::eetf_magic_version), uint8_t(ERL_LIST_EXT), 0, 0};
560+
const std::array<std::uint8_t, 2> map{uint8_t(glz::eetf_magic_version), uint8_t(ERL_MAP_EXT)};
561+
const std::array<std::uint8_t, 2> small_tuple{uint8_t(glz::eetf_magic_version), uint8_t(ERL_SMALL_TUPLE_EXT)};
562+
const std::array<std::uint8_t, 2> large_tuple{uint8_t(glz::eetf_magic_version), uint8_t(ERL_LARGE_TUPLE_EXT)};
563+
564+
expect_unexpected_end(list);
565+
expect_unexpected_end(list_partial);
566+
expect_unexpected_end(map);
567+
expect_unexpected_end(small_tuple);
568+
expect_unexpected_end(large_tuple);
569+
};
570+
571+
"eetf_to_json oversized list arity terminates"_test = [] {
572+
// List header declares 2^32-1 elements but only one is present. The sequence loop must stop
573+
// on the first exhausted read rather than spinning through the declared arity.
574+
const std::array<std::uint8_t, 8> buffer{uint8_t(glz::eetf_magic_version),
575+
uint8_t(ERL_LIST_EXT),
576+
0xFF,
577+
0xFF,
578+
0xFF,
579+
0xFF,
580+
uint8_t(ERL_SMALL_INTEGER_EXT),
581+
1};
582+
std::string json{};
583+
const auto ec = glz::eetf_to_json(buffer, json);
584+
expect(ec.ec == glz::error_code::unexpected_end);
585+
};
586+
526587
"eetf_to_json no header"_test = [] {
527588
constexpr int items = 3;
528589
std::vector<simple> v;

0 commit comments

Comments
 (0)