Skip to content

Commit bc7fccf

Browse files
committed
Return an error for invalid search nodes
1 parent feef4fd commit bc7fccf

4 files changed

Lines changed: 54 additions & 17 deletions

File tree

Changes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
- Fixed search-tree validation for records that point into the 16-byte separator
1212
before the data section. These records are now rejected as corrupt instead of
1313
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.
1417

1518
## 1.13.3 - 2026-03-05
1619

doc/libmaxminddb.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,11 @@ to an `MMDB_search_node_s` structure that will be populated by this function.
773773
774774
The return value is a status code. If you pass a `node_number` that is greater
775775
than or equal to the number of nodes in the database, this function will return
776-
`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.
777781
778782
The first node in the search tree is always node 0. If you wanted to iterate
779783
over the whole search tree, you would start by reading node 0 and then following

src/maxminddb.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,11 @@ int MMDB_read_node(const MMDB_s *const mmdb,
11431143
node->left_record_type = record_type(mmdb, node->left_record);
11441144
node->right_record_type = record_type(mmdb, node->right_record);
11451145

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+
11461151
// Note that offset will be invalid if the record type is not
11471152
// MMDB_RECORD_TYPE_DATA, but that's ok. Any use of the record entry
11481153
// for other data types is a programming error.

t/bad_search_tree_t.c

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,16 @@ static int copy_file(FILE *dest, const char *source_path) {
9292
return result;
9393
}
9494

95-
void test_separator_record_rejected(void) {
95+
// Mutate node 0 of a copy of the IPv4 test database so that one of its
96+
// 24-bit records holds `record_value`, then assert that all corruption-
97+
// rejection paths fire. `record_offset` is 0 for the left record or 3
98+
// for the right record. `lookup_ip` must traverse the corrupted record
99+
// from node 0 (high bit 0 for left, 1 for right). `label` identifies
100+
// the case in TAP output.
101+
static void check_corrupt_record(const char *label,
102+
long record_offset,
103+
unsigned char record_value_be[3],
104+
const char *lookup_ip) {
96105
char *db_file = test_database_path("MaxMind-DB-test-ipv4-24.mmdb");
97106
char temp_path[64];
98107
FILE *temp = open_temp_file(temp_path, sizeof(temp_path));
@@ -108,16 +117,13 @@ void test_separator_record_rejected(void) {
108117
BAIL_OUT("could not copy test database: %s", strerror(errno));
109118
}
110119

111-
// Overwrite node 0's left record with node_count + 1, which points into
112-
// the 16-byte separator between the search tree and data section.
113-
if (fseek(temp, 0, SEEK_SET) != 0) {
120+
if (fseek(temp, record_offset, SEEK_SET) != 0) {
114121
fclose(temp);
115122
unlink(temp_path);
116123
free(db_file);
117124
BAIL_OUT("fseek failed: %s", strerror(errno));
118125
}
119-
unsigned char bad_record[3] = {0x00, 0x00, 0xA4};
120-
if (fwrite(bad_record, 1, sizeof(bad_record), temp) != sizeof(bad_record)) {
126+
if (fwrite(record_value_be, 1, 3, temp) != 3) {
121127
fclose(temp);
122128
unlink(temp_path);
123129
free(db_file);
@@ -131,7 +137,7 @@ void test_separator_record_rejected(void) {
131137

132138
MMDB_s mmdb;
133139
int status = MMDB_open(temp_path, MMDB_MODE_MMAP, &mmdb);
134-
cmp_ok(status, "==", MMDB_SUCCESS, "opened crafted bad search tree MMDB");
140+
cmp_ok(status, "==", MMDB_SUCCESS, "%s: opened crafted bad MMDB", label);
135141
if (status != MMDB_SUCCESS) {
136142
unlink(temp_path);
137143
free(db_file);
@@ -140,29 +146,48 @@ void test_separator_record_rejected(void) {
140146

141147
MMDB_search_node_s node;
142148
status = MMDB_read_node(&mmdb, 0, &node);
143-
cmp_ok(
144-
status, "==", MMDB_SUCCESS, "MMDB_read_node succeeds for crafted node");
145-
cmp_ok(node.left_record_type,
149+
cmp_ok(status,
146150
"==",
147-
MMDB_RECORD_TYPE_INVALID,
148-
"MMDB_read_node marks separator record as invalid");
151+
MMDB_CORRUPT_SEARCH_TREE_ERROR,
152+
"%s: MMDB_read_node rejects record as corrupt",
153+
label);
149154

150155
int gai_error = 0;
151156
int mmdb_error = 0;
152157
MMDB_lookup_result_s result =
153-
MMDB_lookup_string(&mmdb, "1.1.1.1", &gai_error, &mmdb_error);
154-
cmp_ok(gai_error, "==", 0, "lookup string parse succeeds");
158+
MMDB_lookup_string(&mmdb, lookup_ip, &gai_error, &mmdb_error);
159+
cmp_ok(gai_error, "==", 0, "%s: lookup string parse succeeds", label);
155160
cmp_ok(mmdb_error,
156161
"==",
157162
MMDB_CORRUPT_SEARCH_TREE_ERROR,
158-
"MMDB_lookup_string rejects records pointing into the separator");
159-
ok(!result.found_entry, "lookup does not report an entry for corrupt tree");
163+
"%s: MMDB_lookup_string rejects record",
164+
label);
165+
ok(!result.found_entry, "%s: lookup reports no entry", label);
160166

161167
MMDB_close(&mmdb);
162168
unlink(temp_path);
163169
free(db_file);
164170
}
165171

172+
void test_separator_record_rejected(void) {
173+
// The IPv4 test database has node_count == 163 (0xA3). Records in the
174+
// half-open range [node_count + 1, node_count + 16) point into the
175+
// 16-byte data section separator and must be rejected as corrupt.
176+
unsigned char first_separator_byte[3] = {0x00, 0x00, 0xA4};
177+
unsigned char last_separator_byte[3] = {0x00, 0x00, 0xB2};
178+
179+
check_corrupt_record("left record, first separator byte",
180+
0,
181+
first_separator_byte,
182+
"1.1.1.1");
183+
check_corrupt_record("right record, first separator byte",
184+
3,
185+
first_separator_byte,
186+
"128.0.0.0");
187+
check_corrupt_record(
188+
"left record, last separator byte", 0, last_separator_byte, "1.1.1.1");
189+
}
190+
166191
int main(void) {
167192
plan(NO_PLAN);
168193
test_read_node_bounds();

0 commit comments

Comments
 (0)