diff --git a/Changes.md b/Changes.md index c4b2af66..2ed68428 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,24 @@ ## next release +- Bounded the resources that `MMDB_get_entry_data_list()` spends decoding a + single entry. A crafted database could nest data-section pointers to shared + targets so that decoding one entry cost exponential time and memory, or point + many times at one large value so that a caller copying the result materialized + far more data than the file holds. The decoder now follows the Reader Resource + Limits section of the MaxMind DB specification. Each call is limited to 65,536 + values and 2 MiB of string and bytes payload, in addition to the existing + recursive-decoder depth limit of 512. See the `MMDB_get_entry_data_list()` + documentation for details. + - Exceeding a limit returns the new `MMDB_DECODER_LIMIT_ERROR` status. A + full-list failure leaves the output set to `NULL`. + - `MMDB_get_value()`, `MMDB_vget_value()`, and `MMDB_aget_value()` now return + `MMDB_DECODER_LIMIT_ERROR` instead of `MMDB_INVALID_DATA_ERROR` when they + skip a subtree past the depth limit. + - `MMDB_open()` returns `MMDB_INVALID_METADATA_ERROR` when metadata processing + exceeds a decoder limit. + - The limits can be raised when building the library with + `-DMAXIMUM_DATA_STRUCTURE_DEPTH`, `-DMAXIMUM_DATA_STRUCTURE_VALUES`, and + `-DMAXIMUM_DATA_STRUCTURE_BYTES`. - Fixed an out-of-bounds read in `MMDB_lookup_sockaddr()` when callers passed a `sockaddr` with an unsupported address family. The function now rejects any family other than `AF_INET` and `AF_INET6` with diff --git a/README.fuzzing.md b/README.fuzzing.md index 621061c0..706e4bef 100644 --- a/README.fuzzing.md +++ b/README.fuzzing.md @@ -34,7 +34,7 @@ $ cmake --build . -j$(nproc) ```shell $ mkdir -p fuzz_mmdb_seed fuzz_mmdb_seed_corpus -$ find ../t/maxmind-db/test-data/ -type f -size -4k -exec cp {} ./fuzz_mmdb_seed_corpus/ \; +$ find ../t/maxmind-db/test-data/ -type f -size -256k -exec cp {} ./fuzz_mmdb_seed_corpus/ \; $ ./t/fuzz_mmdb fuzz_mmdb_seed/ fuzz_mmdb_seed_corpus/ ``` diff --git a/doc/libmaxminddb.md b/doc/libmaxminddb.md index 10ecc743..092bcff7 100644 --- a/doc/libmaxminddb.md +++ b/doc/libmaxminddb.md @@ -395,6 +395,9 @@ status codes are: array where none exist. - `MMDB_INVALID_NETWORK_ADDRESS_ERROR` - `MMDB_lookup_sockaddr()` was given a `sockaddr` whose family is neither `AF_INET` nor `AF_INET6`. +- `MMDB_DECODER_LIMIT_ERROR` - decoding a data structure would exceed the + configured nesting depth, value-count, or string/bytes payload limit. The + structure may still be valid MaxMind DB data. All status codes should be treated as `int` values. @@ -452,6 +455,10 @@ You can also pass `0` as the `flags` value in which case the database will be opened with the default flags. However, these defaults may change in future releases. The current default is `MMDB_MODE_MMAP`. +If metadata processing exceeds one of the decoder resource limits described +under `MMDB_get_entry_data_list()`, this function returns +`MMDB_INVALID_METADATA_ERROR`. + ## `MMDB_close()` ```c @@ -640,6 +647,31 @@ This function allows you to get all of the data for a complex data structure at once, rather than looking up each piece using repeated calls to `MMDB_get_value()`. +A crafted database can make a full decode expensive, so this function bounds the +work and the caller-visible payload. Each call decodes at most 65,536 list +values and 2 MiB of UTF-8 string and bytes payload. A structure exactly at +either limit is accepted. The recursive-decoder depth limit of 512 also applies. +Exceeding any limit returns `MMDB_DECODER_LIMIT_ERROR` and sets +`entry_data_list` to `NULL`. + +The limits are per call and can be changed when rebuilding libmaxminddb by +defining the positive integer macros `MAXIMUM_DATA_STRUCTURE_DEPTH`, +`MAXIMUM_DATA_STRUCTURE_VALUES`, and `MAXIMUM_DATA_STRUCTURE_BYTES`. For +example, pass `-DMAXIMUM_DATA_STRUCTURE_BYTES=3145728` in the library's compiler +flags. Use an integer constant rather than an expression. A bare shift such as +`1<<31` is evaluated as `int` and does not express a 2 GiB limit. The depth +value must fit in `int`, the value count in `size_t`, and the byte count in +`uint64_t`. This requires rebuilding the library itself. Defining a macro only +while building an application does not change a packaged shared library. + +`MMDB_get_value()`, `MMDB_vget_value()`, and `MMDB_aget_value()` do not expand a +complete structure and therefore do not charge the value-count or payload +budgets. When they skip an unselected map or array, the depth limit applies to +that subtree and may return `MMDB_DECODER_LIMIT_ERROR`; it does not bound the +selected lookup path itself. Applications that cannot rebuild a packaged library +can use those functions to retrieve a specific field from an otherwise +over-limit record. + ```c MMDB_lookup_result_s result = MMDB_lookup_sockaddr(&mmdb, address->ai_addr, &mmdb_error); @@ -717,7 +749,9 @@ int MMDB_get_metadata_as_entry_data_list( This function allows you to retrieve the database metadata as a linked list of `MMDB_entry_data_list_s` structures. This can be a more convenient way to deal -with the metadata than using the metadata structure directly. +with the metadata than using the metadata structure directly. It uses the same +per-call limits as `MMDB_get_entry_data_list()` and returns +`MMDB_DECODER_LIMIT_ERROR` if the complete metadata list exceeds any of them. ```c MMDB_entry_data_list_s *entry_data_list, *first; diff --git a/include/maxminddb.h b/include/maxminddb.h index 59f404db..ea0d9691 100644 --- a/include/maxminddb.h +++ b/include/maxminddb.h @@ -87,6 +87,7 @@ extern "C" { #define MMDB_INVALID_NODE_NUMBER_ERROR (10) #define MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR (11) #define MMDB_INVALID_NETWORK_ADDRESS_ERROR (12) + #define MMDB_DECODER_LIMIT_ERROR (13) #if !(MMDB_UINT128_IS_BYTE_ARRAY) #if MMDB_UINT128_USING_MODE diff --git a/src/data-pool.c b/src/data-pool.c index 3bc63286..e4b00f3b 100644 --- a/src/data-pool.c +++ b/src/data-pool.c @@ -9,20 +9,27 @@ #include #include -// Allocate an MMDB_data_pool_s. It initially has space for size -// MMDB_entry_data_list_s structs. -MMDB_data_pool_s *data_pool_new(size_t const size) { +// Allocate an MMDB_data_pool_s. It initially has space for up to size +// MMDB_entry_data_list_s structs. Its total capacity will not exceed max_size. +MMDB_data_pool_s *data_pool_new(size_t const size, size_t const max_size) { MMDB_data_pool_s *const pool = calloc(1, sizeof(MMDB_data_pool_s)); if (!pool) { return NULL; } - if (size == 0 || - !can_multiply(SIZE_MAX, size, sizeof(MMDB_entry_data_list_s))) { + if (size == 0 || max_size == 0) { data_pool_destroy(pool); return NULL; } - pool->size = size; + size_t initial_size = size; + if (initial_size > max_size) { + initial_size = max_size; + } + if (!can_multiply(SIZE_MAX, initial_size, sizeof(MMDB_entry_data_list_s))) { + data_pool_destroy(pool); + return NULL; + } + pool->size = initial_size; pool->blocks[0] = calloc(pool->size, sizeof(MMDB_entry_data_list_s)); if (!pool->blocks[0]) { data_pool_destroy(pool); @@ -30,7 +37,9 @@ MMDB_data_pool_s *data_pool_new(size_t const size) { } pool->blocks[0]->pool = pool; - pool->sizes[0] = size; + pool->sizes[0] = initial_size; + pool->capacity = initial_size; + pool->max_size = max_size; pool->block = pool->blocks[0]; @@ -62,8 +71,9 @@ void data_pool_destroy(MMDB_data_pool_s *const pool) { free(pool); } -// Claim a new struct from the pool. Doing this may cause the pool's size to -// grow. +// Claim a new struct from the pool. Doing this may grow the pool. NULL means an +// allocation failed or the pool reached its maximum capacity. Decoder callers +// check their logical limit before reaching the capacity limit. MMDB_entry_data_list_s *data_pool_alloc(MMDB_data_pool_s *const pool) { if (!pool) { return NULL; @@ -75,6 +85,10 @@ MMDB_entry_data_list_s *data_pool_alloc(MMDB_data_pool_s *const pool) { return element; } + if (pool->capacity == pool->max_size) { + return NULL; + } + // Take it from a new block of memory. size_t const new_index = pool->index + 1; @@ -83,10 +97,11 @@ MMDB_entry_data_list_s *data_pool_alloc(MMDB_data_pool_s *const pool) { return NULL; } - if (!can_multiply(SIZE_MAX, pool->size, 2)) { - return NULL; + size_t const remaining = pool->max_size - pool->capacity; + size_t new_size = remaining; + if (pool->size <= remaining / 2) { + new_size = pool->size * 2; } - size_t const new_size = pool->size * 2; if (!can_multiply(SIZE_MAX, new_size, sizeof(MMDB_entry_data_list_s))) { return NULL; @@ -104,6 +119,7 @@ MMDB_entry_data_list_s *data_pool_alloc(MMDB_data_pool_s *const pool) { pool->size = new_size; pool->sizes[pool->index] = pool->size; + pool->capacity += new_size; MMDB_entry_data_list_s *const element = pool->block; pool->used = 1; diff --git a/src/data-pool.h b/src/data-pool.h index 9e61b768..23581577 100644 --- a/src/data-pool.h +++ b/src/data-pool.h @@ -6,12 +6,10 @@ #include #include -// This should be large enough that we never need to grow the array of pointers -// to blocks. 32 is enough. Even starting out of with size 1 (1 struct), the -// 32nd element alone will provide 2**32 structs as we exponentially increase -// the number in each block. Being confident that we do not have to grow the -// array lets us avoid writing code to do that. That code would be risky as it -// would rarely be hit and likely not be well tested. +// Keep the block array fixed so that its own growth does not need a rarely used +// reallocation path. Even starting with one struct, 32 geometrically growing +// blocks cover every practical allocation; the last block may be clamped to the +// configured capacity. #define DATA_POOL_NUM_BLOCKS 32 // A pool of memory for MMDB_entry_data_list_s structs. This is so we can @@ -33,6 +31,12 @@ typedef struct MMDB_data_pool_s { // How many used in the current block, counting by structs. size_t used; + // Total number of structs reserved across all blocks. + size_t capacity; + + // Maximum total number of structs this pool may reserve. + size_t max_size; + // The current block we're allocating out of. MMDB_entry_data_list_s *block; @@ -45,7 +49,7 @@ typedef struct MMDB_data_pool_s { } MMDB_data_pool_s; bool can_multiply(size_t const, size_t const, size_t const); -MMDB_data_pool_s *data_pool_new(size_t const); +MMDB_data_pool_s *data_pool_new(size_t const, size_t const); void data_pool_destroy(MMDB_data_pool_s *const); MMDB_entry_data_list_s *data_pool_alloc(MMDB_data_pool_s *const); MMDB_entry_data_list_s *data_pool_to_list(MMDB_data_pool_s *const); diff --git a/src/maxminddb.c b/src/maxminddb.c index 8d82a9e7..634635b4 100644 --- a/src/maxminddb.c +++ b/src/maxminddb.c @@ -34,7 +34,49 @@ typedef ADDRESS_FAMILY sa_family_t; #endif #define MMDB_DATA_SECTION_SEPARATOR (16) -#define MAXIMUM_DATA_STRUCTURE_DEPTH (512) +// The maximum recursive decoder depth for a single entry. This stops unbounded +// recursion, including a pointer cycle. See "Reader Resource Limits" in the +// MaxMind DB specification. +#ifndef MAXIMUM_DATA_STRUCTURE_DEPTH + #define MAXIMUM_DATA_STRUCTURE_DEPTH (512) +#endif + +#if MAXIMUM_DATA_STRUCTURE_DEPTH < 1 || MAXIMUM_DATA_STRUCTURE_DEPTH > INT_MAX + #error "MAXIMUM_DATA_STRUCTURE_DEPTH must be between 1 and INT_MAX" +#endif + +// The maximum number of data-section values decoded for a single entry. This +// bounds a pointer fan-out, where nested pointers to shared targets would +// otherwise cost 2**depth decode operations. The largest real records decode a +// few hundred values, so this leaves a wide margin. See "Reader Resource +// Limits" in the MaxMind DB specification. +#ifndef MAXIMUM_DATA_STRUCTURE_VALUES + #define MAXIMUM_DATA_STRUCTURE_VALUES (1U << 16) +#endif + +// The upper bound matters on platforms where size_t is narrower than the +// preprocessor's integer arithmetic. +#if MAXIMUM_DATA_STRUCTURE_VALUES < 1 || \ + MAXIMUM_DATA_STRUCTURE_VALUES > SIZE_MAX + #error "MAXIMUM_DATA_STRUCTURE_VALUES must be between 1 and SIZE_MAX" +#endif + +// The maximum total bytes of string and bytes payloads decoded for a single +// entry. libmaxminddb borrows payload bytes (each node points into the data +// section, it does not copy), so the value count above already bounds the +// library's own memory. But a fan-out of pointers to one large value produces +// many nodes that all reference it. A caller that copies each node into a +// language string then materializes far more than the file holds. This bounds +// that copied total. The largest real records hold about a kilobyte of +// payload, so 2 MiB leaves a wide margin while stopping the amplification. It +// can be raised at build time with -DMAXIMUM_DATA_STRUCTURE_BYTES=. +#ifndef MAXIMUM_DATA_STRUCTURE_BYTES + #define MAXIMUM_DATA_STRUCTURE_BYTES (1U << 21) +#endif + +#if MAXIMUM_DATA_STRUCTURE_BYTES < 1 + #error "MAXIMUM_DATA_STRUCTURE_BYTES must be at least 1" +#endif #ifdef MMDB_DEBUG #define DEBUG_MSG(msg) fprintf(stderr, msg "\n") @@ -131,6 +173,11 @@ typedef struct record_info_s { uint8_t right_record_offset; } record_info_s; +typedef struct decode_state_s { + size_t values; + uint64_t bytes; +} decode_state_s; + #define METADATA_MARKER "\xab\xcd\xefMaxMind.com" /* This is 128kb */ #define METADATA_BLOCK_MAX_SIZE 131072 @@ -193,7 +240,12 @@ static int get_entry_data_list(const MMDB_s *const mmdb, uint32_t offset, MMDB_entry_data_list_s *const entry_data_list, MMDB_data_pool_s *const pool, + decode_state_s *const decode_state, int depth); +static int +alloc_entry_data_list(MMDB_data_pool_s *const pool, + decode_state_s *const decode_state, + MMDB_entry_data_list_s **const entry_data_list); static float get_ieee754_float(const uint8_t *restrict p); static double get_ieee754_double(const uint8_t *restrict p); static uint32_t get_uint32(const uint8_t *p); @@ -284,6 +336,10 @@ int MMDB_open(const char *const filename, uint32_t flags, MMDB_s *const mmdb) { mmdb->metadata_section_size = metadata_size; status = read_metadata(mmdb); + if (MMDB_DECODER_LIMIT_ERROR == status) { + // Metadata that exceeds a decoder limit is invalid metadata. + status = MMDB_INVALID_METADATA_ERROR; + } if (MMDB_SUCCESS != status) { goto cleanup; } @@ -1368,7 +1424,7 @@ static int skip_map_or_array(const MMDB_s *const mmdb, int depth) { if (depth >= MAXIMUM_DATA_STRUCTURE_DEPTH) { DEBUG_MSG("reached the maximum data structure depth"); - return MMDB_INVALID_DATA_ERROR; + return MMDB_DECODER_LIMIT_ERROR; } if (entry_data->type == MMDB_DATA_TYPE_MAP) { @@ -1688,19 +1744,23 @@ int MMDB_get_entry_data_list(MMDB_entry_s *start, MMDB_entry_data_list_s **const entry_data_list) { *entry_data_list = NULL; - MMDB_data_pool_s *const pool = data_pool_new(MMDB_POOL_INIT_SIZE); + size_t const maximum_values = (size_t)(MAXIMUM_DATA_STRUCTURE_VALUES); + MMDB_data_pool_s *const pool = + data_pool_new(MMDB_POOL_INIT_SIZE, maximum_values); if (!pool) { return MMDB_OUT_OF_MEMORY_ERROR; } - MMDB_entry_data_list_s *const list = data_pool_alloc(pool); - if (!list) { + decode_state_s decode_state = {0}; + MMDB_entry_data_list_s *list = NULL; + int status = alloc_entry_data_list(pool, &decode_state, &list); + if (MMDB_SUCCESS != status) { data_pool_destroy(pool); - return MMDB_OUT_OF_MEMORY_ERROR; + return status; } - int const status = - get_entry_data_list(start->mmdb, start->offset, list, pool, 0); + status = get_entry_data_list( + start->mmdb, start->offset, list, pool, &decode_state, 0); if (MMDB_SUCCESS != status) { data_pool_destroy(pool); return status; @@ -1715,14 +1775,33 @@ int MMDB_get_entry_data_list(MMDB_entry_s *start, return status; } +static int +alloc_entry_data_list(MMDB_data_pool_s *const pool, + decode_state_s *const decode_state, + MMDB_entry_data_list_s **const entry_data_list) { + size_t const maximum_values = (size_t)(MAXIMUM_DATA_STRUCTURE_VALUES); + if (decode_state->values >= maximum_values) { + DEBUG_MSG("reached the maximum number of data structure values"); + return MMDB_DECODER_LIMIT_ERROR; + } + + *entry_data_list = data_pool_alloc(pool); + if (!*entry_data_list) { + return MMDB_OUT_OF_MEMORY_ERROR; + } + decode_state->values++; + return MMDB_SUCCESS; +} + static int get_entry_data_list(const MMDB_s *const mmdb, uint32_t offset, MMDB_entry_data_list_s *const entry_data_list, MMDB_data_pool_s *const pool, + decode_state_s *const decode_state, int depth) { if (depth >= MAXIMUM_DATA_STRUCTURE_DEPTH) { DEBUG_MSG("reached the maximum data structure depth"); - return MMDB_INVALID_DATA_ERROR; + return MMDB_DECODER_LIMIT_ERROR; } depth++; CHECKED_DECODE_ONE(mmdb, offset, &entry_data_list->entry_data); @@ -1745,8 +1824,12 @@ static int get_entry_data_list(const MMDB_s *const mmdb, if (entry_data_list->entry_data.type == MMDB_DATA_TYPE_ARRAY || entry_data_list->entry_data.type == MMDB_DATA_TYPE_MAP) { - int status = get_entry_data_list( - mmdb, last_offset, entry_data_list, pool, depth); + int status = get_entry_data_list(mmdb, + last_offset, + entry_data_list, + pool, + decode_state, + depth); if (MMDB_SUCCESS != status) { DEBUG_MSG("get_entry_data_list on pointer failed."); return status; @@ -1764,14 +1847,19 @@ static int get_entry_data_list(const MMDB_s *const mmdb, return MMDB_INVALID_DATA_ERROR; } while (array_size-- > 0) { - MMDB_entry_data_list_s *entry_data_list_to = - data_pool_alloc(pool); - if (!entry_data_list_to) { - return MMDB_OUT_OF_MEMORY_ERROR; + MMDB_entry_data_list_s *entry_data_list_to = NULL; + int status = alloc_entry_data_list( + pool, decode_state, &entry_data_list_to); + if (MMDB_SUCCESS != status) { + return status; } - int status = get_entry_data_list( - mmdb, array_offset, entry_data_list_to, pool, depth); + status = get_entry_data_list(mmdb, + array_offset, + entry_data_list_to, + pool, + decode_state, + depth); if (MMDB_SUCCESS != status) { DEBUG_MSG("get_entry_data_list on array element failed."); return status; @@ -1793,13 +1881,15 @@ static int get_entry_data_list(const MMDB_s *const mmdb, return MMDB_INVALID_DATA_ERROR; } while (size-- > 0) { - MMDB_entry_data_list_s *list_key = data_pool_alloc(pool); - if (!list_key) { - return MMDB_OUT_OF_MEMORY_ERROR; + MMDB_entry_data_list_s *list_key = NULL; + int status = + alloc_entry_data_list(pool, decode_state, &list_key); + if (MMDB_SUCCESS != status) { + return status; } - int status = - get_entry_data_list(mmdb, offset, list_key, pool, depth); + status = get_entry_data_list( + mmdb, offset, list_key, pool, decode_state, depth); if (MMDB_SUCCESS != status) { DEBUG_MSG("get_entry_data_list on map key failed."); return status; @@ -1807,13 +1897,14 @@ static int get_entry_data_list(const MMDB_s *const mmdb, offset = list_key->entry_data.offset_to_next; - MMDB_entry_data_list_s *list_value = data_pool_alloc(pool); - if (!list_value) { - return MMDB_OUT_OF_MEMORY_ERROR; + MMDB_entry_data_list_s *list_value = NULL; + status = alloc_entry_data_list(pool, decode_state, &list_value); + if (MMDB_SUCCESS != status) { + return status; } - status = - get_entry_data_list(mmdb, offset, list_value, pool, depth); + status = get_entry_data_list( + mmdb, offset, list_value, pool, decode_state, depth); if (MMDB_SUCCESS != status) { DEBUG_MSG("get_entry_data_list on map element failed."); return status; @@ -1826,6 +1917,26 @@ static int get_entry_data_list(const MMDB_s *const mmdb, break; } + // Charge the copied payload. Only string and bytes carry a variable-length + // payload that a caller copies. Integers are size-validated and tiny, + // floats are fixed width, and container data_size is an element count, not + // bytes. Pointers have been resolved to their target above, so a pointer to + // a string is charged here as the string. This runs once per node, so a + // fan-out that references one large value many times is charged each time. + // Check before adding so even an overridden maximum cannot make the + // uint64 counter wrap. + if (entry_data_list->entry_data.type == MMDB_DATA_TYPE_UTF8_STRING || + entry_data_list->entry_data.type == MMDB_DATA_TYPE_BYTES) { + uint64_t const maximum_bytes = (uint64_t)(MAXIMUM_DATA_STRUCTURE_BYTES); + uint64_t const data_size = entry_data_list->entry_data.data_size; + if (data_size > maximum_bytes || + decode_state->bytes > maximum_bytes - data_size) { + DEBUG_MSG("reached the maximum data structure bytes"); + return MMDB_DECODER_LIMIT_ERROR; + } + decode_state->bytes += data_size; + } + return MMDB_SUCCESS; } @@ -2286,6 +2397,9 @@ const char *MMDB_strerror(int error_code) { case MMDB_INVALID_NETWORK_ADDRESS_ERROR: return "The sockaddr family is unsupported; only AF_INET and " "AF_INET6 are accepted"; + case MMDB_DECODER_LIMIT_ERROR: + return "The decoded data structure exceeds the configured resource " + "limits"; default: return "Unknown error code"; } diff --git a/t/CMakeLists.txt b/t/CMakeLists.txt index 04627b60..bb9c23c7 100644 --- a/t/CMakeLists.txt +++ b/t/CMakeLists.txt @@ -22,6 +22,7 @@ set(TEST_TARGET_NAMES metadata_t no_map_get_value_t overflow_bounds_t + pointer_dos_t read_node_t version_t ) diff --git a/t/Makefile.am b/t/Makefile.am index 630c664c..f2c01aca 100644 --- a/t/Makefile.am +++ b/t/Makefile.am @@ -11,7 +11,8 @@ CFLAGS += -I$(top_srcdir)/src noinst_LTLIBRARIES = libmmdbtest.la libmmdbtest_la_SOURCES = maxminddb_test_helper.c maxminddb_test_helper.h -EXTRA_DIST = compile_c++_t.pl external_symbols_t.pl mmdblookup_t.pl \ +EXTRA_DIST = compile_c++_t.pl decoder_limits_t.pl external_symbols_t.pl \ + mmdblookup_t.pl \ libtap/COPYING libtap/INSTALL libtap/Makefile libtap/README.md \ libtap/tap.c libtap/tap.h maxmind-db @@ -24,7 +25,7 @@ check_PROGRAMS = \ get_value_pointer_bug_t invalid_sockaddr_t \ ipv4_start_cache_t ipv6_lookup_in_ipv4_t max_depth_t metadata_t \ metadata_marker_t metadata_pointers_t no_map_get_value_t \ - overflow_bounds_t read_node_t \ + overflow_bounds_t pointer_dos_t read_node_t \ threads_t version_t data_pool_t_LDFLAGS = $(AM_LDFLAGS) -lm @@ -32,6 +33,7 @@ data_pool_t_SOURCES = data-pool-t.c ../src/data-pool.c threads_t_CFLAGS = $(CFLAGS) -pthread -TESTS = $(check_PROGRAMS) compile_c++_t.pl external_symbols_t.pl mmdblookup_t.pl +TESTS = $(check_PROGRAMS) compile_c++_t.pl decoder_limits_t.pl \ + external_symbols_t.pl mmdblookup_t.pl LDADD = libmmdbtest.la libtap/libtap.a diff --git a/t/data-pool-t.c b/t/data-pool-t.c index 6952c035..cd9eb693 100644 --- a/t/data-pool-t.c +++ b/t/data-pool-t.c @@ -24,20 +24,30 @@ int main(void) { static void test_data_pool_new(void) { { - MMDB_data_pool_s *const pool = data_pool_new(0); + MMDB_data_pool_s *const pool = data_pool_new(0, 512); ok(!pool, "size 0 is not valid"); } { - MMDB_data_pool_s *const pool = data_pool_new(SIZE_MAX - 10); + MMDB_data_pool_s *const pool = data_pool_new(SIZE_MAX - 10, SIZE_MAX); ok(!pool, "very large size is not valid"); } { - MMDB_data_pool_s *const pool = data_pool_new(512); + MMDB_data_pool_s *const pool = data_pool_new(512, 1024); ok(pool != NULL, "size 512 is valid"); cmp_ok(pool->size, "==", 512, "size is 512"); cmp_ok(pool->used, "==", 0, "used size is 0"); + cmp_ok(pool->capacity, "==", 512, "capacity is 512"); + cmp_ok(pool->max_size, "==", 1024, "maximum size is 1024"); + data_pool_destroy(pool); + } + + { + MMDB_data_pool_s *const pool = data_pool_new(512, 10); + ok(pool != NULL, "maximum smaller than initial size is valid"); + cmp_ok(pool->size, "==", 10, "initial size is clamped to maximum"); + cmp_ok(pool->capacity, "==", 10, "capacity is clamped to maximum"); data_pool_destroy(pool); } } @@ -48,7 +58,7 @@ static void test_data_pool_destroy(void) { } { - MMDB_data_pool_s *const pool = data_pool_new(512); + MMDB_data_pool_s *const pool = data_pool_new(512, 512); ok(pool != NULL, "created pool"); data_pool_destroy(pool); } @@ -56,7 +66,7 @@ static void test_data_pool_destroy(void) { static void test_data_pool_alloc(void) { { - MMDB_data_pool_s *const pool = data_pool_new(1); + MMDB_data_pool_s *const pool = data_pool_new(1, 3); ok(pool != NULL, "created pool"); cmp_ok(pool->used, "==", 0, "used size starts at 0"); @@ -75,6 +85,12 @@ static void test_data_pool_alloc(void) { cmp_ok(pool->size, "==", 2, "size is 2 (new block)"); cmp_ok(pool->used, "==", 1, "used size is 1 in current block"); + MMDB_entry_data_list_s *const entry3 = data_pool_alloc(pool); + ok(entry3 != NULL, "got the final allowed entry"); + ok(data_pool_alloc(pool) == NULL, + "allocation past maximum capacity is rejected"); + cmp_ok(pool->capacity, "==", 3, "capacity does not exceed maximum"); + ok(entry1->entry_data.offset == 123, "accessing the original entry's memory is ok"); @@ -83,7 +99,8 @@ static void test_data_pool_alloc(void) { { size_t const initial_size = 10; - MMDB_data_pool_s *const pool = data_pool_new(initial_size); + MMDB_data_pool_s *const pool = + data_pool_new(initial_size, initial_size * 3); ok(pool != NULL, "created pool"); MMDB_entry_data_list_s *entry1 = NULL; @@ -124,12 +141,35 @@ static void test_data_pool_alloc(void) { data_pool_destroy(pool); } + + { + size_t const maximum_size = 65536; + MMDB_data_pool_s *const pool = data_pool_new(64, maximum_size); + ok(pool != NULL, "created a decoder-sized pool"); + for (size_t i = 0; i < maximum_size; i++) { + MMDB_entry_data_list_s *const entry = data_pool_alloc(pool); + assert(entry != NULL); + (void)entry; + } + cmp_ok(pool->capacity, + "==", + maximum_size, + "final block is clamped to the remaining capacity"); + cmp_ok(pool->sizes[pool->index], + "==", + 64, + "the clamped final block reserves only 64 entries"); + ok(data_pool_alloc(pool) == NULL, + "decoder-sized pool refuses a 65,537th entry"); + data_pool_destroy(pool); + } } static void test_data_pool_to_list(void) { { size_t const initial_size = 16; - MMDB_data_pool_s *const pool = data_pool_new(initial_size); + MMDB_data_pool_s *const pool = + data_pool_new(initial_size, initial_size); ok(pool != NULL, "created pool"); MMDB_entry_data_list_s *const entry1 = data_pool_alloc(pool); @@ -162,7 +202,8 @@ static void test_data_pool_to_list(void) { { size_t const initial_size = 1; - MMDB_data_pool_s *const pool = data_pool_new(initial_size); + MMDB_data_pool_s *const pool = + data_pool_new(initial_size, initial_size); ok(pool != NULL, "created pool"); MMDB_entry_data_list_s *const entry1 = data_pool_alloc(pool); @@ -180,7 +221,8 @@ static void test_data_pool_to_list(void) { { size_t const initial_size = 2; - MMDB_data_pool_s *const pool = data_pool_new(initial_size); + MMDB_data_pool_s *const pool = + data_pool_new(initial_size, initial_size); ok(pool != NULL, "created pool"); MMDB_entry_data_list_s *const entry1 = data_pool_alloc(pool); @@ -271,7 +313,11 @@ static void test_data_pool_to_list(void) { // this frequently. static bool create_and_check_list(size_t const initial_size, size_t const element_count) { - MMDB_data_pool_s *const pool = data_pool_new(initial_size); + size_t max_size = initial_size; + if (element_count > initial_size) { + max_size = element_count; + } + MMDB_data_pool_s *const pool = data_pool_new(initial_size, max_size); assert(pool != NULL); assert(pool->used == 0); diff --git a/t/decoder_limits_t.pl b/t/decoder_limits_t.pl new file mode 100755 index 00000000..3284565e --- /dev/null +++ b/t/decoder_limits_t.pl @@ -0,0 +1,266 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Cwd qw( abs_path ); +use FindBin qw( $Bin ); + +eval <<'EOF'; +use Test::More 0.88; +use File::Temp qw( tempdir ); +use IPC::Run3 qw( run3 ); +EOF + +if ($@) { + print + "1..0 # skip decoder limit override tests need Test::More 0.88, File::Temp, and IPC::Run3\n"; + exit 0; +} + +my $root = abs_path("$Bin/.."); +my $include_dir = "$root/include"; +my $src_dir = "$root/src"; +my $cc = $ENV{CC} || 'cc'; + +# The checks below rebuild the library with -Werror. Only gcc and clang are +# known to compile it cleanly with the flags used here, so skip elsewhere +# instead of failing on a missing compiler or an unrelated warning. +my ( $cc_version, $cc_stderr ) = ( q{}, q{} ); +my $cc_status = eval { + run3( [ $cc, '--version' ], \undef, \$cc_version, \$cc_stderr ); + $?; +}; +$cc_version .= $cc_stderr; +if ( !defined $cc_status + || $cc_status != 0 + || $cc_version !~ /gcc|clang|Free Software Foundation/ ) { + plan( skip_all => "decoder limit override tests need gcc or clang" ); +} + +# Keep instrumentation such as -fsanitize=address from the environment, but +# not its warning flags. Those vary by CI job and would trip -Werror below. +my @instrumentation = grep { /^-f/ } + map { split ' ' } grep { defined } @ENV{ 'CFLAGS', 'LDFLAGS' }; + +my @base = ( + $cc, + @instrumentation, + '-std=c99', + '-Wall', + '-Wextra', + '-Werror', + '-Wno-unused-function', + '-Wno-unused-parameter', + '-DPACKAGE_VERSION="test"', + "-I$include_dir", + "-I$src_dir", +); + +for my $definition ( + '-DMAXIMUM_DATA_STRUCTURE_DEPTH=1000', + '-DMAXIMUM_DATA_STRUCTURE_VALUES=1000000', +) { + my ( $status, $stderr ) = _run( + @base, + $definition, + '-fsyntax-only', + "$src_dir/maxminddb.c", + ); + is( $status, 0, "$definition compiles without warnings" ) + or diag($stderr); +} + +for my $definition ( + '-DMAXIMUM_DATA_STRUCTURE_DEPTH=0', + '-DMAXIMUM_DATA_STRUCTURE_DEPTH=-1', + '-DMAXIMUM_DATA_STRUCTURE_VALUES=0', + '-DMAXIMUM_DATA_STRUCTURE_VALUES=-1', + '-DMAXIMUM_DATA_STRUCTURE_BYTES=0', + '-DMAXIMUM_DATA_STRUCTURE_BYTES=-1', +) { + my ( $status, $stderr ) = _run( + @base, + $definition, + '-fsyntax-only', + "$src_dir/maxminddb.c", + ); + isnt( $status, 0, "$definition is rejected" ); + like( + $stderr, + qr/MAXIMUM_DATA_STRUCTURE_\w+ must be/, + "$definition explains its range" + ); +} + +my $tempdir = tempdir( CLEANUP => 1 ); +my $source = "$tempdir/override.c"; +open my $fh, '>', $source or die $!; +print {$fh} <<'EOF' or die $!; +#include +#include +#include +#include + +static int fail(const char *path, const char *what, int status, int code) { + fprintf(stderr, "%s: %s: %s\n", path, what, MMDB_strerror(status)); + return code; +} + +static int lookup(const char *path, const char *address, MMDB_s *mmdb, + MMDB_lookup_result_s *result) { + int status = MMDB_open(path, MMDB_MODE_MMAP, mmdb); + if (status != MMDB_SUCCESS) { + return fail(path, "open", status, 1); + } + int gai_error, mmdb_error; + *result = MMDB_lookup_string(mmdb, address, &gai_error, &mmdb_error); + if (gai_error != 0 || mmdb_error != MMDB_SUCCESS || !result->found_entry) { + MMDB_close(mmdb); + return fail(path, "lookup found no entry", mmdb_error, 2); + } + return 0; +} + +static int decode(const char *path, size_t expected_count) { + MMDB_s mmdb; + MMDB_lookup_result_s result; + int code = lookup(path, "1.1.1.1", &mmdb, &result); + if (code != 0) { + return code; + } + MMDB_entry_data_list_s *list = NULL; + int status = MMDB_get_entry_data_list(&result.entry, &list); + if (status != MMDB_SUCCESS) { + MMDB_close(&mmdb); + return fail(path, "full decode", status, 3); + } + size_t count = 0; + for (MMDB_entry_data_list_s *node = list; node; node = node->next) { + count++; + } + MMDB_free_entry_data_list(list); + MMDB_close(&mmdb); + if (count != expected_count) { + fprintf(stderr, "%s: decoded %zu values, expected %zu\n", path, count, + expected_count); + return 4; + } + return 0; +} + +static int +check_status(const char *path, const char *address, int expected_status) { + MMDB_s mmdb; + MMDB_lookup_result_s result; + int code = lookup(path, address, &mmdb, &result); + if (code != 0) { + return code; + } + MMDB_entry_data_list_s *list = NULL; + int status = MMDB_get_entry_data_list(&result.entry, &list); + MMDB_free_entry_data_list(list); + MMDB_close(&mmdb); + if (status != expected_status) { + return fail(path, "full decode returned an unexpected status", status, + 7); + } + return 0; +} + +int main(int argc, char **argv) { + if (argc == 3 && strcmp(argv[1], "--accept") == 0) { + return check_status(argv[2], "1.2.3.4", MMDB_SUCCESS); + } + if (argc == 2) { + return check_status( + argv[1], "1.1.1.1", MMDB_DECODER_LIMIT_ERROR); + } + if (argc != 3) { + return 5; + } + int status = decode(argv[1], 65537); + return status == 0 ? decode(argv[2], 34) : status; +} +EOF +close $fh or die $!; + +# Each override rebuilds the library with the given definitions, then runs the +# program above. Two fixtures means decode both and check the value counts; +# one fixture means expect MMDB_DECODER_LIMIT_ERROR. +my @overrides = ( + { + desc => 'limits one above the defaults', + definitions => [ + '-DMAXIMUM_DATA_STRUCTURE_VALUES=65537', + '-DMAXIMUM_DATA_STRUCTURE_BYTES=2097153', + ], + fixtures => [ + 'MaxMind-DB-test-decoder-value-limit-over.mmdb', + 'MaxMind-DB-test-decoder-payload-limit-over.mmdb', + ], + }, + { + desc => 'a 2 GiB payload limit', + definitions => ['-DMAXIMUM_DATA_STRUCTURE_BYTES=2147483648'], + fixtures => + ['MaxMind-DB-test-payload-amplification-dos-worst-case.mmdb'], + }, + { + # The fixture's first value is 65,535 bytes, one more than the limit, + # so the single-value check rejects it before any total accumulates. + desc => 'a payload limit below a single value', + definitions => ['-DMAXIMUM_DATA_STRUCTURE_BYTES=65534'], + fixtures => ['MaxMind-DB-test-payload-amplification-dos.mmdb'], + }, + { + desc => 'a raised nesting depth limit', + definitions => ['-DMAXIMUM_DATA_STRUCTURE_DEPTH=1000'], + accept => 1, + fixtures => [ + '../bad-data/libmaxminddb/libmaxminddb-deep-array-nesting.mmdb' + ], + }, +); + +my $count = 0; +for my $override (@overrides) { + my $executable = "$tempdir/override-" . $count++; + my ( $compile_status, $compile_stderr ) = _run( + @base, + @{ $override->{definitions} }, + "$src_dir/maxminddb.c", + "$src_dir/data-pool.c", + $source, + '-lm', + '-o', + $executable, + ); + is( $compile_status, 0, "$override->{desc} compiles and links" ) + or diag($compile_stderr); + next if $compile_status != 0; + + my @arguments = + map { "$Bin/maxmind-db/test-data/$_" } @{ $override->{fixtures} }; + unshift @arguments, '--accept' if $override->{accept}; + my ( $status, $stderr ) = _run( $executable, @arguments ); + is( $status, 0, "$override->{desc} takes effect at runtime" ) + or diag($stderr); +} + +done_testing(); + +sub _run { + my @command = @_; + my ( $stdout, $stderr ); + run3( \@command, \undef, \$stdout, \$stderr ); + my $wait = $?; + + # A child killed by a signal has no exit code, so report the signal as a + # failure instead of letting $? >> 8 read as success. + if ( $wait & 127 ) { + my $signal = $wait & 127; + return ( 128 + $signal, "$stderr\nkilled by signal $signal\n" ); + } + return ( $wait >> 8, $stderr ); +} diff --git a/t/fuzz_mmdb.c b/t/fuzz_mmdb.c index e9289431..b5795cc4 100644 --- a/t/fuzz_mmdb.c +++ b/t/fuzz_mmdb.c @@ -1,34 +1,60 @@ +#ifndef _POSIX_C_SOURCE + #define _POSIX_C_SOURCE 200809L +#endif + #include "maxminddb-compat-util.h" #include "maxminddb.h" +#include #include #define kMinInputLength 2 -#define kMaxInputLength 4048 +#define kMaxInputLength (256 * 1024) extern int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { int status; - FILE *fp; MMDB_s mmdb; - char filename[256]; + char filename[] = "/tmp/libfuzzer.XXXXXX"; - if (size < kMinInputLength || size > kMaxInputLength) + if (size < kMinInputLength || size > kMaxInputLength) { return 0; - - sprintf(filename, "/tmp/libfuzzer.%d", getpid()); - - fp = fopen(filename, "wb"); - if (!fp) - return 0; - - fwrite(data, size, sizeof(uint8_t), fp); - fclose(fp); + } + + int const fd = mkstemp(filename); + if (fd == -1) { + abort(); + } + FILE *const fp = fdopen(fd, "wb"); + if (!fp) { + close(fd); + unlink(filename); + abort(); + } + + size_t const written = fwrite(data, sizeof(uint8_t), size, fp); + int const close_status = fclose(fp); + if (written != size || close_status != 0) { + unlink(filename); + abort(); + } status = MMDB_open(filename, MMDB_MODE_MMAP, &mmdb); - if (status == MMDB_SUCCESS) + if (status == MMDB_SUCCESS) { + int gai_error, mmdb_error; + MMDB_lookup_result_s result = + MMDB_lookup_string(&mmdb, "1.1.1.1", &gai_error, &mmdb_error); + if (gai_error == 0 && mmdb_error == MMDB_SUCCESS && + result.found_entry) { + MMDB_entry_data_list_s *entry_data_list = NULL; + MMDB_get_entry_data_list(&result.entry, &entry_data_list); + MMDB_free_entry_data_list(entry_data_list); + } MMDB_close(&mmdb); + } - unlink(filename); + if (unlink(filename) != 0) { + abort(); + } return 0; } diff --git a/t/max_depth_t.c b/t/max_depth_t.c index 7511f3e7..14ea0fc7 100644 --- a/t/max_depth_t.c +++ b/t/max_depth_t.c @@ -23,14 +23,14 @@ void test_deep_nesting_rejected(void) { if (result.found_entry) { /* Looking up non-existent key "z" forces skip_map_or_array to * recurse through all 600 nesting levels. With the depth limit, - * this should return MMDB_INVALID_DATA_ERROR instead of crashing. */ + * this should return MMDB_DECODER_LIMIT_ERROR instead of crashing. */ MMDB_entry_data_s entry_data; const char *lookup_path[] = {"z", NULL}; status = MMDB_aget_value(&result.entry, &entry_data, lookup_path); cmp_ok(status, "==", - MMDB_INVALID_DATA_ERROR, - "MMDB_aget_value returns MMDB_INVALID_DATA_ERROR for " + MMDB_DECODER_LIMIT_ERROR, + "MMDB_aget_value returns MMDB_DECODER_LIMIT_ERROR for " "deeply nested data exceeding max depth"); } @@ -98,8 +98,8 @@ void test_deep_array_nesting_rejected(void) { status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); cmp_ok(status, "==", - MMDB_INVALID_DATA_ERROR, - "MMDB_get_entry_data_list returns MMDB_INVALID_DATA_ERROR " + MMDB_DECODER_LIMIT_ERROR, + "MMDB_get_entry_data_list returns MMDB_DECODER_LIMIT_ERROR " "for deeply nested arrays exceeding max depth"); MMDB_free_entry_data_list(entry_data_list); } diff --git a/t/maxmind-db b/t/maxmind-db index e7b00186..363086b7 160000 --- a/t/maxmind-db +++ b/t/maxmind-db @@ -1 +1 @@ -Subproject commit e7b0018644317ad6f33eb408f4479ccc4ab0e6fd +Subproject commit 363086b7d90650100e91f954937794c6a090c2a0 diff --git a/t/pointer_dos_t.c b/t/pointer_dos_t.c new file mode 100644 index 00000000..2d77e7c1 --- /dev/null +++ b/t/pointer_dos_t.c @@ -0,0 +1,238 @@ +#include "maxminddb_test_helper.h" + +static void test_record_rejected(const char *fixture, + const char *address, + const char *desc) { + char *path = test_database_path(fixture); + MMDB_s *mmdb = open_ok(path, MMDB_MODE_MMAP, desc); + free(path); + if (!mmdb) { + return; + } + + MMDB_lookup_result_s result = + lookup_string_ok(mmdb, address, fixture, desc); + ok(result.found_entry, "%s: entry found", desc); + if (result.found_entry) { + MMDB_entry_data_list_s *entry_data_list = NULL; + int const status = + MMDB_get_entry_data_list(&result.entry, &entry_data_list); + cmp_ok(status, + "==", + MMDB_DECODER_LIMIT_ERROR, + "%s: full decode returns MMDB_DECODER_LIMIT_ERROR", + desc); + ok(entry_data_list == NULL, + "%s: error leaves the output list set to NULL", + desc); + MMDB_free_entry_data_list(entry_data_list); + } + + MMDB_close(mmdb); + free(mmdb); +} + +static void test_record_allowed(const char *fixture, + const char *address, + size_t expected_values, + uint64_t expected_payload, + const char *desc) { + char *path = test_database_path(fixture); + MMDB_s *mmdb = open_ok(path, MMDB_MODE_MMAP, desc); + free(path); + if (!mmdb) { + return; + } + + MMDB_lookup_result_s result = + lookup_string_ok(mmdb, address, fixture, desc); + ok(result.found_entry, "%s: entry found", desc); + if (result.found_entry) { + MMDB_entry_data_list_s *entry_data_list = NULL; + int const status = + MMDB_get_entry_data_list(&result.entry, &entry_data_list); + cmp_ok(status, "==", MMDB_SUCCESS, "%s: full decode succeeds", desc); + ok(entry_data_list != NULL, "%s: full decode returns a list", desc); + + size_t values = 0; + uint64_t payload = 0; + for (MMDB_entry_data_list_s *node = entry_data_list; node; + node = node->next) { + values++; + if (node->entry_data.type == MMDB_DATA_TYPE_UTF8_STRING || + node->entry_data.type == MMDB_DATA_TYPE_BYTES) { + payload += node->entry_data.data_size; + } + } + cmp_ok(values, + "==", + expected_values, + "%s: decoded the expected number of values", + desc); + cmp_ok(payload, + "==", + expected_payload, + "%s: decoded the expected payload bytes", + desc); + MMDB_free_entry_data_list(entry_data_list); + } + + MMDB_close(mmdb); + free(mmdb); +} + +static void test_per_call_state(void) { + const char *fixture = "MaxMind-DB-test-payload-amplification-dos.mmdb"; + char *path = test_database_path(fixture); + MMDB_s *mmdb = open_ok(path, MMDB_MODE_MMAP, "per-call state"); + free(path); + if (!mmdb) { + return; + } + + MMDB_lookup_result_s result = + lookup_string_ok(mmdb, "1.1.1.1", fixture, "per-call state"); + ok(result.found_entry, "per-call state: entry found"); + if (result.found_entry) { + for (int i = 1; i <= 2; i++) { + MMDB_entry_data_list_s *list = NULL; + int const status = MMDB_get_entry_data_list(&result.entry, &list); + cmp_ok(status, + "==", + MMDB_DECODER_LIMIT_ERROR, + "per-call: attack decode %d is rejected", + i); + ok(list == NULL, + "per-call: attack decode %d leaves a NULL list", + i); + MMDB_free_entry_data_list(list); + } + + MMDB_entry_data_list_s *metadata = NULL; + int const status = + MMDB_get_metadata_as_entry_data_list(mmdb, &metadata); + cmp_ok(status, + "==", + MMDB_SUCCESS, + "per-call: metadata decode on the same reader succeeds"); + ok(metadata != NULL, + "per-call: metadata decode on the same reader returns a list"); + MMDB_free_entry_data_list(metadata); + } + + MMDB_close(mmdb); + free(mmdb); +} + +static void test_targeted_lookup_bypasses_full_decode_limit(void) { + const char *fixture = "MaxMind-DB-test-decoder-payload-limit-over.mmdb"; + const char *desc = "targeted oversized lookup"; + char *path = test_database_path(fixture); + MMDB_s *mmdb = open_ok(path, MMDB_MODE_MMAP, desc); + free(path); + if (!mmdb) { + return; + } + + MMDB_lookup_result_s result = + lookup_string_ok(mmdb, "1.1.1.1", fixture, desc); + ok(result.found_entry, "%s: entry found", desc); + if (result.found_entry) { + MMDB_entry_data_s entry_data; + int const status = + MMDB_get_value(&result.entry, &entry_data, "0", NULL); + cmp_ok(status, + "==", + MMDB_SUCCESS, + "targeted lookup succeeds without expanding the structure"); + if (status == MMDB_SUCCESS) { + ok(entry_data.has_data, "targeted lookup returns data"); + cmp_ok(entry_data.type, + "==", + MMDB_DATA_TYPE_BYTES, + "targeted lookup returns the bytes value"); + cmp_ok(entry_data.data_size, + "==", + 65535, + "targeted lookup returns the complete bytes value"); + } + } + + MMDB_close(mmdb); + free(mmdb); +} + +static void test_metadata_limit_error(void) { + char *db_file = + test_database_path("MaxMind-DB-test-metadata-payload-limit.mmdb"); + MMDB_s mmdb; + int const status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb); + cmp_ok(status, + "==", + MMDB_INVALID_METADATA_ERROR, + "metadata decoder limit is reported as invalid metadata by open"); + if (status == MMDB_SUCCESS) { + MMDB_close(&mmdb); + } + free(db_file); +} + +int main(void) { + plan(NO_PLAN); + + is(MMDB_strerror(MMDB_DECODER_LIMIT_ERROR), + "The decoded data structure exceeds the configured resource limits", + "decoder limit status has a distinct error message"); + + test_record_rejected("MaxMind-DB-test-pointer-decoder-dos.mmdb", + "1.1.1.1", + "IPv4 value-count fan-out"); + test_record_rejected("MaxMind-DB-test-pointer-decoder-dos-ipv6.mmdb", + "2001:db8::1", + "IPv6 value-count fan-out"); + test_record_rejected("MaxMind-DB-test-payload-amplification-dos.mmdb", + "1.1.1.1", + "bytes payload amplification"); + test_record_rejected( + "MaxMind-DB-test-payload-amplification-dos-worst-case.mmdb", + "1.1.1.1", + "worst-case bytes payload amplification"); + test_record_rejected( + "MaxMind-DB-test-payload-amplification-dos-string.mmdb", + "1.1.1.1", + "string payload amplification"); + + test_record_allowed("MaxMind-DB-test-decoder-value-limit.mmdb", + "1.1.1.1", + 65536, + 0, + "exact value-count limit"); + test_record_allowed( + "MaxMind-DB-test-decoder-value-limit-pointer-heavy.mmdb", + "1.1.1.1", + 65535, + 0, + "pointer-heavy record under the value-count limit"); + test_record_rejected("MaxMind-DB-test-decoder-value-limit-over.mmdb", + "1.1.1.1", + "one over the value-count limit"); + test_record_allowed("MaxMind-DB-test-decoder-payload-limit.mmdb", + "1.1.1.1", + 34, + 2097152, + "exact payload-byte limit"); + test_record_rejected("MaxMind-DB-test-decoder-payload-limit-over.mmdb", + "1.1.1.1", + "one over the payload-byte limit"); + + test_record_allowed("GeoIP2-City-Test.mmdb", + "81.2.69.142", + 120, + 679, + "normal production-style record"); + test_per_call_state(); + test_targeted_lookup_bypasses_full_decode_limit(); + test_metadata_limit_error(); + + done_testing(); +}