Skip to content

Commit e124a04

Browse files
oschwaldclaude
andcommitted
Validate array/map size in get_entry_data_list against remaining data
A crafted database could claim millions of array/map elements while only having a few bytes of data, causing disproportionate memory allocation (~40x amplification). Now validate that the claimed element count does not exceed the remaining data section bytes before entering the allocation loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e104b0a commit e124a04

4 files changed

Lines changed: 365 additions & 2 deletions

File tree

Changes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## 1.13.0
22

3+
* `MMDB_get_entry_data_list()` now validates that the claimed array/map
4+
size is plausible given the remaining bytes in the data section. A
5+
crafted database could previously claim millions of array elements
6+
while only having a few bytes of data, causing disproportionate memory
7+
allocation (memory amplification DoS).
38
* On Windows, `GetFileSize()` was replaced with `GetFileSizeEx()` to
49
correctly handle files larger than 4GB. The previous code passed
510
`NULL` for the high DWORD, discarding the upper 32 bits of the file

src/maxminddb.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,10 @@ static int get_entry_data_list(const MMDB_s *const mmdb,
17241724
case MMDB_DATA_TYPE_ARRAY: {
17251725
uint32_t array_size = entry_data_list->entry_data.data_size;
17261726
uint32_t array_offset = entry_data_list->entry_data.offset_to_next;
1727+
if (array_offset >= mmdb->data_section_size ||
1728+
array_size > mmdb->data_section_size - array_offset) {
1729+
return MMDB_INVALID_DATA_ERROR;
1730+
}
17271731
while (array_size-- > 0) {
17281732
MMDB_entry_data_list_s *entry_data_list_to =
17291733
data_pool_alloc(pool);
@@ -1747,6 +1751,11 @@ static int get_entry_data_list(const MMDB_s *const mmdb,
17471751
uint32_t size = entry_data_list->entry_data.data_size;
17481752

17491753
offset = entry_data_list->entry_data.offset_to_next;
1754+
/* Each map entry needs at least a key and a value (1 byte each). */
1755+
if (offset >= mmdb->data_section_size ||
1756+
size > (mmdb->data_section_size - offset) / 2) {
1757+
return MMDB_INVALID_DATA_ERROR;
1758+
}
17501759
while (size-- > 0) {
17511760
MMDB_entry_data_list_s *list_key = data_pool_alloc(pool);
17521761
if (!list_key) {

t/Makefile.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ EXTRA_DIST = compile_c++_t.pl external_symbols_t.pl mmdblookup_t.pl \
1616
libtap/tap.c libtap/tap.h maxmind-db
1717

1818
check_PROGRAMS = \
19-
bad_pointers_t bad_databases_t bad_epoch_t bad_indent_t basic_lookup_t \
20-
data_entry_list_t \
19+
bad_pointers_t bad_databases_t bad_data_size_t bad_epoch_t bad_indent_t \
20+
basic_lookup_t data_entry_list_t \
2121
data-pool-t data_types_t double_close_t dump_t gai_error_t get_value_t \
2222
get_value_pointer_bug_t \
2323
ipv4_start_cache_t ipv6_lookup_in_ipv4_t max_depth_t metadata_t \

t/bad_data_size_t.c

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
#include "maxminddb_test_helper.h"
2+
#include <stdlib.h>
3+
4+
/* MMDB binary format constants */
5+
#define METADATA_MARKER "\xab\xcd\xefMaxMind.com"
6+
#define METADATA_MARKER_LEN 14
7+
#define DATA_SEPARATOR_SIZE 16
8+
9+
static int write_map(uint8_t *buf, uint32_t size) {
10+
buf[0] = (7 << 5) | (size & 0x1f);
11+
return 1;
12+
}
13+
14+
static int write_string(uint8_t *buf, const char *str, uint32_t len) {
15+
buf[0] = (2 << 5) | (len & 0x1f);
16+
memcpy(buf + 1, str, len);
17+
return 1 + len;
18+
}
19+
20+
static int write_uint16(uint8_t *buf, uint16_t value) {
21+
buf[0] = (5 << 5) | 2;
22+
buf[1] = (value >> 8) & 0xff;
23+
buf[2] = value & 0xff;
24+
return 3;
25+
}
26+
27+
static int write_uint32(uint8_t *buf, uint32_t value) {
28+
buf[0] = (6 << 5) | 4;
29+
buf[1] = (value >> 24) & 0xff;
30+
buf[2] = (value >> 16) & 0xff;
31+
buf[3] = (value >> 8) & 0xff;
32+
buf[4] = value & 0xff;
33+
return 5;
34+
}
35+
36+
static int write_uint64(uint8_t *buf, uint64_t value) {
37+
buf[0] = (0 << 5) | 8;
38+
buf[1] = 2; /* extended type: 7 + 2 = 9 (uint64) */
39+
buf[2] = (value >> 56) & 0xff;
40+
buf[3] = (value >> 48) & 0xff;
41+
buf[4] = (value >> 40) & 0xff;
42+
buf[5] = (value >> 32) & 0xff;
43+
buf[6] = (value >> 24) & 0xff;
44+
buf[7] = (value >> 16) & 0xff;
45+
buf[8] = (value >> 8) & 0xff;
46+
buf[9] = value & 0xff;
47+
return 10;
48+
}
49+
50+
static int write_meta_key(uint8_t *buf, const char *key) {
51+
return write_string(buf, key, strlen(key));
52+
}
53+
54+
/*
55+
* Write a map control byte with a large size using case-31 encoding.
56+
* Type 7 (map) fits in the base types: control byte = (7 << 5) | size_marker.
57+
* For case-31 size encoding: control byte size bits = 31,
58+
* then 3 bytes for (size - 65821).
59+
*/
60+
static int write_large_map(uint8_t *buf, uint32_t size) {
61+
uint32_t adjusted = size - 65821;
62+
buf[0] = (7 << 5) | 31; /* type 7 (map), size = case 31 */
63+
buf[1] = (adjusted >> 16) & 0xff;
64+
buf[2] = (adjusted >> 8) & 0xff;
65+
buf[3] = adjusted & 0xff;
66+
return 4;
67+
}
68+
69+
/*
70+
* Write an array control byte with a large size using case-31 encoding.
71+
* Type 11 (array) is extended: control byte = (0 << 5) | size_marker,
72+
* followed by extended type byte 4.
73+
* For case-31 size encoding: control byte size bits = 31,
74+
* then 3 bytes for (size - 65821).
75+
*/
76+
static int write_large_array(uint8_t *buf, uint32_t size) {
77+
uint32_t adjusted = size - 65821;
78+
buf[0] = (0 << 5) | 31; /* extended type, size = case 31 */
79+
buf[1] = 4; /* extended type: 7 + 4 = 11 (array) */
80+
buf[2] = (adjusted >> 16) & 0xff;
81+
buf[3] = (adjusted >> 8) & 0xff;
82+
buf[4] = adjusted & 0xff;
83+
return 5;
84+
}
85+
86+
/*
87+
* Create a crafted MMDB with an array claiming 1,000,000 elements but
88+
* only a few bytes of actual data.
89+
*/
90+
static void create_bad_data_size_db(const char *path) {
91+
uint32_t node_count = 1;
92+
uint32_t record_size = 24;
93+
uint32_t record_value = node_count + 16;
94+
95+
/* The data section needs enough bytes for the array header + a few
96+
* elements but NOT enough for 1M elements. */
97+
size_t data_buf_size = 64;
98+
size_t metadata_buf_size = 512;
99+
size_t search_tree_size = 6;
100+
size_t total_size = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size +
101+
METADATA_MARKER_LEN + metadata_buf_size;
102+
103+
uint8_t *file = calloc(1, total_size);
104+
if (!file) {
105+
BAIL_OUT("calloc failed");
106+
}
107+
108+
size_t pos = 0;
109+
110+
/* Search tree: 1 node, 24-bit records, both pointing to data */
111+
file[pos++] = (record_value >> 16) & 0xff;
112+
file[pos++] = (record_value >> 8) & 0xff;
113+
file[pos++] = record_value & 0xff;
114+
file[pos++] = (record_value >> 16) & 0xff;
115+
file[pos++] = (record_value >> 8) & 0xff;
116+
file[pos++] = record_value & 0xff;
117+
118+
/* 16-byte null separator */
119+
memset(file + pos, 0, DATA_SEPARATOR_SIZE);
120+
pos += DATA_SEPARATOR_SIZE;
121+
122+
/* Data section: array claiming 1,000,000 elements */
123+
pos += write_large_array(file + pos, 1000000);
124+
125+
/* Only write a few bytes of actual data (way less than 1M entries) */
126+
pos += write_string(file + pos, "x", 1);
127+
pos += write_string(file + pos, "y", 1);
128+
129+
/* Pad to data_buf_size */
130+
size_t data_end = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size;
131+
if (pos < data_end) {
132+
pos = data_end;
133+
}
134+
135+
/* Metadata marker */
136+
memcpy(file + pos, METADATA_MARKER, METADATA_MARKER_LEN);
137+
pos += METADATA_MARKER_LEN;
138+
139+
/* Metadata map */
140+
pos += write_map(file + pos, 9);
141+
142+
pos += write_meta_key(file + pos, "binary_format_major_version");
143+
pos += write_uint16(file + pos, 2);
144+
145+
pos += write_meta_key(file + pos, "binary_format_minor_version");
146+
pos += write_uint16(file + pos, 0);
147+
148+
pos += write_meta_key(file + pos, "build_epoch");
149+
pos += write_uint64(file + pos, 1000000000ULL);
150+
151+
pos += write_meta_key(file + pos, "database_type");
152+
pos += write_string(file + pos, "Test", 4);
153+
154+
pos += write_meta_key(file + pos, "description");
155+
pos += write_map(file + pos, 0);
156+
157+
pos += write_meta_key(file + pos, "ip_version");
158+
pos += write_uint16(file + pos, 4);
159+
160+
pos += write_meta_key(file + pos, "languages");
161+
file[pos++] = (0 << 5) | 0;
162+
file[pos++] = 4; /* extended type: 7 + 4 = 11 (array) */
163+
164+
pos += write_meta_key(file + pos, "node_count");
165+
pos += write_uint32(file + pos, node_count);
166+
167+
pos += write_meta_key(file + pos, "record_size");
168+
pos += write_uint16(file + pos, record_size);
169+
170+
FILE *f = fopen(path, "wb");
171+
if (!f) {
172+
free(file);
173+
BAIL_OUT("fopen failed");
174+
}
175+
fwrite(file, 1, pos, f);
176+
fclose(f);
177+
free(file);
178+
}
179+
180+
void test_bad_data_size_rejected(void) {
181+
const char *path = "/tmp/test_bad_data_size.mmdb";
182+
create_bad_data_size_db(path);
183+
184+
MMDB_s mmdb;
185+
int status = MMDB_open(path, MMDB_MODE_MMAP, &mmdb);
186+
cmp_ok(status, "==", MMDB_SUCCESS, "opened bad-data-size MMDB");
187+
188+
if (status != MMDB_SUCCESS) {
189+
diag("MMDB_open failed: %s", MMDB_strerror(status));
190+
remove(path);
191+
return;
192+
}
193+
194+
int gai_error, mmdb_error;
195+
MMDB_lookup_result_s result =
196+
MMDB_lookup_string(&mmdb, "1.2.3.4", &gai_error, &mmdb_error);
197+
198+
cmp_ok(mmdb_error, "==", MMDB_SUCCESS, "lookup succeeded");
199+
ok(result.found_entry, "entry found");
200+
201+
if (result.found_entry) {
202+
MMDB_entry_data_list_s *entry_data_list = NULL;
203+
status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
204+
cmp_ok(status,
205+
"==",
206+
MMDB_INVALID_DATA_ERROR,
207+
"MMDB_get_entry_data_list returns INVALID_DATA_ERROR "
208+
"for array with size exceeding remaining data");
209+
MMDB_free_entry_data_list(entry_data_list);
210+
}
211+
212+
MMDB_close(&mmdb);
213+
remove(path);
214+
}
215+
216+
/*
217+
* Create a crafted MMDB with a map claiming 1,000,000 entries but
218+
* only a few bytes of actual data.
219+
*/
220+
static void create_bad_map_size_db(const char *path) {
221+
uint32_t node_count = 1;
222+
uint32_t record_size = 24;
223+
uint32_t record_value = node_count + 16;
224+
225+
size_t data_buf_size = 64;
226+
size_t metadata_buf_size = 512;
227+
size_t search_tree_size = 6;
228+
size_t total_size = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size +
229+
METADATA_MARKER_LEN + metadata_buf_size;
230+
231+
uint8_t *file = calloc(1, total_size);
232+
if (!file) {
233+
BAIL_OUT("calloc failed");
234+
}
235+
236+
size_t pos = 0;
237+
238+
/* Search tree: 1 node, 24-bit records, both pointing to data */
239+
file[pos++] = (record_value >> 16) & 0xff;
240+
file[pos++] = (record_value >> 8) & 0xff;
241+
file[pos++] = record_value & 0xff;
242+
file[pos++] = (record_value >> 16) & 0xff;
243+
file[pos++] = (record_value >> 8) & 0xff;
244+
file[pos++] = record_value & 0xff;
245+
246+
/* 16-byte null separator */
247+
memset(file + pos, 0, DATA_SEPARATOR_SIZE);
248+
pos += DATA_SEPARATOR_SIZE;
249+
250+
/* Data section: map claiming 1,000,000 entries */
251+
pos += write_large_map(file + pos, 1000000);
252+
253+
/* Only write a couple of key-value pairs (way less than 1M entries) */
254+
pos += write_string(file + pos, "k", 1);
255+
pos += write_string(file + pos, "v", 1);
256+
257+
/* Pad to data_buf_size */
258+
size_t data_end = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size;
259+
if (pos < data_end) {
260+
pos = data_end;
261+
}
262+
263+
/* Metadata marker */
264+
memcpy(file + pos, METADATA_MARKER, METADATA_MARKER_LEN);
265+
pos += METADATA_MARKER_LEN;
266+
267+
/* Metadata map */
268+
pos += write_map(file + pos, 9);
269+
270+
pos += write_meta_key(file + pos, "binary_format_major_version");
271+
pos += write_uint16(file + pos, 2);
272+
273+
pos += write_meta_key(file + pos, "binary_format_minor_version");
274+
pos += write_uint16(file + pos, 0);
275+
276+
pos += write_meta_key(file + pos, "build_epoch");
277+
pos += write_uint64(file + pos, 1000000000ULL);
278+
279+
pos += write_meta_key(file + pos, "database_type");
280+
pos += write_string(file + pos, "Test", 4);
281+
282+
pos += write_meta_key(file + pos, "description");
283+
pos += write_map(file + pos, 0);
284+
285+
pos += write_meta_key(file + pos, "ip_version");
286+
pos += write_uint16(file + pos, 4);
287+
288+
pos += write_meta_key(file + pos, "languages");
289+
file[pos++] = (0 << 5) | 0;
290+
file[pos++] = 4; /* extended type: 7 + 4 = 11 (array) */
291+
292+
pos += write_meta_key(file + pos, "node_count");
293+
pos += write_uint32(file + pos, node_count);
294+
295+
pos += write_meta_key(file + pos, "record_size");
296+
pos += write_uint16(file + pos, record_size);
297+
298+
FILE *f = fopen(path, "wb");
299+
if (!f) {
300+
free(file);
301+
BAIL_OUT("fopen failed");
302+
}
303+
fwrite(file, 1, pos, f);
304+
fclose(f);
305+
free(file);
306+
}
307+
308+
void test_bad_map_size_rejected(void) {
309+
const char *path = "/tmp/test_bad_map_size.mmdb";
310+
create_bad_map_size_db(path);
311+
312+
MMDB_s mmdb;
313+
int status = MMDB_open(path, MMDB_MODE_MMAP, &mmdb);
314+
cmp_ok(status, "==", MMDB_SUCCESS, "opened bad-map-size MMDB");
315+
316+
if (status != MMDB_SUCCESS) {
317+
diag("MMDB_open failed: %s", MMDB_strerror(status));
318+
remove(path);
319+
return;
320+
}
321+
322+
int gai_error, mmdb_error;
323+
MMDB_lookup_result_s result =
324+
MMDB_lookup_string(&mmdb, "1.2.3.4", &gai_error, &mmdb_error);
325+
326+
cmp_ok(mmdb_error, "==", MMDB_SUCCESS, "lookup succeeded");
327+
ok(result.found_entry, "entry found");
328+
329+
if (result.found_entry) {
330+
MMDB_entry_data_list_s *entry_data_list = NULL;
331+
status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
332+
cmp_ok(status,
333+
"==",
334+
MMDB_INVALID_DATA_ERROR,
335+
"MMDB_get_entry_data_list returns INVALID_DATA_ERROR "
336+
"for map with size exceeding remaining data");
337+
MMDB_free_entry_data_list(entry_data_list);
338+
}
339+
340+
MMDB_close(&mmdb);
341+
remove(path);
342+
}
343+
344+
int main(void) {
345+
plan(NO_PLAN);
346+
test_bad_data_size_rejected();
347+
test_bad_map_size_rejected();
348+
done_testing();
349+
}

0 commit comments

Comments
 (0)