diff --git a/lang/c/src/avro_private.h b/lang/c/src/avro_private.h index f97ef6b5a71..0c71c6962c1 100644 --- a/lang/c/src/avro_private.h +++ b/lang/c/src/avro_private.h @@ -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 */ @@ -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 diff --git a/lang/c/src/consume-binary.c b/lang/c/src/consume-binary.c index 5e1db20684f..058202e6f52 100644 --- a/lang/c/src/consume-binary.c +++ b/lang/c/src/consume-binary.c @@ -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: "); } @@ -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)); } @@ -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: "); } @@ -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)); } diff --git a/lang/c/src/datum_skip.c b/lang/c/src/datum_skip.c index e0ce561642e..3c9f687fc37 100644 --- a/lang/c/src/datum_skip.c +++ b/lang/c/src/datum_skip.c @@ -17,6 +17,7 @@ #include "avro_private.h" #include "avro/errors.h" +#include #include #include #include @@ -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: "); @@ -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: "); diff --git a/lang/c/src/encoding.h b/lang/c/src/encoding.h index b90c91dd5c8..8646f0cb9b8 100644 --- a/lang/c/src/encoding.h +++ b/lang/c/src/encoding.h @@ -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 diff --git a/lang/c/src/encoding_binary.c b/lang/c/src/encoding_binary.c index 459d5fb3db3..e1abacaea04 100644 --- a/lang/c/src/encoding_binary.c +++ b/lang/c/src/encoding_binary.c @@ -21,6 +21,7 @@ #include "encoding.h" #include #include +#include #include #include @@ -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; @@ -180,6 +205,7 @@ 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: "); @@ -187,8 +213,31 @@ static int read_string(avro_reader_t reader, char **s, int64_t *len) 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) { avro_set_error("Cannot allocate buffer for string value"); return ENOMEM; diff --git a/lang/c/src/io.c b/lang/c/src/io.c index c1e2f5dc9b1..3d72b299311 100644 --- a/lang/c/src/io.c +++ b/lang/c/src/io.c @@ -20,6 +20,7 @@ #include "avro/errors.h" #include "avro/io.h" #include "avro_private.h" +#include "encoding.h" #include #include #include @@ -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;} diff --git a/lang/c/src/map.c b/lang/c/src/map.c index c85ffbd8493..062b30a1fa6 100644 --- a/lang/c/src/map.c +++ b/lang/c/src/map.c @@ -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); el = ((char *) raw_entry) + sizeof(avro_raw_map_entry_t); is_new = 1; } diff --git a/lang/c/src/value-read.c b/lang/c/src/value-read.c index b6b6e79fadd..d732bc85ca4 100644 --- a/lang/c/src/value-read.c +++ b/lang/c/src/value-read.c @@ -18,11 +18,13 @@ #include #include #include +#include #include "avro/allocation.h" #include "avro/basics.h" #include "avro/data.h" #include "avro/io.h" +#include "avro/schema.h" #include "avro/value.h" #include "avro_private.h" #include "encoding.h" @@ -38,6 +40,169 @@ static int read_value(avro_reader_t reader, avro_value_t *dest); +/* + * Minimum number of bytes a single value of the given schema can occupy on the + * wire. Used to reject an array/map block count that could not be backed by the + * bytes remaining. A type that can encode to zero bytes (null) returns 0, which + * disables the collection check for it (so an array of nulls is not falsely + * rejected). A recursion guard breaks self-referencing schemas. + */ +static int64_t +min_bytes_per_element(avro_schema_t schema, int depth) +{ + if (schema == NULL) { + return 0; + } + switch (avro_typeof(schema)) { + case AVRO_NULL: + return 0; + case AVRO_FLOAT: + return 4; + case AVRO_DOUBLE: + return 8; + case AVRO_FIXED: + return avro_schema_fixed_size(schema); + case AVRO_RECORD: { + size_t n = avro_schema_record_size(schema); + int64_t total = 0; + size_t i; + if (depth > 64) { + /* Safety net for a pathologically deep (or cyclic) + * schema. Return 0 (a valid conservative lower bound) + * rather than over-estimating: if the true minimum + * cannot be computed, treat the element as possibly + * zero-byte so the tighter zero-byte collection cap is + * applied instead of the much larger structural cap. */ + return 0; + } + for (i = 0; i < n; i++) { + avro_schema_t field = + avro_schema_record_field_get_by_index(schema, i); + int64_t field_min = min_bytes_per_element(field, depth + 1); + /* Saturate rather than overflow: a wrapped (negative) + * total would disable the collection check. */ + if (field_min > INT64_MAX - total) { + return INT64_MAX; + } + total += field_min; + } + return total; + } + case AVRO_LINK: + return min_bytes_per_element(avro_schema_link_target(schema), + depth + 1); + default: + /* boolean, int, long, bytes, string, enum, union, array, map: + * all encode to at least one byte. */ + return 1; + } +} + +/* Non-static wrapper so the skip path (datum_skip.c) can compute the minimum + * on-wire size of a collection's element schema. */ +int64_t +avro_min_bytes_per_element(avro_schema_t schema) +{ + return min_bytes_per_element(schema, 0); +} + +/* + * Maximum number of zero-byte-encoded collection elements (e.g. an array of + * nulls) to allocate from a single decode. Such elements consume no input, so + * the bytes-remaining check cannot bound their count; without a cap a tiny + * payload can declare a huge block count and exhaust memory. Overridable via + * the AVRO_MAX_COLLECTION_ITEMS environment variable. + */ +#define AVRO_DEFAULT_MAX_COLLECTION_ITEMS ((int64_t) 10000000) + +/* + * Structural cap on the number of elements in any array or map (an overflow / + * defense-in-depth guard), matching the historical Integer.MAX_VALUE - 8 limit. + * Non-zero-byte elements are also bounded by the bytes remaining. + */ +#define AVRO_DEFAULT_MAX_COLLECTION_STRUCTURAL ((int64_t) 2147483639) + +/* + * Populate the zero-byte and structural collection limits. The + * AVRO_MAX_COLLECTION_ITEMS environment variable, when a non-negative integer, + * caps both; otherwise the tighter zero-byte default and the structural default + * are used. Shared with the skip path (see avro_collection_limits). + */ +void +avro_collection_limits(int64_t *zero_byte, int64_t *structural) +{ + const char *env = getenv("AVRO_MAX_COLLECTION_ITEMS"); + if (env != NULL && *env != '\0') { + char *end; + long long value = strtoll(env, &end, 10); + if (*end == '\0' && value >= 0) { + /* Clamp to INT64_MAX and to SIZE_MAX: the block count is + * later cast to size_t in the read loop, so a limit above + * SIZE_MAX would permit a count that truncates on cast + * (notably on 32-bit) and weaken the bound. */ + uint64_t v = (uint64_t) value; + if (v > (uint64_t) INT64_MAX) { + v = (uint64_t) INT64_MAX; + } + if (v > (uint64_t) SIZE_MAX) { + v = (uint64_t) SIZE_MAX; + } + *zero_byte = *structural = (int64_t) v; + return; + } + } + *zero_byte = AVRO_DEFAULT_MAX_COLLECTION_ITEMS; + *structural = AVRO_DEFAULT_MAX_COLLECTION_STRUCTURAL; +} + +/* + * Reject a collection (array or map) block that could not be backed by the + * input, before appending. + * + * For elements with a positive minimum on-wire size, the declared count is + * checked against the bytes remaining. For zero-byte elements (e.g. an array of + * nulls), which consume no input and so cannot be bounded by the bytes + * remaining, the cumulative count is checked against a configurable limit. + */ +static int +ensure_collection_available(avro_reader_t reader, int64_t existing, + int64_t count, int64_t min_bytes, + int64_t zero_byte, int64_t structural) +{ + int64_t available; + if (count <= 0) { + return 0; + } + if (min_bytes > 0) { + available = avro_reader_bytes_available(reader); + if (available >= 0 && count > available / min_bytes) { + avro_set_error("Collection claims %" PRId64 + " elements with at least %" PRId64 + " bytes each, but only %" PRId64 " bytes available", + count, min_bytes, available); + return EINVAL; + } + /* Structural / overflow guard, also covering readers that cannot + * report the bytes remaining. Compare without adding so + * existing + count cannot overflow. */ + if (count > structural || existing > structural - count) { + avro_set_error("Cannot read a collection of more than %" PRId64 + " elements; raise AVRO_MAX_COLLECTION_ITEMS " + "if this is legitimate", structural); + return EINVAL; + } + return 0; + } + /* Compare without adding, so existing + count cannot overflow. */ + if (count > zero_byte || existing > zero_byte - count) { + avro_set_error("Cannot read a collection of more than %" PRId64 + " zero-byte elements; raise AVRO_MAX_COLLECTION_ITEMS " + "if this is legitimate", zero_byte); + return EINVAL; + } + return 0; +} + static int read_array_value(avro_reader_t reader, avro_value_t *dest) { @@ -46,6 +211,16 @@ read_array_value(avro_reader_t reader, avro_value_t *dest) size_t index = 0; /* index within the entire array */ int64_t block_count; int64_t block_size; + int64_t min_bytes; + int64_t zero_byte, structural; + avro_schema_t array_schema = avro_value_get_schema(dest); + + min_bytes = min_bytes_per_element( + array_schema ? avro_schema_array_items(array_schema) : NULL, 0); + /* Read the limits once per decode rather than per block, so a + * many-block array does not re-parse AVRO_MAX_COLLECTION_ITEMS each + * time (and the effective limit is stable across the decode). */ + avro_collection_limits(&zero_byte, &structural); check_prefix(rval, avro_binary_encoding. read_long(reader, &block_count), @@ -53,12 +228,20 @@ read_array_value(avro_reader_t reader, avro_value_t *dest) while (block_count != 0) { if (block_count < 0) { - block_count = block_count * -1; + /* Reject INT64_MIN: its negation is not + * representable in int64_t (CWE-190). */ + if (block_count == INT64_MIN) { + avro_set_error("Invalid array block count"); + return EINVAL; + } + block_count = -block_count; check_prefix(rval, avro_binary_encoding. read_long(reader, &block_size), "Cannot read array block size: "); } + check(rval, ensure_collection_available(reader, (int64_t) index, block_count, min_bytes, zero_byte, structural)); + for (i = 0; i < (size_t) block_count; i++, index++) { avro_value_t child; @@ -83,18 +266,39 @@ read_map_value(avro_reader_t reader, avro_value_t *dest) size_t index = 0; /* index within the entire array */ int64_t block_count; int64_t block_size; + int64_t min_bytes; + int64_t zero_byte, structural; + avro_schema_t map_schema = avro_value_get_schema(dest); + + /* Map keys are strings (>= 1 byte length prefix) plus the value. + * Saturate the +1 so a maxed-out value minimum cannot wrap. */ + min_bytes = min_bytes_per_element( + map_schema ? avro_schema_map_values(map_schema) : NULL, 0); + if (min_bytes < INT64_MAX) { + min_bytes += 1; + } + /* Read the limits once per decode (see read_array_value). */ + avro_collection_limits(&zero_byte, &structural); check_prefix(rval, avro_binary_encoding.read_long(reader, &block_count), "Cannot read map block count: "); while (block_count != 0) { if (block_count < 0) { - block_count = block_count * -1; + /* Reject INT64_MIN: its negation is not + * representable in int64_t (CWE-190). */ + if (block_count == INT64_MIN) { + avro_set_error("Invalid map block count"); + return EINVAL; + } + block_count = -block_count; check_prefix(rval, avro_binary_encoding. read_long(reader, &block_size), "Cannot read map block size: "); } + check(rval, ensure_collection_available(reader, (int64_t) index, block_count, min_bytes, zero_byte, structural)); + for (i = 0; i < (size_t) block_count; i++, index++) { char *key; int64_t key_size; @@ -330,9 +534,28 @@ read_value(avro_reader_t reader, avro_value_t *dest) case AVRO_ENUM: { int64_t val; + int symbol_count; + avro_schema_t enum_schema; check_prefix(rval, avro_binary_encoding. read_long(reader, &val), "Cannot read enum value: "); + enum_schema = avro_value_get_schema(dest); + /* A custom value interface could return a NULL or + * non-enum schema, for which + * avro_schema_enum_number_of_symbols returns a negative + * error code that would make the range check below accept + * out-of-range values. Reject that up front. */ + if (!is_avro_enum(enum_schema)) { + avro_set_error("Not an enum schema for enum value"); + return EINVAL; + } + symbol_count = + avro_schema_enum_number_of_symbols(enum_schema); + if (val < 0 || val >= symbol_count) { + avro_set_error("Invalid enum value: %" PRId64, + val); + return EINVAL; + } return avro_value_set_enum(dest, val); } diff --git a/lang/c/tests/CMakeLists.txt b/lang/c/tests/CMakeLists.txt index 6b4164fa740..50d76659b23 100644 --- a/lang/c/tests/CMakeLists.txt +++ b/lang/c/tests/CMakeLists.txt @@ -88,3 +88,5 @@ add_avro_test_checkmem(test_avro_1691) add_avro_test_checkmem(test_avro_1906) add_avro_test_checkmem(test_avro_1904) add_avro_test_checkmem(test_avro_4246) +add_avro_test_checkmem(test_avro_4293) +add_avro_test_checkmem(test_avro_4275) diff --git a/lang/c/tests/test_avro_4275.c b/lang/c/tests/test_avro_4275.c new file mode 100644 index 00000000000..b4d4bf0b78e --- /dev/null +++ b/lang/c/tests/test_avro_4275.c @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/** + * Regression test for INT64_MIN negation overflow in read_array_value() + * and read_map_value() (CWE-190). + * + * The Avro binary format encodes block counts as zigzag-encoded varints. + * A negative block count means the absolute value is the actual count, + * preceded by a byte-size field. When block_count == INT64_MIN, the + * negation overflows (undefined behavior in C). This test verifies that + * the decoder rejects such malformed input gracefully. + */ + +#include +#include +#include +#include +#include +#include + +/* + * Zigzag encoding of INT64_MIN is 0xFFFFFFFFFFFFFFFF, which encodes as + * the 10-byte varint: FF FF FF FF FF FF FF FF FF 01 + */ +static const char int64min_block_count[] = { + (char)0xFF, (char)0xFF, (char)0xFF, (char)0xFF, (char)0xFF, + (char)0xFF, (char)0xFF, (char)0xFF, (char)0xFF, 0x01 +}; + +/* + * A valid array with negative block count: [-3] means 3 items follow, + * preceded by the block byte-size. + * + * Layout: + * 05 = varint 5 = zigzag(-3) => block_count = -3 + * 06 = varint 6 = zigzag(3) => block_size = 3 bytes + * 14 28 3C = zigzag ints: 10, 20, 30 + * 00 = terminator (block_count = 0) + */ +static const char valid_neg_block_array[] = { + '\x05', /* block_count = -3 */ + '\x06', /* block_size = 3 */ + '\x14', '\x28', '\x3C', /* ints: 10, 20, 30 */ + '\x00' /* terminator */ +}; + +/* + * A valid map with negative block count: [-2] means 2 entries follow. + * + * Layout: + * 03 = varint 3 = zigzag(-2) => block_count = -2 + * 0C = varint 12 = zigzag(6) => block_size = 6 bytes + * 02 61 14 = key "a" (len=1, 'a'), value int 10 + * 02 62 28 = key "b" (len=1, 'b'), value int 20 + * 00 = terminator + */ +static const char valid_neg_block_map[] = { + '\x03', /* block_count = -2 */ + '\x0C', /* block_size = 6 */ + '\x02', '\x61', '\x14', /* key "a", value 10 */ + '\x02', '\x62', '\x28', /* key "b", value 20 */ + '\x00' /* terminator */ +}; + +static int test_array_int64min(void) +{ + int rc; + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + avro_value_t value; + avro_reader_t reader = NULL; + + rc = avro_schema_from_json_literal( + "{\"type\":\"array\",\"items\":\"int\"}", &schema); + if (rc != 0) { + fprintf(stderr, "Failed to parse array schema: %s\n", + avro_strerror()); + return 1; + } + + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + fprintf(stderr, "Failed to create array iface\n"); + avro_schema_decref(schema); + return 1; + } + + rc = avro_generic_value_new(iface, &value); + if (rc != 0) { + fprintf(stderr, "Failed to create array value: %s\n", + avro_strerror()); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + reader = avro_reader_memory(int64min_block_count, + sizeof(int64min_block_count)); + if (reader == NULL) { + fprintf(stderr, "Failed to create reader\n"); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + /* This MUST fail with EINVAL rather than looping unboundedly or + * triggering undefined behavior from negating INT64_MIN. */ + rc = avro_value_read(reader, &value); + if (rc != EINVAL) { + fprintf(stderr, + "FAIL: INT64_MIN array block count: expected EINVAL, " + "got rc=%d (%s)\n", rc, avro_strerror()); + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + printf("PASS: INT64_MIN array block count rejected with EINVAL: %s\n", + avro_strerror()); + + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 0; +} + +static int test_map_int64min(void) +{ + int rc; + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + avro_value_t value; + avro_reader_t reader = NULL; + + rc = avro_schema_from_json_literal( + "{\"type\":\"map\",\"values\":\"int\"}", &schema); + if (rc != 0) { + fprintf(stderr, "Failed to parse map schema: %s\n", + avro_strerror()); + return 1; + } + + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + fprintf(stderr, "Failed to create map iface\n"); + avro_schema_decref(schema); + return 1; + } + + rc = avro_generic_value_new(iface, &value); + if (rc != 0) { + fprintf(stderr, "Failed to create map value: %s\n", + avro_strerror()); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + reader = avro_reader_memory(int64min_block_count, + sizeof(int64min_block_count)); + if (reader == NULL) { + fprintf(stderr, "Failed to create reader\n"); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + /* This MUST fail with EINVAL rather than looping unboundedly or + * triggering undefined behavior from negating INT64_MIN. */ + rc = avro_value_read(reader, &value); + if (rc != EINVAL) { + fprintf(stderr, + "FAIL: INT64_MIN map block count: expected EINVAL, " + "got rc=%d (%s)\n", rc, avro_strerror()); + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + printf("PASS: INT64_MIN map block count rejected with EINVAL: %s\n", + avro_strerror()); + + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 0; +} + +static int test_valid_neg_block_array(void) +{ + int rc; + size_t count; + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + avro_value_t value; + avro_reader_t reader = NULL; + + rc = avro_schema_from_json_literal( + "{\"type\":\"array\",\"items\":\"int\"}", &schema); + if (rc != 0) { + fprintf(stderr, "Failed to parse array schema: %s\n", + avro_strerror()); + return 1; + } + + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + fprintf(stderr, "Failed to create array iface\n"); + avro_schema_decref(schema); + return 1; + } + + rc = avro_generic_value_new(iface, &value); + if (rc != 0) { + fprintf(stderr, "Failed to create array value: %s\n", + avro_strerror()); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + reader = avro_reader_memory(valid_neg_block_array, + sizeof(valid_neg_block_array)); + if (reader == NULL) { + fprintf(stderr, "Failed to create reader\n"); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + rc = avro_value_read(reader, &value); + if (rc != 0) { + fprintf(stderr, + "FAIL: valid negative block count array rejected: %s\n", + avro_strerror()); + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + rc = avro_value_get_size(&value, &count); + if (rc != 0 || count != 3) { + fprintf(stderr, + "FAIL: expected 3 elements, got %zu (rc=%d)\n", + count, rc); + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + printf("PASS: valid negative block count array decoded (%zu items)\n", + count); + + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 0; +} + +static int test_valid_neg_block_map(void) +{ + int rc; + size_t count; + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + avro_value_t value; + avro_reader_t reader = NULL; + + rc = avro_schema_from_json_literal( + "{\"type\":\"map\",\"values\":\"int\"}", &schema); + if (rc != 0) { + fprintf(stderr, "Failed to parse map schema: %s\n", + avro_strerror()); + return 1; + } + + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + fprintf(stderr, "Failed to create map iface\n"); + avro_schema_decref(schema); + return 1; + } + + rc = avro_generic_value_new(iface, &value); + if (rc != 0) { + fprintf(stderr, "Failed to create map value: %s\n", + avro_strerror()); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + reader = avro_reader_memory(valid_neg_block_map, + sizeof(valid_neg_block_map)); + if (reader == NULL) { + fprintf(stderr, "Failed to create reader\n"); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + rc = avro_value_read(reader, &value); + if (rc != 0) { + fprintf(stderr, + "FAIL: valid negative block count map rejected: %s\n", + avro_strerror()); + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + rc = avro_value_get_size(&value, &count); + if (rc != 0 || count != 2) { + fprintf(stderr, + "FAIL: expected 2 map entries, got %zu (rc=%d)\n", + count, rc); + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 1; + } + + printf("PASS: valid negative block count map decoded (%zu entries)\n", + count); + + avro_reader_free(reader); + avro_value_decref(&value); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return 0; +} + +int main(void) +{ + int failures = 0; + + printf("Testing INT64_MIN block count rejection (CWE-190)...\n\n"); + + failures += test_array_int64min(); + failures += test_map_int64min(); + failures += test_valid_neg_block_array(); + failures += test_valid_neg_block_map(); + + printf("\n%s: %d test(s) failed\n", + failures ? "FAIL" : "OK", failures); + + return failures ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/lang/c/tests/test_avro_4293.c b/lang/c/tests/test_avro_4293.c new file mode 100644 index 00000000000..4f6b5387144 --- /dev/null +++ b/lang/c/tests/test_avro_4293.c @@ -0,0 +1,547 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* + * A bytes/string value is encoded as a length prefix followed by that many + * bytes of data. A malicious or truncated input can declare a huge length with + * little or no actual data, which previously caused a correspondingly huge + * allocation before the shortfall was noticed. When the reader knows how many + * bytes remain (a memory-backed reader), the declared length must be rejected + * before allocating for it. + */ + +#include +#include +#include +#include +#include +#include + +/* Portable set/unset of the collection-limit environment variable: MSVC has no + * setenv/unsetenv, so use _putenv_s (an empty value unsets the variable). */ +#ifdef _WIN32 +static void set_collection_limit(const char *value) +{ + _putenv_s("AVRO_MAX_COLLECTION_ITEMS", value); +} +static void unset_collection_limit(void) +{ + _putenv_s("AVRO_MAX_COLLECTION_ITEMS", ""); +} +#else +static void set_collection_limit(const char *value) +{ + setenv("AVRO_MAX_COLLECTION_ITEMS", value, 1); +} +static void unset_collection_limit(void) +{ + unsetenv("AVRO_MAX_COLLECTION_ITEMS"); +} +#endif + +/* Decodes buf against the given schema. Returns the avro_value_read rc (0 on a + * successful read, non-zero when the read is rejected), or -1 if the test + * harness itself fails to set up (avro_generic_value_new / avro_reader_memory). + * Callers distinguish "rejected" (> 0) from "harness error" (-1). */ +static int try_decode(avro_value_iface_t *iface, const char *buf, size_t buf_size) +{ + int rc; + avro_value_t decoded; + avro_reader_t reader; + + rc = avro_generic_value_new(iface, &decoded); + if (rc != 0) { + fprintf(stderr, "avro_generic_value_new failed: %s\n", avro_strerror()); + return -1; + } + + reader = avro_reader_memory(buf, buf_size); + if (reader == NULL) { + fprintf(stderr, "avro_reader_memory failed\n"); + avro_value_decref(&decoded); + return -1; + } + + rc = avro_value_read(reader, &decoded); + + avro_reader_free(reader); + avro_value_decref(&decoded); + return rc; +} + +static int check_rejects_oversized(const char *schema_literal, const char *label) +{ + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + int rc; + int ret = EXIT_FAILURE; + + /* + * A length prefix declaring 127 bytes (zig-zag long 254 -> varint + * 0xFE 0x01), followed by no data at all. The reader has 0 bytes + * remaining after the prefix, so a declared length of 127 must be + * rejected before allocating. + */ + const char oversized[] = { (char) 0xFE, (char) 0x01 }; + + if (avro_schema_from_json_length(schema_literal, strlen(schema_literal), + &schema) != 0) { + fprintf(stderr, "%s: failed to parse schema: %s\n", label, avro_strerror()); + return EXIT_FAILURE; + } + + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + fprintf(stderr, "%s: failed to create iface\n", label); + avro_schema_decref(schema); + return EXIT_FAILURE; + } + + rc = try_decode(iface, oversized, sizeof(oversized)); + if (rc == 0) { + fprintf(stderr, "%s: FAIL - oversized length was accepted\n", label); + } else if (rc != EINVAL) { + /* The availability checks reject with EINVAL before allocating. + * A different error (e.g. ENOSPC from failing to read after a + * large allocation) would indicate the pre-fix behavior. */ + fprintf(stderr, "%s: FAIL - rejected with rc=%d (expected EINVAL): %s\n", + label, rc, avro_strerror()); + } else { + fprintf(stderr, "%s: oversized length rejected as expected: %s\n", + label, avro_strerror()); + ret = EXIT_SUCCESS; + } + + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return ret; +} + +/* Decodes an enum whose declared symbol index is out of range for the schema. + * The index must be rejected before it is stored, rather than accepted as an + * out-of-bounds ordinal. */ +static int check_enum_index_rejected(const char *encoded, size_t size, const char *label) +{ + const char *schema_literal = + "{\"type\":\"enum\",\"name\":\"E\",\"symbols\":[\"A\",\"B\"]}"; + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + int rc; + int ret = EXIT_FAILURE; + + if (avro_schema_from_json_length(schema_literal, strlen(schema_literal), + &schema) != 0) { + fprintf(stderr, "%s: failed to parse schema: %s\n", label, avro_strerror()); + return EXIT_FAILURE; + } + + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + fprintf(stderr, "%s: failed to create iface\n", label); + avro_schema_decref(schema); + return EXIT_FAILURE; + } + + rc = try_decode(iface, encoded, size); + if (rc == 0) { + fprintf(stderr, "%s: FAIL - out-of-range enum index was accepted\n", label); + } else { + fprintf(stderr, "%s: out-of-range enum index rejected as expected: %s\n", + label, avro_strerror()); + ret = EXIT_SUCCESS; + } + + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return ret; +} + +static int check_accepts_valid(void) +{ + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + int rc; + int ret = EXIT_FAILURE; + const char *text = NULL; + size_t text_size = 0; + avro_value_t decoded; + avro_reader_t reader; + + /* A well-formed 3-byte string "abc": length 3 (zig-zag 6 -> 0x06), + * then the bytes 'a','b','c'. This must still decode. */ + const char valid[] = { 0x06, 'a', 'b', 'c' }; + + if (avro_schema_from_json_literal("\"string\"", &schema) != 0) { + fprintf(stderr, "valid: failed to parse schema: %s\n", avro_strerror()); + return EXIT_FAILURE; + } + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + fprintf(stderr, "valid: failed to create iface\n"); + avro_schema_decref(schema); + return EXIT_FAILURE; + } + + if (avro_generic_value_new(iface, &decoded) != 0) { + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return EXIT_FAILURE; + } + reader = avro_reader_memory(valid, sizeof(valid)); + if (reader == NULL) { + fprintf(stderr, "valid: avro_reader_memory failed\n"); + avro_value_decref(&decoded); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return EXIT_FAILURE; + } + rc = avro_value_read(reader, &decoded); + if (rc == 0 && avro_value_get_string(&decoded, &text, &text_size) == 0 && + text_size == 4 && strcmp(text, "abc") == 0) { + fprintf(stderr, "valid: well-formed string decoded as expected\n"); + ret = EXIT_SUCCESS; + } else { + fprintf(stderr, "valid: FAIL - well-formed string did not decode (rc=%d)\n", rc); + } + + avro_reader_free(reader); + avro_value_decref(&decoded); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return ret; +} + +/* An array of nulls within the configured limit: null elements occupy zero + * bytes, so a moderate declared count is legitimate and must not be rejected by + * the available-bytes check (it is instead bounded by the zero-byte item cap). */ +static int check_accepts_null_array(void) +{ + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + avro_value_t decoded; + avro_reader_t reader; + int rc; + int ret = EXIT_FAILURE; + size_t count = 0; + + /* count 127 (zig-zag 254 -> 0xFE 0x01), then the end-of-array marker 0. */ + const char null_array[] = { (char) 0xFE, (char) 0x01, 0x00 }; + + if (avro_schema_from_json_length("{\"type\":\"array\",\"items\":\"null\"}", + strlen("{\"type\":\"array\",\"items\":\"null\"}"), + &schema) != 0) { + fprintf(stderr, "null-array: failed to parse schema: %s\n", avro_strerror()); + return EXIT_FAILURE; + } + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + avro_schema_decref(schema); + return EXIT_FAILURE; + } + if (avro_generic_value_new(iface, &decoded) != 0) { + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return EXIT_FAILURE; + } + reader = avro_reader_memory(null_array, sizeof(null_array)); + if (reader == NULL) { + fprintf(stderr, "null-array: avro_reader_memory failed\n"); + avro_value_decref(&decoded); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return EXIT_FAILURE; + } + rc = avro_value_read(reader, &decoded); + if (rc == 0 && avro_value_get_size(&decoded, &count) == 0 && count == 127) { + fprintf(stderr, "null-array: 127 nulls decoded, not falsely rejected\n"); + ret = EXIT_SUCCESS; + } else { + fprintf(stderr, "null-array: FAIL (rc=%d count=%zu): %s\n", + rc, count, avro_strerror()); + } + + avro_reader_free(reader); + avro_value_decref(&decoded); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return ret; +} + +/* + * Zero-byte-element collection allocation limit + */ + +/* Encode a long as an Avro zig-zag varint into buf; returns the byte count. */ +static size_t encode_long(int64_t n, char *buf) +{ + /* Compute the sign extension portably: right-shifting a negative signed + * value is implementation-defined, so derive the all-ones/zero mask from + * a comparison instead of (n >> 63). */ + uint64_t sign = (n < 0) ? ~(uint64_t) 0 : (uint64_t) 0; + uint64_t z = ((uint64_t) n << 1) ^ sign; + size_t i = 0; + do { + uint8_t b = z & 0x7F; + z >>= 7; + if (z) { + b |= 0x80; + } + buf[i++] = (char) b; + } while (z); + return i; +} + +/* Build one array block of `count` elements (a negative count is followed by a + * block byte-size), plus the end-of-array marker. Returns the total length. */ +static size_t build_null_array(int64_t count, int negative, char *buf) +{ + size_t n = 0; + if (negative) { + n += encode_long(-count, buf + n); + n += encode_long(0, buf + n); /* block byte-size */ + } else { + n += encode_long(count, buf + n); + } + n += encode_long(0, buf + n); /* end-of-array marker */ + return n; +} + +/* Decode `buf` against `schema_literal` and return the avro_value_read rc. */ +static int decode_schema(const char *schema_literal, const char *buf, size_t buf_size) +{ + avro_schema_t schema = NULL; + avro_value_iface_t *iface = NULL; + int rc; + + if (avro_schema_from_json_length(schema_literal, strlen(schema_literal), + &schema) != 0) { + fprintf(stderr, "failed to parse schema: %s\n", avro_strerror()); + return -1; + } + iface = avro_generic_class_from_schema(schema); + if (iface == NULL) { + avro_schema_decref(schema); + return -1; + } + rc = try_decode(iface, buf, buf_size); + avro_value_iface_decref(iface); + avro_schema_decref(schema); + return rc; +} + +static int check_null_collection_rejected(const char *schema_literal, int64_t count, + int negative, const char *label) +{ + char buf[32]; + size_t len = build_null_array(count, negative, buf); + int rc = decode_schema(schema_literal, buf, len); + if (rc == EINVAL) { + fprintf(stderr, "%s: rejected as expected: %s\n", label, avro_strerror()); + return EXIT_SUCCESS; + } + fprintf(stderr, "%s: FAIL - rc=%d (expected EINVAL)\n", label, rc); + return EXIT_FAILURE; +} + +static int check_null_array_accepted(int64_t count, const char *label) +{ + char buf[32]; + size_t len = build_null_array(count, 0, buf); + int rc = decode_schema("{\"type\":\"array\",\"items\":\"null\"}", buf, len); + if (rc == 0) { + fprintf(stderr, "%s: accepted as expected\n", label); + return EXIT_SUCCESS; + } + fprintf(stderr, "%s: FAIL - rc=%d (expected 0)\n", label, rc); + return EXIT_FAILURE; +} + +/* Two blocks of `each` null elements followed by the end marker. */ +static int check_null_array_cumulative_rejected(int64_t each, const char *label) +{ + char buf[48]; + size_t n = 0; + int rc; + n += encode_long(each, buf + n); + n += encode_long(each, buf + n); + n += encode_long(0, buf + n); + rc = decode_schema("{\"type\":\"array\",\"items\":\"null\"}", buf, n); + if (rc == EINVAL) { + fprintf(stderr, "%s: rejected as expected: %s\n", label, avro_strerror()); + return EXIT_SUCCESS; + } + fprintf(stderr, "%s: FAIL - rc=%d (expected EINVAL)\n", label, rc); + return EXIT_FAILURE; +} + +/* Skipping a zero-byte array/map is bounded the same way as reading it. */ +static int check_skip_null_collection_rejected(const char *schema_literal, int64_t count, + const char *label) +{ + avro_schema_t schema = NULL; + avro_reader_t reader; + char buf[32]; + size_t len; + int rc; + + if (avro_schema_from_json_length(schema_literal, strlen(schema_literal), + &schema) != 0) { + return EXIT_FAILURE; + } + len = build_null_array(count, 0, buf); + reader = avro_reader_memory(buf, len); + if (reader == NULL) { + avro_schema_decref(schema); + return EXIT_FAILURE; + } + rc = avro_skip_data(reader, schema); + avro_reader_free(reader); + avro_schema_decref(schema); + if (rc == EINVAL) { + fprintf(stderr, "%s: skip rejected as expected\n", label); + return EXIT_SUCCESS; + } + fprintf(stderr, "%s: FAIL - skip rc=%d (expected EINVAL)\n", label, rc); + return EXIT_FAILURE; +} + +/* A backed non-zero-byte array that passes the bytes check is still bounded by + * the structural cap (exercised with a lowered limit). */ +static int check_structural_cap_rejected(const char *label) +{ + char buf[64]; + size_t n = 0; + int i; + int rc; + n += encode_long(10, buf + n); /* block count 10 */ + for (i = 0; i < 10; i++) { + n += encode_long(i, buf + n); /* 10 real longs (1 byte each) */ + } + n += encode_long(0, buf + n); /* end marker */ + rc = decode_schema("{\"type\":\"array\",\"items\":\"long\"}", buf, n); + if (rc == EINVAL) { + fprintf(stderr, "%s: rejected as expected: %s\n", label, avro_strerror()); + return EXIT_SUCCESS; + } + fprintf(stderr, "%s: FAIL - rc=%d (expected EINVAL)\n", label, rc); + return EXIT_FAILURE; +} + +int main(void) +{ + const char *array_null = "{\"type\":\"array\",\"items\":\"null\"}"; + const char *record_null = + "{\"type\":\"array\",\"items\":" + "{\"type\":\"record\",\"name\":\"R\",\"fields\":[{\"name\":\"n\",\"type\":\"null\"}]}}"; + const char *map_null = "{\"type\":\"map\",\"values\":\"null\"}"; + + if (check_rejects_oversized("\"string\"", "string") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (check_rejects_oversized("\"bytes\"", "bytes") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + /* An array/map declaring 127 elements with no data must be rejected: the + * count exceeds the bytes that could back it. */ + if (check_rejects_oversized("{\"type\":\"array\",\"items\":\"long\"}", "array") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (check_rejects_oversized("{\"type\":\"map\",\"values\":\"long\"}", "map") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (check_accepts_valid() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (check_accepts_null_array() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + + /* The reported exploit: 200,000,000 nulls must be rejected by the + * default limit before allocating. */ + if (check_null_collection_rejected(array_null, 200000000, 0, + "array 200M (default limit)") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + /* A record whose only field is null also encodes to zero bytes. */ + if (check_null_collection_rejected(record_null, 200000000, 0, + "array 200M") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + /* A negative block count is normalized and must still be bounded. */ + if (check_null_collection_rejected(array_null, 200000000, 1, + "array negative count") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + + /* With a lowered limit, boundary behavior: 1000 accepted, 1001 rejected. */ + set_collection_limit("1000"); + if (check_null_array_accepted(1000, "array 1000 within limit") != EXIT_SUCCESS) { + unset_collection_limit(); + return EXIT_FAILURE; + } + if (check_null_collection_rejected(array_null, 1001, 0, + "array 1001 over limit") != EXIT_SUCCESS) { + unset_collection_limit(); + return EXIT_FAILURE; + } + if (check_null_array_cumulative_rejected(600, "array cumulative 600+600") != EXIT_SUCCESS) { + unset_collection_limit(); + return EXIT_FAILURE; + } + /* Skipping a huge zero-byte array is bounded too. */ + if (check_skip_null_collection_rejected(array_null, 5000, + "skip array over limit") != EXIT_SUCCESS) { + unset_collection_limit(); + return EXIT_FAILURE; + } + unset_collection_limit(); + + /* A backed non-zero-byte array is bounded by the structural cap. */ + set_collection_limit("5"); + if (check_structural_cap_rejected("array over structural cap") != EXIT_SUCCESS) { + unset_collection_limit(); + return EXIT_FAILURE; + } + unset_collection_limit(); + + /* A huge map is bounded by the available-bytes check (each entry + * carries a >= 1 byte key), rejected with EINVAL as well. */ + if (check_null_collection_rejected(map_null, 200000000, 0, + "map 200M (available bytes)") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + + /* An enum symbol index out of range (>= symbol count, or negative) must be + * rejected rather than stored as an out-of-bounds ordinal. The schema has + * two symbols; index 9 (zig-zag long -> 0x12) and index -1 (-> 0x01) are + * both invalid. */ + { + const char enum_too_large[] = { (char) 0x12 }; + const char enum_negative[] = { (char) 0x01 }; + if (check_enum_index_rejected(enum_too_large, sizeof(enum_too_large), + "enum index 9 of 2") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (check_enum_index_rejected(enum_negative, sizeof(enum_negative), + "enum index -1") != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} +