Skip to content

Commit c3ff558

Browse files
committed
Return an error for invalid search nodes
1 parent 98f388b commit c3ff558

4 files changed

Lines changed: 46 additions & 102 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: 33 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,5 @@
1-
// Required for mkstemp visibility under -D_POSIX_C_SOURCE=200112L on glibc.
2-
#define _XOPEN_SOURCE 500
3-
41
#include "maxminddb_test_helper.h"
52

6-
#include <errno.h>
7-
#include <string.h>
8-
9-
#ifdef _WIN32
10-
#include <io.h>
11-
#include <stdio.h>
12-
#define unlink _unlink
13-
#else
14-
#include <unistd.h>
15-
#endif
16-
173
/*
184
* Test the off-by-one fix in MMDB_read_node: node_number >= node_count
195
* must return MMDB_INVALID_NODE_NUMBER_ERROR. Previously the check used
@@ -55,114 +41,60 @@ void test_read_node_bounds(void) {
5541
free(mmdb);
5642
}
5743

58-
static FILE *open_temp_file(char *path, size_t path_size) {
59-
#ifdef _WIN32
60-
errno_t err = tmpnam_s(path, path_size);
61-
if (err != 0) {
62-
return NULL;
63-
}
64-
return fopen(path, "wb");
65-
#else
66-
snprintf(path, path_size, "./bad-search-tree-XXXXXX");
67-
int fd = mkstemp(path);
68-
if (fd < 0) {
69-
return NULL;
70-
}
71-
return fdopen(fd, "wb");
72-
#endif
73-
}
74-
75-
static int copy_file(FILE *dest, const char *source_path) {
76-
FILE *source = fopen(source_path, "rb");
77-
if (!source) {
78-
return -1;
79-
}
80-
81-
unsigned char buffer[4096];
82-
size_t bytes_read;
83-
while ((bytes_read = fread(buffer, 1, sizeof(buffer), source)) > 0) {
84-
if (fwrite(buffer, 1, bytes_read, dest) != bytes_read) {
85-
fclose(source);
86-
return -1;
87-
}
88-
}
89-
90-
int result = ferror(source) ? -1 : 0;
91-
fclose(source);
92-
return result;
93-
}
94-
95-
void test_separator_record_rejected(void) {
96-
char *db_file = test_database_path("MaxMind-DB-test-ipv4-24.mmdb");
97-
char temp_path[64];
98-
FILE *temp = open_temp_file(temp_path, sizeof(temp_path));
99-
if (!temp) {
100-
free(db_file);
101-
BAIL_OUT("could not create temp file: %s", strerror(errno));
102-
}
103-
104-
if (copy_file(temp, db_file) != 0) {
105-
fclose(temp);
106-
unlink(temp_path);
107-
free(db_file);
108-
BAIL_OUT("could not copy test database: %s", strerror(errno));
109-
}
110-
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) {
114-
fclose(temp);
115-
unlink(temp_path);
116-
free(db_file);
117-
BAIL_OUT("fseek failed: %s", strerror(errno));
118-
}
119-
unsigned char bad_record[3] = {0x00, 0x00, 0xA4};
120-
if (fwrite(bad_record, 1, sizeof(bad_record), temp) != sizeof(bad_record)) {
121-
fclose(temp);
122-
unlink(temp_path);
123-
free(db_file);
124-
BAIL_OUT("fwrite failed: %s", strerror(errno));
125-
}
126-
if (fclose(temp) != 0) {
127-
unlink(temp_path);
128-
free(db_file);
129-
BAIL_OUT("fclose failed: %s", strerror(errno));
130-
}
131-
44+
// Open a pre-built corruption fixture and assert that all rejection paths
45+
// fire. `lookup_ip` must traverse the corrupted record from node 0 — high
46+
// bit 0 for a corrupted left record, 1 for a corrupted right record.
47+
static void check_corrupt_record(const char *label,
48+
const char *fixture,
49+
const char *lookup_ip) {
50+
char *db_file = bad_database_path(fixture);
13251
MMDB_s mmdb;
133-
int status = MMDB_open(temp_path, MMDB_MODE_MMAP, &mmdb);
134-
cmp_ok(status, "==", MMDB_SUCCESS, "opened crafted bad search tree MMDB");
52+
int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb);
53+
cmp_ok(status, "==", MMDB_SUCCESS, "%s: opened crafted bad MMDB", label);
13554
if (status != MMDB_SUCCESS) {
136-
unlink(temp_path);
13755
free(db_file);
13856
return;
13957
}
14058

14159
MMDB_search_node_s node;
14260
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,
61+
cmp_ok(status,
14662
"==",
147-
MMDB_RECORD_TYPE_INVALID,
148-
"MMDB_read_node marks separator record as invalid");
63+
MMDB_CORRUPT_SEARCH_TREE_ERROR,
64+
"%s: MMDB_read_node rejects record as corrupt",
65+
label);
14966

15067
int gai_error = 0;
15168
int mmdb_error = 0;
15269
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");
70+
MMDB_lookup_string(&mmdb, lookup_ip, &gai_error, &mmdb_error);
71+
cmp_ok(gai_error, "==", 0, "%s: lookup string parse succeeds", label);
15572
cmp_ok(mmdb_error,
15673
"==",
15774
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");
75+
"%s: MMDB_lookup_string rejects record",
76+
label);
77+
ok(!result.found_entry, "%s: lookup reports no entry", label);
16078

16179
MMDB_close(&mmdb);
162-
unlink(temp_path);
16380
free(db_file);
16481
}
16582

83+
void test_separator_record_rejected(void) {
84+
// Records in the half-open range [node_count + 1, node_count + 16) point
85+
// into the 16-byte separator between the search tree and data section
86+
// and must be rejected as corrupt.
87+
check_corrupt_record("left record, first separator byte",
88+
"libmaxminddb-separator-record-min-left.mmdb",
89+
"1.1.1.1");
90+
check_corrupt_record("right record, first separator byte",
91+
"libmaxminddb-separator-record-min-right.mmdb",
92+
"128.0.0.0");
93+
check_corrupt_record("left record, last separator byte",
94+
"libmaxminddb-separator-record-max-left.mmdb",
95+
"1.1.1.1");
96+
}
97+
16698
int main(void) {
16799
plan(NO_PLAN);
168100
test_read_node_bounds();

0 commit comments

Comments
 (0)