Skip to content

Commit f802f14

Browse files
committed
Make typed-array length checks overflow-safe (BEVE, CBOR)
BEVE typed-array bounds checks computed it + count * element_size > end (or end - it < count * element_size), where count comes from the wire. On the 32-bit targets the project builds (armv7, gcc-x86) that product overflows size_t and wraps small, so an oversized typed array slips past the check and a later read/copy/span runs off the buffer; the additive form is also out-of-bounds pointer arithmetic. Route all ten BEVE sites (read, skip, beve_to_json) through a new typed_array_out_of_bounds helper. On 32-bit it tests the fit as count > (remaining - padding) / element_size, a division that cannot overflow. On 64-bit, where int_from_compressed already caps counts at 2^48 (so the product is <= ~2^55 and cannot overflow), an if constexpr selects the existing single multiply + compare against end - it, leaving 64-bit codegen unchanged (verified identical assembly). CBOR map validation used count * 2 > end - it where count comes from an unclamped decode_arg, so the product overflows uint64_t on every platform for count >= 2^63; rewrite as count > (end - it) / 2.
1 parent 4d59c47 commit f802f14

5 files changed

Lines changed: 47 additions & 42 deletions

File tree

include/glaze/beve/beve_to_json.hpp

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,7 @@ namespace glz
302302
ctx.error = error_code::syntax_error;
303303
return;
304304
}
305-
if (uint64_t(end - it) < padding + byte_count_inner * n) [[unlikely]] {
306-
ctx.error = error_code::unexpected_end;
307-
return;
308-
}
305+
if (typed_array_out_of_bounds(ctx, it, end, n, byte_count_inner, padding)) return;
309306
it += padding;
310307

311308
// Now decode as a normal numeric typed array

include/glaze/beve/header.hpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@ namespace glz
2828
}
2929
}
3030

31+
// Bounds-checks a typed-array body of `count` elements of `element_size` bytes (element_size >= 1),
32+
// optionally preceded by `padding` alignment bytes, against the bytes remaining in [it, end).
33+
// Callers maintain it <= end (every advance is bounds-checked), so end - it is non-negative.
34+
// Returns true and sets unexpected_end when the body does not fit.
35+
//
36+
// On 64-bit, int_from_compressed caps wire counts at 2^48 and element_size <= 128, so
37+
// padding + count * element_size <= ~2^55 cannot overflow size_t: the fit is a single multiply
38+
// and compare against end - it (which, unlike it + offset, is never out-of-bounds pointer
39+
// arithmetic), so the 64-bit path pays nothing for the 32-bit safety. On 32-bit that product
40+
// overflows size_t -- wrapping small so an oversized array would slip past the check -- so there
41+
// the fit is tested as count > (remaining - padding) / element_size, a division that cannot
42+
// overflow.
43+
[[nodiscard]] GLZ_ALWAYS_INLINE bool typed_array_out_of_bounds(is_context auto& ctx, auto&& it, auto&& end,
44+
size_t count, size_t element_size,
45+
size_t padding = 0) noexcept
46+
{
47+
const uint64_t available = uint64_t(end - it);
48+
if constexpr (sizeof(size_t) > sizeof(uint32_t)) {
49+
if (available < padding + count * element_size) [[unlikely]] {
50+
ctx.error = error_code::unexpected_end;
51+
return true;
52+
}
53+
}
54+
else {
55+
if (available < padding || count > (available - padding) / element_size) [[unlikely]] {
56+
ctx.error = error_code::unexpected_end;
57+
return true;
58+
}
59+
}
60+
return false;
61+
}
62+
3163
// Byteswaps a numeric value in-place for little-endian wire format compatibility.
3264
// Call ONLY inside: if constexpr (std::endian::native == std::endian::big) blocks.
3365
// On little-endian systems, this function is never instantiated (zero overhead).

include/glaze/beve/read.hpp

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -924,10 +924,7 @@ namespace glz
924924
return;
925925
}
926926

927-
if ((it + padding + n * sizeof(V)) > end) [[unlikely]] {
928-
ctx.error = error_code::unexpected_end;
929-
return;
930-
}
927+
if (typed_array_out_of_bounds(ctx, it, end, n, sizeof(V), padding)) return;
931928
it += padding;
932929

933930
value = std::span<T, Extent>{reinterpret_cast<const V*>(&(*it)), n};
@@ -1060,10 +1057,7 @@ namespace glz
10601057
n = value.size();
10611058
}
10621059

