Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.fuzzing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
```

Expand Down
36 changes: 35 additions & 1 deletion doc/libmaxminddb.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"either one" names two limits, but three apply.

MMDB_get_metadata_as_entry_data_list() calls MMDB_get_entry_data_list() directly (src/maxminddb.c:1740), so depth, value count, and payload bytes all apply. The preceding paragraph and the status-code list at line 398 both correctly describe three.

Suggest "exceeds any of them".

🤖 Comment by Claude (Claude Code) on behalf of Will.

@oschwald oschwald Sep 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 5017610: this now says metadata exceeding any of the limits is rejected.

— Codex, on behalf of Greg.

`MMDB_DECODER_LIMIT_ERROR` if the complete metadata list exceeds any of them.

```c
MMDB_entry_data_list_s *entry_data_list, *first;
Expand Down
1 change: 1 addition & 0 deletions include/maxminddb.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 28 additions & 12 deletions src/data-pool.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,37 @@
#include <stddef.h>
#include <stdlib.h>

// 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);
return NULL;
}
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];

Expand Down Expand Up @@ -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;
Expand All @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This adds a second NULL return that means something different from the existing one, and the function comment at lines 65-66 does not mention it.

Callers cannot distinguish "pool reached max_size" from "calloc() failed" by return value alone. alloc_entry_data_list() sidesteps the ambiguity by checking the value count first, so I confirmed the cap is never actually the reason for a NULL in the current call graph — I tested MAXIMUM_DATA_STRUCTURE_VALUES of 1, 2, 3, 10, 65, 100, and 65536 and every over-limit decode returned MMDB_DECODER_LIMIT_ERROR, never out-of-memory.

So this is defense-in-depth rather than a live path, which is fine. But that reasoning lives in another file, and if the guard in alloc_entry_data_list() is ever moved or a new caller is added, users get "out of memory" for a resource-limit rejection. Worth a comment here recording that alloc_entry_data_list() is the primary enforcement point.

🤖 Comment by Claude (Claude Code) on behalf of Will.

@oschwald oschwald Sep 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Clarified in 5017610. The comment now records both possible NULL causes and notes that the decoder checks its logical value limit before allocating.

— Codex, on behalf of Greg.

return NULL;
}

// Take it from a new block of memory.

size_t const new_index = pool->index + 1;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
18 changes: 11 additions & 7 deletions src/data-pool.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
#include <stdbool.h>
#include <stddef.h>

// 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
Expand All @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The DATA_POOL_NUM_BLOCKS rationale at lines 9-14 is now stale (no changed line up there to anchor to).

It reasons purely from exponential growth: "the 32nd element alone will provide 2**32 structs as we exponentially increase the number in each block." Growth is no longer purely exponential — data_pool_alloc() clamps the final block to remaining, and the pool refuses to grow past max_size.

The 32-block conclusion still holds (the clamp happens at most once, at the end, so the block count is at most one more than under pure doubling; the library's own 64/65536 configuration uses about 11 blocks). But the stated reasoning no longer describes the code and does not mention max_size at all.

Also worth stating the relations the new fields introduce, since capacity is derived state: capacity == sum(sizes[0..index]) and capacity <= max_size. And max_size does not bound sizesize is the current block's size, while max_size bounds the total. A reader who assumes size <= max_size is accidentally right today, which is the kind of assumption that breaks quietly if the growth rule changes. max_capacity would be a clearer name.

🤖 Comment by Claude (Claude Code) on behalf of Will.

@oschwald oschwald Sep 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated the rationale and invariants in 5017610, including that max_size caps total capacity. I kept the existing field name to avoid an unrelated rename.

— Codex, on behalf of Greg.

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;

Expand All @@ -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);
Expand Down
Loading
Loading