-
Notifications
You must be signed in to change notification settings - Fork 1.8k
AVRO-4293: [c] Bound allocation when decoding length-prefixed values and collections #3858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cfb7698
896a688
b549013
3601d0a
d780e77
c603c96
cdf28ee
36b71c2
9b626d9
e333337
889be2d
e06c25d
7966926
db7a9e1
3a0b1ac
4ecd191
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| #include "encoding.h" | ||
| #include <stdlib.h> | ||
| #include <limits.h> | ||
| #include <stdint.h> | ||
| #include <errno.h> | ||
| #include <string.h> | ||
|
|
||
|
|
@@ -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,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
+237
to
241
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. Added an explicit
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already handled — read_string bounds str_len with |
||
| avro_set_error("Cannot allocate buffer for string value"); | ||
| return ENOMEM; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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 - 1bound and does the +1 in size_t before allocating.