Skip to content

Commit 3dc7fbc

Browse files
committed
Return an error for invalid search nodes
1 parent dd076ee commit 3dc7fbc

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
@@ -89,7 +89,16 @@ static int copy_file(FILE *dest, const char *source_path) {
8989
return result;
9090
}
9191

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

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) {
117+
if (fseek(temp, record_offset, SEEK_SET) != 0) {
111118
fclose(temp);
112119
unlink(temp_path);
113120
free(db_file);
114121
BAIL_OUT("fseek failed: %s", strerror(errno));
115122
}
116-
unsigned char bad_record[3] = {0x00, 0x00, 0xA4};
117-
if (fwrite(bad_record, 1, sizeof(bad_record), temp) != sizeof(bad_record)) {
123+
if (fwrite(record_value_be, 1, 3, temp) != 3) {
118124
fclose(temp);
119125
unlink(temp_path);
120126
free(db_file);
@@ -128,7 +134,7 @@ void test_separator_record_rejected(void) {
128134

129135
MMDB_s mmdb;
130136
int status = MMDB_open(temp_path, MMDB_MODE_MMAP, &mmdb);
131-
cmp_ok(status, "==", MMDB_SUCCESS, "opened crafted bad search tree MMDB");
137+
cmp_ok(status, "==", MMDB_SUCCESS, "%s: opened crafted bad MMDB", label);
132138
if (status != MMDB_SUCCESS) {
133139
unlink(temp_path);
134140
free(db_file);
@@ -137,29 +143,48 @@ void test_separator_record_rejected(void) {
137143

138144
MMDB_search_node_s node;
139145
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,
146+
cmp_ok(status,
143147
"==",
144-
MMDB_RECORD_TYPE_INVALID,
145-
"MMDB_read_node marks separator record as invalid");
148+
MMDB_CORRUPT_SEARCH_TREE_ERROR,
149+
"%s: MMDB_read_node rejects record as corrupt",
150+
label);
146151

147152
int gai_error = 0;
148153
int mmdb_error = 0;
149154
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");
155+
MMDB_lookup_string(&mmdb, lookup_ip, &gai_error, &mmdb_error);
156+
cmp_ok(gai_error, "==", 0, "%s: lookup string parse succeeds", label);
152157
cmp_ok(mmdb_error,
153158
"==",
154159
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");
160+
"%s: MMDB_lookup_string rejects record",
161+
label);
162+
ok(!result.found_entry, "%s: lookup reports no entry", label);
157163

158164
MMDB_close(&mmdb);
159165
unlink(temp_path);
160166
free(db_file);
161167
}
162168

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

0 commit comments

Comments
 (0)