Skip to content

Commit d8f1436

Browse files
oschwaldclaude
andcommitted
Add regression tests for the decoder resource limits
Exercise MMDB_get_entry_data_list against the coordinated fixtures. The value-count fan-out, the payload amplification, and its worst case under the value-count limit are each rejected with MMDB_INVALID_DATA_ERROR and leave a NULL output list. A normal record still decodes, confirming no false rejection, and a rejected decode does not affect a later one, confirming the counters are per call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fb16310 commit d8f1436

3 files changed

Lines changed: 145 additions & 1 deletion

File tree

t/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ set(TEST_TARGET_NAMES
2222
metadata_t
2323
no_map_get_value_t
2424
overflow_bounds_t
25+
pointer_dos_t
2526
read_node_t
2627
version_t
2728
)

t/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ check_PROGRAMS = \
2424
get_value_pointer_bug_t invalid_sockaddr_t \
2525
ipv4_start_cache_t ipv6_lookup_in_ipv4_t max_depth_t metadata_t \
2626
metadata_marker_t metadata_pointers_t no_map_get_value_t \
27-
overflow_bounds_t read_node_t \
27+
overflow_bounds_t pointer_dos_t read_node_t \
2828
threads_t version_t
2929

3030
data_pool_t_LDFLAGS = $(AM_LDFLAGS) -lm

