Skip to content

Commit 996b88c

Browse files
authored
Merge pull request #431 from maxmind/greg/fix-findings
Fix several minor safety issues
2 parents 47e3a40 + 5f40c27 commit 996b88c

10 files changed

Lines changed: 200 additions & 11 deletions

File tree

Changes.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
## next release
2+
3+
- Fixed an out-of-bounds read in `MMDB_lookup_sockaddr()` when callers passed a
4+
`sockaddr` with an unsupported address family. The function now rejects any
5+
family other than `AF_INET` and `AF_INET6` with
6+
`MMDB_INVALID_NETWORK_ADDRESS_ERROR`.
7+
- Fixed metadata parsing for files that end immediately after the
8+
`\xAB\xCD\xEFMaxMind.com` marker. Such files are now rejected as invalid
9+
metadata instead of allowing a zero-length metadata section to reach the
10+
decoder.
11+
- Fixed search-tree validation for records that point into the 16-byte separator
12+
before the data section. These records are now rejected as corrupt instead of
13+
being exposed as apparent data entries with underflowed offsets.
14+
- `MMDB_read_node()` now returns `MMDB_CORRUPT_SEARCH_TREE_ERROR` instead of
15+
`MMDB_SUCCESS` with `MMDB_RECORD_TYPE_INVALID` record types when a node's
16+
child record is invalid.
17+
118
## 1.13.3 - 2026-03-05
219

320
- Fixed validation of empty maps and arrays at the end of the metadata section.

doc/libmaxminddb.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ status codes are:
393393
array index larger than an array or smaller than the minimum offset from the
394394
end of an array. It can also happen when the path expects to find a map or
395395
array where none exist.
396+
- `MMDB_INVALID_NETWORK_ADDRESS_ERROR` - `MMDB_lookup_sockaddr()` was given a
397+
`sockaddr` whose family is neither `AF_INET` nor `AF_INET6`.
396398

397399
All status codes should be treated as `int` values.
398400

