Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
cfb7698
AVRO-4293: [c] Validate available bytes before allocating for length-…
iemejia Jul 11, 2026
896a688
AVRO-4293: [c] Keep collection check enabled for deep schemas; assert…
iemejia Jul 11, 2026
b549013
AVRO-4293: [c] Saturate min-bytes arithmetic; null-check reader in tests
iemejia Jul 11, 2026
3601d0a
AVRO-4293: [c] Apply depth guard only to records so deep null returns 0
iemejia Jul 12, 2026
d780e77
AVRO-4293: [c] Cap zero-byte collection allocation; fix map NULL-deref
iemejia Jul 12, 2026
c603c96
AVRO-4293: [c] Reject INT64_MIN array/map block count
iemejia Jul 12, 2026
cdf28ee
AVRO-4293: [c] Bound bytes/string allocation size; portable test env
iemejia Jul 12, 2026
36b71c2
AVRO-4293: [c] Handle map key allocation failure; fix test comment
iemejia Jul 12, 2026
9b626d9
AVRO-4293: [c] Depth guard returns 0; clamp collection limits
iemejia Jul 12, 2026
e333337
AVRO-4293: [c] Bound read_string length to avoid int64_t overflow
iemejia Jul 12, 2026
889be2d
AVRO-4293: [c] Bounds-check enum symbol index on read
iemejia Jul 12, 2026
e06c25d
AVRO-4293: [c] Portable sign extension in the test's encode_long helper
iemejia Jul 13, 2026
7966926
AVRO-4293: [c] Read collection limits once per decode, not per block
iemejia Jul 13, 2026
db7a9e1
AVRO-4293: [c] Guard enum schema before the symbol-index range check
iemejia Jul 13, 2026
3a0b1ac
AVRO-4293: [c] Bound read_bytes length to avoid int64_t len+1 overflow
iemejia Jul 13, 2026
4ecd191
AVRO-4293: [c] Clarify try_decode return-value contract in test comment
iemejia Jul 13, 2026
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
5 changes: 5 additions & 0 deletions lang/c/src/avro_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extern "C" {

#include "avro/errors.h"
#include "avro/platform.h"
#include "avro/schema.h"

#ifdef HAVE_CONFIG_H
/* This is only true for now in the autotools build */
Expand Down Expand Up @@ -95,5 +96,9 @@ extern "C" {
#define nullstrcmp(s1, s2) \
(((s1) && (s2)) ? strcmp(s1, s2) : ((s1) || (s2)))

/* Collection allocation limits, shared between the read and skip paths. */
int64_t avro_min_bytes_per_element(avro_schema_t schema);
void avro_collection_limits(int64_t *zero_byte, int64_t *structural);

CLOSE_EXTERN
#endif
20 changes: 18 additions & 2 deletions lang/c/src/consume-binary.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,16 @@ read_array(avro_reader_t reader, const avro_encoding_t * enc,

check_prefix(rval, enc->read_long(reader, &block_count),
"Cannot read array block count: ");
if (block_count == INT64_MIN) {
avro_set_error("Invalid array block count");
return EINVAL;
}
check(rval, avro_consumer_call(consumer, array_start_block,
1, block_count, ud));

while (block_count != 0) {
if (block_count < 0) {
block_count = block_count * -1;
block_count = -block_count;
check_prefix(rval, enc->read_long(reader, &block_size),
"Cannot read array block size: ");
}
Expand All @@ -77,6 +81,10 @@ read_array(avro_reader_t reader, const avro_encoding_t * enc,

check_prefix(rval, enc->read_long(reader, &block_count),
"Cannot read array block count: ");
if (block_count == INT64_MIN) {
avro_set_error("Invalid array block count");
return EINVAL;
}
check(rval, avro_consumer_call(consumer, array_start_block,
0, block_count, ud));
}
Expand All @@ -96,12 +104,16 @@ read_map(avro_reader_t reader, const avro_encoding_t * enc,

check_prefix(rval, enc->read_long(reader, &block_count),
"Cannot read map block count: ");
if (block_count == INT64_MIN) {
avro_set_error("Invalid map block count");
return EINVAL;
}
check(rval, avro_consumer_call(consumer, map_start_block,
1, block_count, ud));

while (block_count != 0) {
if (block_count < 0) {
block_count = block_count * -1;
block_count = -block_count;
check_prefix(rval, enc->read_long(reader, &block_size),
"Cannot read map block size: ");
}
Expand Down Expand Up @@ -135,6 +147,10 @@ read_map(avro_reader_t reader, const avro_encoding_t * enc,

check_prefix(rval, enc->read_long(reader, &block_count),
"Cannot read map block count: ");
if (block_count == INT64_MIN) {
avro_set_error("Invalid map block count");
return EINVAL;
}
check(rval, avro_consumer_call(consumer, map_start_block,
0, block_count, ud));
}
Expand Down
42 changes: 40 additions & 2 deletions lang/c/src/datum_skip.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "avro_private.h"
#include "avro/errors.h"
#include <inttypes.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
Expand All @@ -30,17 +31,38 @@ static int skip_array(avro_reader_t reader, const avro_encoding_t * enc,
int64_t i;
int64_t block_count;
int64_t block_size;
int64_t skipped = 0;
int64_t zero_byte, structural, limit;

/* Zero-byte elements skip with no per-item input, so bound them tightly;
* other elements consume bytes (bounded by EOF) so only the structural
* cap applies. Skipping allocates nothing, but an unbounded per-item loop
* is a CPU exhaustion. */
avro_collection_limits(&zero_byte, &structural);
limit = (avro_min_bytes_per_element(writers_schema->items) > 0)
? structural : zero_byte;

check_prefix(rval, enc->read_long(reader, &block_count),
"Cannot read array block count: ");

while (block_count != 0) {
if (block_count < 0) {
block_count = block_count * -1;
if (block_count == INT64_MIN) {
avro_set_error("Invalid array block count");
return EINVAL;
}
block_count = -block_count;
check_prefix(rval, enc->read_long(reader, &block_size),
"Cannot read array block size: ");
}

if (block_count > limit || skipped > limit - block_count) {
avro_set_error("Cannot skip a collection of more than %"
PRId64 " elements", limit);
return EINVAL;
}
skipped += block_count;

for (i = 0; i < block_count; i++) {
check_prefix(rval, avro_skip_data(reader, writers_schema->items),
"Cannot skip array element: ");
Expand All @@ -57,16 +79,32 @@ static int skip_map(avro_reader_t reader, const avro_encoding_t * enc,
{
int rval;
int64_t i, block_count;
int64_t skipped = 0;
int64_t zero_byte, structural;

/* Map entries always carry a >= 1 byte key, so the structural cap applies
* (they are never zero-byte). */
avro_collection_limits(&zero_byte, &structural);

check_prefix(rval, enc->read_long(reader, &block_count),
"Cannot read map block count: ");
while (block_count != 0) {
int64_t block_size;
if (block_count < 0) {
block_count = block_count * -1;
if (block_count == INT64_MIN) {
avro_set_error("Invalid map block count");
return EINVAL;
}
block_count = -block_count;
check_prefix(rval, enc->read_long(reader, &block_size),
"Cannot read map block size: ");
}
if (block_count > structural || skipped > structural - block_count) {
avro_set_error("Cannot skip a collection of more than %"
PRId64 " elements", structural);
return EINVAL;
}
skipped += block_count;
for (i = 0; i < block_count; i++) {
check_prefix(rval, enc->skip_string(reader),
"Cannot skip map key: ");
Expand Down
10 changes: 10 additions & 0 deletions lang/c/src/encoding.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,15 @@ typedef struct avro_encoding_t avro_encoding_t;
extern const avro_encoding_t avro_binary_encoding; /* in
* encoding_binary
*/

/*
* Returns the number of bytes still available to read from a memory-backed
* reader, or -1 when the amount remaining is unknown (e.g. file readers).
* Defined in io.c. Used to reject a length prefix that exceeds the data
* actually available before allocating for it, to guard against an
* out-of-memory attack from a malicious or truncated input.
*/
int64_t avro_reader_bytes_available(avro_reader_t reader);

CLOSE_EXTERN
#endif
53 changes: 51 additions & 2 deletions lang/c/src/encoding_binary.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "encoding.h"
#include <stdlib.h>
#include <limits.h>
#include <stdint.h>
#include <errno.h>
#include <string.h>

Expand Down Expand Up @@ -125,13 +126,37 @@ static int64_t size_int(avro_writer_t writer, const int32_t i)
static int read_bytes(avro_reader_t reader, char **bytes, int64_t * len)
{
int rval;
int64_t available;
check_prefix(rval, read_long(reader, len),
"Cannot read bytes length: ");
if (*len < 0) {
avro_set_error("Invalid bytes length: %" PRId64, *len);
return EINVAL;
}
*bytes = (char *) avro_malloc(*len + 1);
/* Reject a declared length that exceeds the data actually available
* before allocating for it, to guard against an out-of-memory attack
* from a malicious or truncated input. Only enforced when the reader
* can report the amount remaining. */
available = avro_reader_bytes_available(reader);
if (available >= 0 && *len > available) {
avro_set_error("Bytes length %" PRId64
" exceeds %" PRId64 " bytes available",
*len, available);
return EINVAL;
}
/* Bound the length so the +1 (NUL terminator) cannot overflow either
* the size_t allocation size here or the int64_t len + 1 computed by the
* AVRO_BYTES caller in value-read.c. On 64-bit platforms SIZE_MAX >
* INT64_MAX, so the size_t check alone would let len == INT64_MAX through
* and make len + 1 overflow (undefined behavior); the INT64_MAX - 1 bound
* rejects it. */
if ((uint64_t) *len > (uint64_t) (SIZE_MAX - 1)
|| *len > INT64_MAX - 1) {
avro_set_error("Bytes length %" PRId64
" exceeds the maximum allocatable size", *len);
return EINVAL;
}
*bytes = (char *) avro_malloc((size_t) *len + 1);
if (!*bytes) {
avro_set_error("Cannot allocate buffer for bytes value");
return ENOMEM;
Expand Down Expand Up @@ -180,15 +205,39 @@ size_bytes(avro_writer_t writer, const char *bytes, const int64_t len)
static int read_string(avro_reader_t reader, char **s, int64_t *len)
{
int64_t str_len = 0;
int64_t available;
int rval;
check_prefix(rval, read_long(reader, &str_len),
"Cannot read string length: ");
if (str_len < 0) {
avro_set_error("Invalid string length: %" PRId64, str_len);
return EINVAL;
}
/* Reject a declared length that exceeds the data actually available
* before allocating for it, to guard against an out-of-memory attack
* from a malicious or truncated input. Only enforced when the reader
* can report the amount remaining. */
available = avro_reader_bytes_available(reader);
if (available >= 0 && str_len > available) {
avro_set_error("String length %" PRId64
" exceeds %" PRId64 " bytes available",
str_len, available);
return EINVAL;
}
/* Bound the length so the +1 (NUL terminator) cannot overflow either
* the size_t allocation size (undersizing the buffer, leading to an
* out-of-bounds read/write) or the int64_t returned in *len. On 64-bit
* platforms SIZE_MAX > INT64_MAX, so the size_t check alone would let
* str_len == INT64_MAX through and make str_len + 1 overflow (undefined
* behavior); the INT64_MAX - 1 bound rejects it. */
if ((uint64_t) str_len > (uint64_t) (SIZE_MAX - 1)
|| str_len > INT64_MAX - 1) {
avro_set_error("String length %" PRId64
" exceeds the maximum allocatable size", str_len);
return EINVAL;
}
*len = str_len + 1;
*s = (char *) avro_malloc(*len);
*s = (char *) avro_malloc((size_t) str_len + 1);
if (!*s) {
Comment on lines 239 to 241

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cdf28ee: read_string applies the same SIZE_MAX - 1 bound and does the +1 in size_t before allocating.

Comment on lines +237 to 241

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Added an explicit INT64_MAX - 1 bound alongside the SIZE_MAX - 1 check before computing *len = str_len + 1, since on 64-bit platforms SIZE_MAX > INT64_MAX and the size_t check alone would let str_len == INT64_MAX through and make the +1 overflow (UB). Fixed in e333337.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled — read_string bounds str_len with str_len > INT64_MAX - 1 (in addition to SIZE_MAX-1) before computing str_len + 1 (encoding_binary.c:233-234).

avro_set_error("Cannot allocate buffer for string value");
return ENOMEM;
Expand Down
14 changes: 14 additions & 0 deletions lang/c/src/io.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "avro/errors.h"
#include "avro/io.h"
#include "avro_private.h"
#include "encoding.h"
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
Expand Down Expand Up @@ -198,6 +199,19 @@ avro_read_memory(struct _avro_reader_memory_t *reader, void *buf, int64_t len)
return 0;
}

int64_t
avro_reader_bytes_available(avro_reader_t reader)
{
if (is_memory_io(reader)) {
struct _avro_reader_memory_t *mem_reader =
avro_reader_to_memory(reader);
return mem_reader->len - mem_reader->read;
}
/* File readers buffer internally and cannot cheaply report how many
* bytes remain in the underlying file, so the amount is unknown. */
return -1;
}

#define bytes_available(reader) (reader->end - reader->cur)
#define buffer_reset(reader) {reader->cur = reader->end = reader->buffer;}

Expand Down
23 changes: 19 additions & 4 deletions lang/c/src/map.c
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,31 @@ int avro_raw_map_get_or_create(avro_raw_map_t *map, const char *key,
el = (char *) raw_entry + sizeof(avro_raw_map_entry_t);
is_new = 0;
} else {
char *key_copy;
size_t key_size;
i = map->elements.element_count;
avro_raw_map_entry_t *raw_entry =
(avro_raw_map_entry_t *) avro_raw_array_append(&map->elements);
raw_entry->key = avro_strdup(key);
st_insert((st_table *) map->indices_by_key,
(st_data_t) raw_entry->key, (st_data_t) i);
if (!raw_entry) {
avro_str_free((char*)raw_entry->key);
/* avro_raw_array_append returns NULL on allocation
* failure; check before dereferencing it. */
return -ENOMEM;
}
/* Duplicate the key with a NULL-safe allocation: avro_strdup()
* memcpy()s into the buffer without checking it, so it would
* crash on OOM. Only index the entry once the copy succeeds; on
* failure, roll back the element we just appended so the map is
* not left with a half-initialized (NULL-key) entry. */
key_size = strlen(key) + 1;
key_copy = avro_str_alloc(key_size);
if (!key_copy) {
map->elements.element_count--;
return -ENOMEM;
}
memcpy(key_copy, key, key_size);
raw_entry->key = key_copy;
st_insert((st_table *) map->indices_by_key,
(st_data_t) raw_entry->key, (st_data_t) i);
Comment on lines +119 to +133

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, but this is a pre-existing limitation of the vendored st.c hash table (st_insert -> avro_new without a NULL check), which is used pervasively across the C SDK; changing st_insert's contract to return an allocation error is a broader change beyond this collection-allocation DoS fix. This PR removed the NULL-deref on avro_raw_array_append/avro_strdup in map.c; I've filed the st_insert OOM-safety as a separate follow-up rather than expanding the scope here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed as AVRO-4305 (https://issues.apache.org/jira/browse/AVRO-4305) to track making st_insert() OOM-safe separately.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — st_insert() allocating via avro_new() without a NULL check is a pre-existing OOM-safety gap in st.c, outside this PR's scope. Tracked as a dedicated follow-up: AVRO-4305 ("Make C st_insert OOM-safe").

el = ((char *) raw_entry) + sizeof(avro_raw_map_entry_t);
is_new = 1;
}
Expand Down
Loading
Loading