t/pointer_dos_t.c

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#include "maxminddb_test_helper.h"
2+
3+
/* Decoding a crafted fan-out record must be rejected, not run to exhaustion.
4+
* The value-count and payload byte limits both surface as
5+
* MMDB_INVALID_DATA_ERROR from MMDB_get_entry_data_list, and the output list
6+
* must be left NULL. */
7+
static void test_fan_out_rejected(const char *fixture, const char *desc) {
8+
char *db_file = test_database_path(fixture);
9+
10+
MMDB_s mmdb;
11+
int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb);
12+
cmp_ok(status, "==", MMDB_SUCCESS, "opened %s fixture", desc);
13+
if (status != MMDB_SUCCESS) {
14+
diag("MMDB_open failed: %s", MMDB_strerror(status));
15+
free(db_file);
16+
return;
17+
}
18+
19+
int gai_error, mmdb_error;
20+
MMDB_lookup_result_s result =
21+
MMDB_lookup_string(&mmdb, "1.1.1.1", &gai_error, &mmdb_error);
22+
cmp_ok(mmdb_error, "==", MMDB_SUCCESS, "%s: lookup succeeded", desc);
23+
ok(result.found_entry, "%s: entry found", desc);
24+
25+
if (result.found_entry) {
26+
MMDB_entry_data_list_s *entry_data_list = NULL;
27+
status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
28+
cmp_ok(status,
29+
"==",
30+
MMDB_INVALID_DATA_ERROR,
31+
"%s: MMDB_get_entry_data_list returns MMDB_INVALID_DATA_ERROR",
32+
desc);
33+
ok(entry_data_list == NULL,
34+
"%s: output list is NULL after the error",
35+
desc);
36+
MMDB_free_entry_data_list(entry_data_list);
37+
}
38+
39+
MMDB_close(&mmdb);
40+
free(db_file);
41+
}
42+
43+
/* A normal record must still decode fully. Its payload is far below the limit,
44+
* so the limits must not reject legitimate data. */
45+
static void test_normal_record_allowed(void) {
46+
char *db_file = test_database_path("GeoIP2-City-Test.mmdb");
47+
48+
MMDB_s mmdb;
49+
int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb);
50+
cmp_ok(status, "==", MMDB_SUCCESS, "opened GeoIP2-City-Test");
51+
if (status != MMDB_SUCCESS) {
52+
diag("MMDB_open failed: %s", MMDB_strerror(status));
53+
free(db_file);
54+
return;
55+
}
56+
57+
int gai_error, mmdb_error;
58+
MMDB_lookup_result_s result =
59+
MMDB_lookup_string(&mmdb, "81.2.69.142", &gai_error, &mmdb_error);
60+
ok(result.found_entry, "normal record: entry found");
61+
62+
if (result.found_entry) {
63+
MMDB_entry_data_list_s *entry_data_list = NULL;
64+
status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
65+
cmp_ok(status,
66+
"==",
67+
MMDB_SUCCESS,
68+
"normal record decodes with no false rejection");
69+
ok(entry_data_list != NULL, "normal record: list returned");
70+
MMDB_free_entry_data_list(entry_data_list);
71+
}
72+
73+
MMDB_close(&mmdb);
74+
free(db_file);
75+
}
76+
77+
/* The counters are per call. A rejected decode must not leave state that
78+
* changes a later decode, on the same reader or another one. */
79+
static void test_per_call_state(void) {
80+
char *dos_file =
81+
test_database_path("MaxMind-DB-test-payload-amplification-dos.mmdb");
82+
MMDB_s dos;
83+
if (MMDB_open(dos_file, MMDB_MODE_MMAP, &dos) == MMDB_SUCCESS) {
84+
int gai, err;
85+
MMDB_lookup_result_s result =
86+
MMDB_lookup_string(&dos, "1.1.1.1", &gai, &err);
87+
if (result.found_entry) {
88+
MMDB_entry_data_list_s *first = NULL;
89+
int s1 = MMDB_get_entry_data_list(&result.entry, &first);
90+
cmp_ok(s1,
91+
"==",
92+
MMDB_INVALID_DATA_ERROR,
93+
"per-call: first decode of the attack record is rejected");
94+
MMDB_free_entry_data_list(first);
95+
96+
MMDB_entry_data_list_s *second = NULL;
97+
int s2 = MMDB_get_entry_data_list(&result.entry, &second);
98+
cmp_ok(s2,
99+
"==",
100+
MMDB_INVALID_DATA_ERROR,
101+
"per-call: repeating it is still rejected, no leaked count");
102+
MMDB_free_entry_data_list(second);
103+
}
104+
MMDB_close(&dos);
105+
}
106+
free(dos_file);
107+
108+
char *ok_file = test_database_path("GeoIP2-City-Test.mmdb");
109+
MMDB_s good;
110+
if (MMDB_open(ok_file, MMDB_MODE_MMAP, &good) == MMDB_SUCCESS) {
111+
int gai, err;
112+
MMDB_lookup_result_s result =
113+
MMDB_lookup_string(&good, "81.2.69.142", &gai, &err);
114+
if (result.found_entry) {
115+
MMDB_entry_data_list_s *list = NULL;
116+
int status = MMDB_get_entry_data_list(&result.entry, &list);
117+
cmp_ok(status,
118+
"==",
119+
MMDB_SUCCESS,
120+
"per-call: a valid decode after a rejected one still works");
121+
MMDB_free_entry_data_list(list);
122+
}
123+
MMDB_close(&good);
124+
}
125+
free(ok_file);
126+
}
127+
128+
int main(void) {
129+
plan(NO_PLAN);
130+
/* Value-count limit: nested arrays of pointers to shared targets. */
131+
test_fan_out_rejected("MaxMind-DB-test-pointer-decoder-dos.mmdb",
132+
"value-count fan-out");
133+
/* Payload byte limit: many pointers to one large string. */
134+
test_fan_out_rejected("MaxMind-DB-test-payload-amplification-dos.mmdb",
135+
"payload amplification");
136+
/* Worst case under the value-count limit, caught only by the byte limit. */
137+
test_fan_out_rejected(
138+
"MaxMind-DB-test-payload-amplification-dos-worst-case.mmdb",
139+
"worst-case payload");
140+
test_normal_record_allowed();
141+
test_per_call_state();
142+
done_testing();
143+
}

0 commit comments

Comments
 (0)