Skip to content

Commit dd076ee

Browse files
committed
Reject search tree records in separator
1 parent a849dc5 commit dd076ee

3 files changed

Lines changed: 134 additions & 4 deletions

File tree

Changes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
`\xAB\xCD\xEFMaxMind.com` marker. Such files are now rejected as invalid
99
metadata instead of allowing a zero-length metadata section to reach the
1010
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.
1114

1215
## 1.13.3 - 2026-03-05
1316

src/maxminddb.c

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,12 +1000,12 @@ static int find_address_in_search_tree(const MMDB_s *const mmdb,
10001000

10011001
result->netmask = current_bit;
10021002

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

1008-
if (value == node_count) {
1008+
if (type == MMDB_RECORD_TYPE_EMPTY) {
10091009
// record is empty
10101010
result->found_entry = false;
10111011
return MMDB_SUCCESS;
@@ -1093,7 +1093,14 @@ static uint8_t record_type(const MMDB_s *const mmdb, uint64_t record) {
10931093
return MMDB_RECORD_TYPE_EMPTY;
10941094
}
10951095

1096-
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) {
10971104
return MMDB_RECORD_TYPE_DATA;
10981105
}
10991106

t/bad_search_tree_t.c

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
#include "maxminddb_test_helper.h"
22

3+
#include <errno.h>
4+
#include <string.h>
5+
6+
#ifdef _WIN32
7+
#include <io.h>
8+
#include <stdio.h>
9+
#define unlink _unlink
10+
#else
11+
#include <unistd.h>
12+
#endif
13+
314
/*
415
* Test the off-by-one fix in MMDB_read_node: node_number >= node_count
516
* must return MMDB_INVALID_NODE_NUMBER_ERROR. Previously the check used
@@ -41,8 +52,117 @@ void test_read_node_bounds(void) {
4152
free(mmdb);
4253
}
4354

55+
static FILE *open_temp_file(char *path, size_t path_size) {
56+
#ifdef _WIN32
57+
errno_t err = tmpnam_s(path, path_size);
58+
if (err != 0) {
59+
return NULL;
60+
}
61+
return fopen(path, "wb");
62+
#else
63+
snprintf(path, path_size, "./bad-search-tree-XXXXXX");
64+
int fd = mkstemp(path);
65+
if (fd < 0) {
66+
return NULL;
67+
}
68+
return fdopen(fd, "wb");
69+
#endif
70+
}
71+
72+
static int copy_file(FILE *dest, const char *source_path) {
73+
FILE *source = fopen(source_path, "rb");
74+
if (!source) {
75+
return -1;
76+
}
77+
78+
unsigned char buffer[4096];
79+
size_t bytes_read;
80+
while ((bytes_read = fread(buffer, 1, sizeof(buffer), source)) > 0) {
81+
if (fwrite(buffer, 1, bytes_read, dest) != bytes_read) {
82+
fclose(source);
83+
return -1;
84+
}
85+
}
86+
87+
int result = ferror(source) ? -1 : 0;
88+
fclose(source);
89+
return result;
90+
}
91+
92+
void test_separator_record_rejected(void) {
93+
char *db_file = test_database_path("MaxMind-DB-test-ipv4-24.mmdb");
94+
char temp_path[64];
95+
FILE *temp = open_temp_file(temp_path, sizeof(temp_path));
96+
if (!temp) {
97+
free(db_file);
98+
BAIL_OUT("could not create temp file: %s", strerror(errno));
99+
}
100+
101+
if (copy_file(temp, db_file) != 0) {
102+
fclose(temp);
103+
unlink(temp_path);
104+
free(db_file);
105+
BAIL_OUT("could not copy test database: %s", strerror(errno));
106+
}
107+
108+
// Overwrite node 0's left record with node_count + 1, which points into
109+
// the 16-byte separator between the search tree and data section.
110+
if (fseek(temp, 0, SEEK_SET) != 0) {
111+
fclose(temp);
112+
unlink(temp_path);
113+
free(db_file);
114+
BAIL_OUT("fseek failed: %s", strerror(errno));
115+
}
116+
unsigned char bad_record[3] = {0x00, 0x00, 0xA4};
117+
if (fwrite(bad_record, 1, sizeof(bad_record), temp) != sizeof(bad_record)) {
118+
fclose(temp);
119+
unlink(temp_path);
120+
free(db_file);
121+
BAIL_OUT("fwrite failed: %s", strerror(errno));
122+
}
123+
if (fclose(temp) != 0) {
124+
unlink(temp_path);
125+
free(db_file);
126+
BAIL_OUT("fclose failed: %s", strerror(errno));
127+
}
128+
129+
MMDB_s mmdb;
130+
int status = MMDB_open(temp_path, MMDB_MODE_MMAP, &mmdb);
131+
cmp_ok(status, "==", MMDB_SUCCESS, "opened crafted bad search tree MMDB");
132+
if (status != MMDB_SUCCESS) {
133+
unlink(temp_path);
134+
free(db_file);
135+
return;
136+
}
137+
138+
MMDB_search_node_s node;
139+
status = MMDB_read_node(&mmdb, 0, &node);
140+
cmp_ok(
141+
status, "==", MMDB_SUCCESS, "MMDB_read_node succeeds for crafted node");
142+
cmp_ok(node.left_record_type,
143+
"==",
144+
MMDB_RECORD_TYPE_INVALID,
145+
"MMDB_read_node marks separator record as invalid");
146+
147+
int gai_error = 0;
148+
int mmdb_error = 0;
149+
MMDB_lookup_result_s result =
150+
MMDB_lookup_string(&mmdb, "1.1.1.1", &gai_error, &mmdb_error);
151+
cmp_ok(gai_error, "==", 0, "lookup string parse succeeds");
152+
cmp_ok(mmdb_error,
153+
"==",
154+
MMDB_CORRUPT_SEARCH_TREE_ERROR,
155+
"MMDB_lookup_string rejects records pointing into the separator");
156+
ok(!result.found_entry, "lookup does not report an entry for corrupt tree");
157+
158+
MMDB_close(&mmdb);
159+
unlink(temp_path);
160+
free(db_file);
161+
}
162+
44163
int main(void) {
45164
plan(NO_PLAN);
46165
test_read_node_bounds();
166+
test_separator_record_rejected();
47167
done_testing();
48168
}

0 commit comments

Comments
 (0)