1063-
if ((it + n * element_size) > end) [[unlikely]] {
1064-
ctx.error = error_code::unexpected_end;
1065-
return 0;
1066-
}
1060+
if (typed_array_out_of_bounds(ctx, it, end, n, element_size)) return 0;
10671061
if (!validate_and_resize(n)) {
10681062
return 0;
10691063
}
@@ -1114,10 +1108,7 @@ namespace glz
11141108
ctx.error = error_code::syntax_error;
11151109
return;
11161110
}
1117-
if ((it + padding + count * elem_byte_count) > end) [[unlikely]] {
1118-
ctx.error = error_code::unexpected_end;
1119-
return;
1120-
}
1111+
if (typed_array_out_of_bounds(ctx, it, end, count, elem_byte_count, padding)) return;
11211112
it += padding;
11221113

11231114
if (!validate_and_resize(count)) {
@@ -1156,10 +1147,7 @@ namespace glz
11561147
return;
11571148
}
11581149

1159-
if ((it + padding + count * sizeof(V)) > end) [[unlikely]] {
1160-
ctx.error = error_code::unexpected_end;
1161-
return;
1162-
}
1150+
if (typed_array_out_of_bounds(ctx, it, end, count, sizeof(V), padding)) return;
11631151
it += padding;
11641152

11651153
if (!validate_and_resize(count)) {
@@ -1228,10 +1216,7 @@ namespace glz
12281216
}
12291217
else if constexpr (std::endian::native == std::endian::big && sizeof(V) > 1) {
12301218
// On big endian, read and swap each element
1231-
if ((it + n * sizeof(V)) > end) [[unlikely]] {
1232-
ctx.error = error_code::unexpected_end;
1233-
return;
1234-
}
1219+
if (typed_array_out_of_bounds(ctx, it, end, n, sizeof(V))) return;
12351220
for (size_t i = 0; i < n; ++i) {
12361221
std::memcpy(&value[i], it, sizeof(V));
12371222
byteswap_le(value[i]);
@@ -1240,10 +1225,7 @@ namespace glz
12401225
}
12411226
else {
12421227
// Little endian or single-byte: bulk memcpy
1243-
if ((it + n * sizeof(V)) > end) [[unlikely]] {
1244-
ctx.error = error_code::unexpected_end;
1245-
return;
1246-
}
1228+
if (typed_array_out_of_bounds(ctx, it, end, n, sizeof(V))) return;
12471229
std::memcpy(value.data(), it, n * sizeof(V));
12481230
it += n * sizeof(V);
12491231
}
@@ -1371,10 +1353,7 @@ namespace glz
13711353
n = value.size();
13721354
}
13731355

1374-
if (uint64_t(end - it) < n * sizeof(V)) [[unlikely]] {
1375-
ctx.error = error_code::unexpected_end;
1376-
return;
1377-
}
1356+
if (typed_array_out_of_bounds(ctx, it, end, n, sizeof(V))) return;
13781357
if constexpr (check_max_array_size(Opts) > 0) {
13791358
if (n > check_max_array_size(Opts)) [[unlikely]] {
13801359
ctx.error = error_code::invalid_length;

include/glaze/beve/skip.hpp

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,7 @@ namespace glz
133133
ctx.error = error_code::syntax_error;
134134
return;
135135
}
136-
if (uint64_t(end - it) < padding + elem_byte_count * n) [[unlikely]] {
137-
ctx.error = error_code::unexpected_end;
138-
return;
139-
}
136+
if (typed_array_out_of_bounds(ctx, it, end, n, elem_byte_count, padding)) return;
140137
it += padding + elem_byte_count * n;
141138
return;
142139
}
@@ -152,10 +149,7 @@ namespace glz
152149
return;
153150
}
154151
const uint8_t byte_count = byte_count_lookup[tag >> 5];
155-
if (uint64_t(end - it) < byte_count * n) [[unlikely]] {
156-
ctx.error = error_code::unexpected_end;
157-
return;
158-
}
152+
if (typed_array_out_of_bounds(ctx, it, end, n, byte_count)) return;
159153
it += byte_count * n;
160154
break;
161155
}

include/glaze/cbor/read.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,8 +1200,11 @@ namespace glz
12001200
if (bool(ctx.error)) [[unlikely]]
12011201
return;
12021202

1203-
// Validate count against remaining buffer size (minimum 2 bytes per key-value pair)
1204-
if (count * 2 > static_cast<uint64_t>(end - it)) [[unlikely]] {
1203+
// Validate count against remaining buffer size (minimum 2 bytes per key-value pair).
1204+
// Tested as a division so count * 2 cannot overflow uint64_t for an attacker-supplied
1205+
// count (decode_arg returns an unclamped 64-bit value); this is equivalent to
1206+
// count * 2 > end - it.
1207+
if (count > static_cast<uint64_t>(end - it) / 2) [[unlikely]] {
12051208
ctx.error = error_code::unexpected_end;
12061209
return;
12071210
}

0 commit comments

Comments
 (0)