@@ -518,7 +520,8 @@ MMDB_lookup_result_s MMDB_lookup_sockaddr(
518520
```
519521
520522
This function looks up an IP address that has already been resolved by
521-
`getaddrinfo()`.
523+
`getaddrinfo()`. The `sockaddr` passed to this function must be an `AF_INET` or
524+
`AF_INET6` address.
522525
523526
Other than not calling `getaddrinfo()` itself, this function is identical to the
524527
`MMDB_lookup_string()` function.
@@ -770,7 +773,11 @@ to an `MMDB_search_node_s` structure that will be populated by this function.
770773
771774
The return value is a status code. If you pass a `node_number` that is greater
772775
than or equal to the number of nodes in the database, this function will return
773-
`MMDB_INVALID_NODE_NUMBER_ERROR`, otherwise it will return `MMDB_SUCCESS`.
776+
`MMDB_INVALID_NODE_NUMBER_ERROR`. If the node's child records are invalid or
777+
point outside the search tree or data section, it will return
778+
`MMDB_CORRUPT_SEARCH_TREE_ERROR`. Otherwise it will return `MMDB_SUCCESS`. On
779+
any non-`MMDB_SUCCESS` return the contents of `*node` are unspecified and must
780+
not be read.
774781
775782
The first node in the search tree is always node 0. If you wanted to iterate
776783
over the whole search tree, you would start by reading node 0 and then following

include/maxminddb.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ extern "C" {
8686
#define MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR (9)
8787
#define MMDB_INVALID_NODE_NUMBER_ERROR (10)
8888
#define MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR (11)
89+
#define MMDB_INVALID_NETWORK_ADDRESS_ERROR (12)
8990

9091
#if !(MMDB_UINT128_IS_BYTE_ARRAY)
9192
#if MMDB_UINT128_USING_MODE

src/maxminddb.c

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ static const uint8_t *find_metadata(const uint8_t *file_content,
518518
}
519519
} while (NULL != tmp);
520520

521-
if (search_area == start) {
521+
if (search_area == start || max_size <= 0) {
522522
return NULL;
523523
}
524524

@@ -926,23 +926,33 @@ MMDB_lookup_result_s MMDB_lookup_sockaddr(const MMDB_s *const mmdb,
926926

927927
uint8_t mapped_address[16];
928928
uint8_t const *address;
929+
// Reject families other than AF_INET/AF_INET6 before casting to
930+
// sockaddr_in/sockaddr_in6, which would otherwise read past the
931+
// truncated struct sockaddr the caller passed in.
929932
if (mmdb->metadata.ip_version == 4) {
930933
if (sockaddr->sa_family == AF_INET6) {
931934
*mmdb_error = MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR;
932935
return result;
933936
}
937+
if (sockaddr->sa_family != AF_INET) {
938+
*mmdb_error = MMDB_INVALID_NETWORK_ADDRESS_ERROR;
939+
return result;
940+
}
934941
address = (uint8_t const *)&((struct sockaddr_in const *)sockaddr)
935942
->sin_addr.s_addr;
936943
} else {
937944
if (sockaddr->sa_family == AF_INET6) {
938945
address = (uint8_t const *)&((struct sockaddr_in6 const *)sockaddr)
939946
->sin6_addr.s6_addr;
940-
} else {
947+
} else if (sockaddr->sa_family == AF_INET) {
941948
address = mapped_address;
942949
memset(mapped_address, 0, 12);
943950
memcpy(mapped_address + 12,
944951
&((struct sockaddr_in const *)sockaddr)->sin_addr.s_addr,
945952
4);
953+
} else {
954+
*mmdb_error = MMDB_INVALID_NETWORK_ADDRESS_ERROR;
955+
return result;
946956
}
947957
}
948958

@@ -990,12 +1000,12 @@ static int find_address_in_search_tree(const MMDB_s *const mmdb,
9901000

9911001
result->netmask = current_bit;
9921002

993-
if (value >= (uint64_t)node_count + mmdb->data_section_size) {
994-
// The pointer points off the end of the database.
1003+
uint8_t type = record_type(mmdb, value);
1004+
if (type == MMDB_RECORD_TYPE_INVALID) {
9951005
return MMDB_CORRUPT_SEARCH_TREE_ERROR;
9961006
}
9971007

998-
if (value == node_count) {
1008+
if (type == MMDB_RECORD_TYPE_EMPTY) {
9991009
// record is empty
10001010
result->found_entry = false;
10011011
return MMDB_SUCCESS;
@@ -1083,7 +1093,14 @@ static uint8_t record_type(const MMDB_s *const mmdb, uint64_t record) {
10831093
return MMDB_RECORD_TYPE_EMPTY;
10841094
}
10851095

1086-
if (record - node_count < mmdb->data_section_size) {
1096+
uint64_t data_offset = record - node_count;
1097+
if (data_offset < MMDB_DATA_SECTION_SEPARATOR) {
1098+
DEBUG_MSG("record points into the data section separator");
1099+
return MMDB_RECORD_TYPE_INVALID;
1100+
}
1101+
1102+
data_offset -= MMDB_DATA_SECTION_SEPARATOR;
1103+
if (data_offset < mmdb->data_section_size) {
10871104
return MMDB_RECORD_TYPE_DATA;
10881105
}
10891106

@@ -1126,6 +1143,11 @@ int MMDB_read_node(const MMDB_s *const mmdb,
11261143
node->left_record_type = record_type(mmdb, node->left_record);
11271144
node->right_record_type = record_type(mmdb, node->right_record);
11281145

1146+
if (node->left_record_type == MMDB_RECORD_TYPE_INVALID ||
1147+
node->right_record_type == MMDB_RECORD_TYPE_INVALID) {
1148+
return MMDB_CORRUPT_SEARCH_TREE_ERROR;
1149+
}
1150+
11291151
// Note that offset will be invalid if the record type is not
11301152
// MMDB_RECORD_TYPE_DATA, but that's ok. Any use of the record entry
11311153
// for other data types is a programming error.
@@ -1421,6 +1443,13 @@ static int decode_one(const MMDB_s *const mmdb,
14211443
MMDB_entry_data_s *entry_data) {
14221444
const uint8_t *mem = mmdb->data_section;
14231445

1446+
if (mmdb->data_section_size == 0) {
1447+
// decode_one is also called with a fake mmdb whose data_section
1448+
// points at the metadata; either way an empty section is invalid.
1449+
DEBUG_MSG("decode_one called with an empty section");
1450+
return MMDB_INVALID_DATA_ERROR;
1451+
}
1452+
14241453
// We subtract rather than add as it possible that offset + 1
14251454
// could overflow for a corrupt database while an underflow
14261455
// from data_section_size - 1 should not be possible.
@@ -2254,6 +2283,9 @@ const char *MMDB_strerror(int error_code) {
22542283
case MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR:
22552284
return "You attempted to look up an IPv6 address in an IPv4-only "
22562285
"database";
2286+
case MMDB_INVALID_NETWORK_ADDRESS_ERROR:
2287+
return "The sockaddr family is unsupported; only AF_INET and "
2288+
"AF_INET6 are accepted";
22572289
default:
22582290
return "Unknown error code";
22592291
}

t/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ set(TEST_TARGET_NAMES
1717
get_value_t
1818
ipv4_start_cache_t
1919
ipv6_lookup_in_ipv4_t
20+
metadata_marker_t
2021
metadata_pointers_t
2122
metadata_t
2223
no_map_get_value_t
@@ -32,6 +33,7 @@ if(UNIX) # or if (NOT WIN32)
3233
bad_epoch_t
3334
bad_indent_t
3435
empty_container_metadata_t
36+
invalid_sockaddr_t
3537
max_depth_t
3638
threads_t
3739
)

t/Makefile.am

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ check_PROGRAMS = \
2121
basic_lookup_t data_entry_list_t \
2222
data-pool-t data_types_t double_close_t dump_t empty_container_metadata_t \
2323
gai_error_t get_value_t \
24-
get_value_pointer_bug_t \
24+
get_value_pointer_bug_t invalid_sockaddr_t \
2525
ipv4_start_cache_t ipv6_lookup_in_ipv4_t max_depth_t metadata_t \
26-
metadata_pointers_t no_map_get_value_t overflow_bounds_t read_node_t \
26+
metadata_marker_t metadata_pointers_t no_map_get_value_t \
27+
overflow_bounds_t read_node_t \
2728
threads_t version_t
2829

2930
data_pool_t_LDFLAGS = $(AM_LDFLAGS) -lm

t/bad_search_tree_t.c

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,63 @@ void test_read_node_bounds(void) {
4141
free(mmdb);
4242
}
4343

44+
// Open a pre-built corruption fixture and assert that all rejection paths
45+
// fire. `lookup_ip` must traverse the corrupted record from node 0 — high
46+
// bit 0 for a corrupted left record, 1 for a corrupted right record.
47+
static void check_corrupt_record(const char *label,
48+
const char *fixture,
49+
const char *lookup_ip) {
50+
char *db_file = bad_database_path(fixture);
51+
MMDB_s mmdb;
52+
int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb);
53+
cmp_ok(status, "==", MMDB_SUCCESS, "%s: opened crafted bad MMDB", label);
54+
if (status != MMDB_SUCCESS) {
55+
free(db_file);
56+
return;
57+
}
58+
59+
MMDB_search_node_s node;
60+
status = MMDB_read_node(&mmdb, 0, &node);
61+
cmp_ok(status,
62+
"==",
63+
MMDB_CORRUPT_SEARCH_TREE_ERROR,
64+
"%s: MMDB_read_node rejects record as corrupt",
65+
label);
66+
67+
int gai_error = 0;
68+
int mmdb_error = 0;
69+
MMDB_lookup_result_s result =
70+
MMDB_lookup_string(&mmdb, lookup_ip, &gai_error, &mmdb_error);
71+
cmp_ok(gai_error, "==", 0, "%s: lookup string parse succeeds", label);
72+
cmp_ok(mmdb_error,
73+
"==",
74+
MMDB_CORRUPT_SEARCH_TREE_ERROR,
75+
"%s: MMDB_lookup_string rejects record",
76+
label);
77+
ok(!result.found_entry, "%s: lookup reports no entry", label);
78+
79+
MMDB_close(&mmdb);
80+
free(db_file);
81+
}
82+
83+
void test_separator_record_rejected(void) {
84+
// Records in the half-open range [node_count + 1, node_count + 16) point
85+
// into the 16-byte separator between the search tree and data section
86+
// and must be rejected as corrupt.
87+
check_corrupt_record("left record, first separator byte",
88+
"libmaxminddb-separator-record-min-left.mmdb",
89+
"1.1.1.1");
90+
check_corrupt_record("right record, first separator byte",
91+
"libmaxminddb-separator-record-min-right.mmdb",
92+
"128.0.0.0");
93+
check_corrupt_record("left record, last separator byte",
94+
"libmaxminddb-separator-record-max-left.mmdb",
95+
"1.1.1.1");
96+
}
97+
4498
int main(void) {
4599
plan(NO_PLAN);
46100
test_read_node_bounds();
101+
test_separator_record_rejected();
47102
done_testing();
48103
}

t/invalid_sockaddr_t.c

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#include "maxminddb_test_helper.h"
2+
3+
static void test_invalid_sockaddr_family(const char *filename,
4+
sa_family_t family,
5+
const char *open_msg,
6+
const char *family_msg) {
7+
char *db_file = test_database_path(filename);
8+
MMDB_s *mmdb = open_ok(db_file, MMDB_MODE_MMAP, open_msg);
9+
free(db_file);
10+
11+
if (!mmdb) {
12+
return;
13+
}
14+
15+
struct sockaddr addr = {.sa_family = family};
16+
int mmdb_error = MMDB_SUCCESS;
17+
MMDB_lookup_result_s result =
18+
MMDB_lookup_sockaddr(mmdb, &addr, &mmdb_error);
19+
20+
ok(!result.found_entry, "%s: no entry returned", family_msg);
21+
cmp_ok(result.netmask, "==", 0, "%s: netmask left at zero", family_msg);
22+
cmp_ok(mmdb_error,
23+
"==",
24+
MMDB_INVALID_NETWORK_ADDRESS_ERROR,
25+
"%s: MMDB_lookup_sockaddr rejects unsupported family",
26+
family_msg);
27+
28+
MMDB_close(mmdb);
29+
free(mmdb);
30+
}
31+
32+
int main(void) {
33+
plan(NO_PLAN);
34+
test_invalid_sockaddr_family("MaxMind-DB-test-ipv4-24.mmdb",
35+
AF_UNIX,
36+
"opened IPv4 test database (AF_UNIX)",
37+
"AF_UNIX against IPv4 db");
38+
test_invalid_sockaddr_family("MaxMind-DB-test-ipv4-24.mmdb",
39+
AF_UNSPEC,
40+
"opened IPv4 test database (AF_UNSPEC)",
41+
"AF_UNSPEC against IPv4 db");
42+
test_invalid_sockaddr_family("MaxMind-DB-test-ipv6-24.mmdb",
43+
AF_UNIX,
44+
"opened IPv6 test database (AF_UNIX)",
45+
"AF_UNIX against IPv6 db");
46+
test_invalid_sockaddr_family("MaxMind-DB-test-ipv6-24.mmdb",
47+
AF_UNSPEC,
48+
"opened IPv6 test database (AF_UNSPEC)",
49+
"AF_UNSPEC against IPv6 db");
50+
done_testing();
51+
}

t/metadata_marker_t.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#include "maxminddb_test_helper.h"
2+
3+
static void test_trailing_metadata_marker(void) {
4+
char *db_file = bad_database_path("libmaxminddb-metadata-marker-only.mmdb");
5+
MMDB_s mmdb;
6+
int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb);
7+
cmp_ok(status,
8+
"==",
9+
MMDB_INVALID_METADATA_ERROR,
10+
"MMDB_open rejects a file containing only the metadata marker");
11+
12+
if (status == MMDB_SUCCESS) {
13+
MMDB_close(&mmdb);
14+
}
15+
16+
free(db_file);
17+
}
18+
19+
int main(void) {
20+
plan(NO_PLAN);
21+
test_trailing_metadata_marker();
22+
done_testing();
23+
}

0 commit comments

Comments
 (0)