|
| 1 | +#include "maxminddb_test_helper.h" |
| 2 | +#include <stdlib.h> |
| 3 | + |
| 4 | +/* MMDB binary format constants */ |
| 5 | +#define METADATA_MARKER "\xab\xcd\xefMaxMind.com" |
| 6 | +#define METADATA_MARKER_LEN 14 |
| 7 | +#define DATA_SEPARATOR_SIZE 16 |
| 8 | + |
| 9 | +static int write_map(uint8_t *buf, uint32_t size) { |
| 10 | + buf[0] = (7 << 5) | (size & 0x1f); |
| 11 | + return 1; |
| 12 | +} |
| 13 | + |
| 14 | +static int write_string(uint8_t *buf, const char *str, uint32_t len) { |
| 15 | + buf[0] = (2 << 5) | (len & 0x1f); |
| 16 | + memcpy(buf + 1, str, len); |
| 17 | + return 1 + len; |
| 18 | +} |
| 19 | + |
| 20 | +static int write_uint16(uint8_t *buf, uint16_t value) { |
| 21 | + buf[0] = (5 << 5) | 2; |
| 22 | + buf[1] = (value >> 8) & 0xff; |
| 23 | + buf[2] = value & 0xff; |
| 24 | + return 3; |
| 25 | +} |
| 26 | + |
| 27 | +static int write_uint32(uint8_t *buf, uint32_t value) { |
| 28 | + buf[0] = (6 << 5) | 4; |
| 29 | + buf[1] = (value >> 24) & 0xff; |
| 30 | + buf[2] = (value >> 16) & 0xff; |
| 31 | + buf[3] = (value >> 8) & 0xff; |
| 32 | + buf[4] = value & 0xff; |
| 33 | + return 5; |
| 34 | +} |
| 35 | + |
| 36 | +static int write_uint64(uint8_t *buf, uint64_t value) { |
| 37 | + buf[0] = (0 << 5) | 8; |
| 38 | + buf[1] = 2; /* extended type: 7 + 2 = 9 (uint64) */ |
| 39 | + buf[2] = (value >> 56) & 0xff; |
| 40 | + buf[3] = (value >> 48) & 0xff; |
| 41 | + buf[4] = (value >> 40) & 0xff; |
| 42 | + buf[5] = (value >> 32) & 0xff; |
| 43 | + buf[6] = (value >> 24) & 0xff; |
| 44 | + buf[7] = (value >> 16) & 0xff; |
| 45 | + buf[8] = (value >> 8) & 0xff; |
| 46 | + buf[9] = value & 0xff; |
| 47 | + return 10; |
| 48 | +} |
| 49 | + |
| 50 | +static int write_meta_key(uint8_t *buf, const char *key) { |
| 51 | + return write_string(buf, key, strlen(key)); |
| 52 | +} |
| 53 | + |
| 54 | +/* |
| 55 | + * Write a map control byte with a large size using case-31 encoding. |
| 56 | + * Type 7 (map) fits in the base types: control byte = (7 << 5) | size_marker. |
| 57 | + * For case-31 size encoding: control byte size bits = 31, |
| 58 | + * then 3 bytes for (size - 65821). |
| 59 | + */ |
| 60 | +static int write_large_map(uint8_t *buf, uint32_t size) { |
| 61 | + uint32_t adjusted = size - 65821; |
| 62 | + buf[0] = (7 << 5) | 31; /* type 7 (map), size = case 31 */ |
| 63 | + buf[1] = (adjusted >> 16) & 0xff; |
| 64 | + buf[2] = (adjusted >> 8) & 0xff; |
| 65 | + buf[3] = adjusted & 0xff; |
| 66 | + return 4; |
| 67 | +} |
| 68 | + |
| 69 | +/* |
| 70 | + * Write an array control byte with a large size using case-31 encoding. |
| 71 | + * Type 11 (array) is extended: control byte = (0 << 5) | size_marker, |
| 72 | + * followed by extended type byte 4. |
| 73 | + * For case-31 size encoding: control byte size bits = 31, |
| 74 | + * then 3 bytes for (size - 65821). |
| 75 | + */ |
| 76 | +static int write_large_array(uint8_t *buf, uint32_t size) { |
| 77 | + uint32_t adjusted = size - 65821; |
| 78 | + buf[0] = (0 << 5) | 31; /* extended type, size = case 31 */ |
| 79 | + buf[1] = 4; /* extended type: 7 + 4 = 11 (array) */ |
| 80 | + buf[2] = (adjusted >> 16) & 0xff; |
| 81 | + buf[3] = (adjusted >> 8) & 0xff; |
| 82 | + buf[4] = adjusted & 0xff; |
| 83 | + return 5; |
| 84 | +} |
| 85 | + |
| 86 | +/* |
| 87 | + * Create a crafted MMDB with an array claiming 1,000,000 elements but |
| 88 | + * only a few bytes of actual data. |
| 89 | + */ |
| 90 | +static void create_bad_data_size_db(const char *path) { |
| 91 | + uint32_t node_count = 1; |
| 92 | + uint32_t record_size = 24; |
| 93 | + uint32_t record_value = node_count + 16; |
| 94 | + |
| 95 | + /* The data section needs enough bytes for the array header + a few |
| 96 | + * elements but NOT enough for 1M elements. */ |
| 97 | + size_t data_buf_size = 64; |
| 98 | + size_t metadata_buf_size = 512; |
| 99 | + size_t search_tree_size = 6; |
| 100 | + size_t total_size = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size + |
| 101 | + METADATA_MARKER_LEN + metadata_buf_size; |
| 102 | + |
| 103 | + uint8_t *file = calloc(1, total_size); |
| 104 | + if (!file) { |
| 105 | + BAIL_OUT("calloc failed"); |
| 106 | + } |
| 107 | + |
| 108 | + size_t pos = 0; |
| 109 | + |
| 110 | + /* Search tree: 1 node, 24-bit records, both pointing to data */ |
| 111 | + file[pos++] = (record_value >> 16) & 0xff; |
| 112 | + file[pos++] = (record_value >> 8) & 0xff; |
| 113 | + file[pos++] = record_value & 0xff; |
| 114 | + file[pos++] = (record_value >> 16) & 0xff; |
| 115 | + file[pos++] = (record_value >> 8) & 0xff; |
| 116 | + file[pos++] = record_value & 0xff; |
| 117 | + |
| 118 | + /* 16-byte null separator */ |
| 119 | + memset(file + pos, 0, DATA_SEPARATOR_SIZE); |
| 120 | + pos += DATA_SEPARATOR_SIZE; |
| 121 | + |
| 122 | + /* Data section: array claiming 1,000,000 elements */ |
| 123 | + pos += write_large_array(file + pos, 1000000); |
| 124 | + |
| 125 | + /* Only write a few bytes of actual data (way less than 1M entries) */ |
| 126 | + pos += write_string(file + pos, "x", 1); |
| 127 | + pos += write_string(file + pos, "y", 1); |
| 128 | + |
| 129 | + /* Pad to data_buf_size */ |
| 130 | + size_t data_end = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size; |
| 131 | + if (pos < data_end) { |
| 132 | + pos = data_end; |
| 133 | + } |
| 134 | + |
| 135 | + /* Metadata marker */ |
| 136 | + memcpy(file + pos, METADATA_MARKER, METADATA_MARKER_LEN); |
| 137 | + pos += METADATA_MARKER_LEN; |
| 138 | + |
| 139 | + /* Metadata map */ |
| 140 | + pos += write_map(file + pos, 9); |
| 141 | + |
| 142 | + pos += write_meta_key(file + pos, "binary_format_major_version"); |
| 143 | + pos += write_uint16(file + pos, 2); |
| 144 | + |
| 145 | + pos += write_meta_key(file + pos, "binary_format_minor_version"); |
| 146 | + pos += write_uint16(file + pos, 0); |
| 147 | + |
| 148 | + pos += write_meta_key(file + pos, "build_epoch"); |
| 149 | + pos += write_uint64(file + pos, 1000000000ULL); |
| 150 | + |
| 151 | + pos += write_meta_key(file + pos, "database_type"); |
| 152 | + pos += write_string(file + pos, "Test", 4); |
| 153 | + |
| 154 | + pos += write_meta_key(file + pos, "description"); |
| 155 | + pos += write_map(file + pos, 0); |
| 156 | + |
| 157 | + pos += write_meta_key(file + pos, "ip_version"); |
| 158 | + pos += write_uint16(file + pos, 4); |
| 159 | + |
| 160 | + pos += write_meta_key(file + pos, "languages"); |
| 161 | + file[pos++] = (0 << 5) | 0; |
| 162 | + file[pos++] = 4; /* extended type: 7 + 4 = 11 (array) */ |
| 163 | + |
| 164 | + pos += write_meta_key(file + pos, "node_count"); |
| 165 | + pos += write_uint32(file + pos, node_count); |
| 166 | + |
| 167 | + pos += write_meta_key(file + pos, "record_size"); |
| 168 | + pos += write_uint16(file + pos, record_size); |
| 169 | + |
| 170 | + FILE *f = fopen(path, "wb"); |
| 171 | + if (!f) { |
| 172 | + free(file); |
| 173 | + BAIL_OUT("fopen failed"); |
| 174 | + } |
| 175 | + fwrite(file, 1, pos, f); |
| 176 | + fclose(f); |
| 177 | + free(file); |
| 178 | +} |
| 179 | + |
| 180 | +void test_bad_data_size_rejected(void) { |
| 181 | + const char *path = "/tmp/test_bad_data_size.mmdb"; |
| 182 | + create_bad_data_size_db(path); |
| 183 | + |
| 184 | + MMDB_s mmdb; |
| 185 | + int status = MMDB_open(path, MMDB_MODE_MMAP, &mmdb); |
| 186 | + cmp_ok(status, "==", MMDB_SUCCESS, "opened bad-data-size MMDB"); |
| 187 | + |
| 188 | + if (status != MMDB_SUCCESS) { |
| 189 | + diag("MMDB_open failed: %s", MMDB_strerror(status)); |
| 190 | + remove(path); |
| 191 | + return; |
| 192 | + } |
| 193 | + |
| 194 | + int gai_error, mmdb_error; |
| 195 | + MMDB_lookup_result_s result = |
| 196 | + MMDB_lookup_string(&mmdb, "1.2.3.4", &gai_error, &mmdb_error); |
| 197 | + |
| 198 | + cmp_ok(mmdb_error, "==", MMDB_SUCCESS, "lookup succeeded"); |
| 199 | + ok(result.found_entry, "entry found"); |
| 200 | + |
| 201 | + if (result.found_entry) { |
| 202 | + MMDB_entry_data_list_s *entry_data_list = NULL; |
| 203 | + status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); |
| 204 | + cmp_ok(status, |
| 205 | + "==", |
| 206 | + MMDB_INVALID_DATA_ERROR, |
| 207 | + "MMDB_get_entry_data_list returns INVALID_DATA_ERROR " |
| 208 | + "for array with size exceeding remaining data"); |
| 209 | + MMDB_free_entry_data_list(entry_data_list); |
| 210 | + } |
| 211 | + |
| 212 | + MMDB_close(&mmdb); |
| 213 | + remove(path); |
| 214 | +} |
| 215 | + |
| 216 | +/* |
| 217 | + * Create a crafted MMDB with a map claiming 1,000,000 entries but |
| 218 | + * only a few bytes of actual data. |
| 219 | + */ |
| 220 | +static void create_bad_map_size_db(const char *path) { |
| 221 | + uint32_t node_count = 1; |
| 222 | + uint32_t record_size = 24; |
| 223 | + uint32_t record_value = node_count + 16; |
| 224 | + |
| 225 | + size_t data_buf_size = 64; |
| 226 | + size_t metadata_buf_size = 512; |
| 227 | + size_t search_tree_size = 6; |
| 228 | + size_t total_size = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size + |
| 229 | + METADATA_MARKER_LEN + metadata_buf_size; |
| 230 | + |
| 231 | + uint8_t *file = calloc(1, total_size); |
| 232 | + if (!file) { |
| 233 | + BAIL_OUT("calloc failed"); |
| 234 | + } |
| 235 | + |
| 236 | + size_t pos = 0; |
| 237 | + |
| 238 | + /* Search tree: 1 node, 24-bit records, both pointing to data */ |
| 239 | + file[pos++] = (record_value >> 16) & 0xff; |
| 240 | + file[pos++] = (record_value >> 8) & 0xff; |
| 241 | + file[pos++] = record_value & 0xff; |
| 242 | + file[pos++] = (record_value >> 16) & 0xff; |
| 243 | + file[pos++] = (record_value >> 8) & 0xff; |
| 244 | + file[pos++] = record_value & 0xff; |
| 245 | + |
| 246 | + /* 16-byte null separator */ |
| 247 | + memset(file + pos, 0, DATA_SEPARATOR_SIZE); |
| 248 | + pos += DATA_SEPARATOR_SIZE; |
| 249 | + |
| 250 | + /* Data section: map claiming 1,000,000 entries */ |
| 251 | + pos += write_large_map(file + pos, 1000000); |
| 252 | + |
| 253 | + /* Only write a couple of key-value pairs (way less than 1M entries) */ |
| 254 | + pos += write_string(file + pos, "k", 1); |
| 255 | + pos += write_string(file + pos, "v", 1); |
| 256 | + |
| 257 | + /* Pad to data_buf_size */ |
| 258 | + size_t data_end = search_tree_size + DATA_SEPARATOR_SIZE + data_buf_size; |
| 259 | + if (pos < data_end) { |
| 260 | + pos = data_end; |
| 261 | + } |
| 262 | + |
| 263 | + /* Metadata marker */ |
| 264 | + memcpy(file + pos, METADATA_MARKER, METADATA_MARKER_LEN); |
| 265 | + pos += METADATA_MARKER_LEN; |
| 266 | + |
| 267 | + /* Metadata map */ |
| 268 | + pos += write_map(file + pos, 9); |
| 269 | + |
| 270 | + pos += write_meta_key(file + pos, "binary_format_major_version"); |
| 271 | + pos += write_uint16(file + pos, 2); |
| 272 | + |
| 273 | + pos += write_meta_key(file + pos, "binary_format_minor_version"); |
| 274 | + pos += write_uint16(file + pos, 0); |
| 275 | + |
| 276 | + pos += write_meta_key(file + pos, "build_epoch"); |
| 277 | + pos += write_uint64(file + pos, 1000000000ULL); |
| 278 | + |
| 279 | + pos += write_meta_key(file + pos, "database_type"); |
| 280 | + pos += write_string(file + pos, "Test", 4); |
| 281 | + |
| 282 | + pos += write_meta_key(file + pos, "description"); |
| 283 | + pos += write_map(file + pos, 0); |
| 284 | + |
| 285 | + pos += write_meta_key(file + pos, "ip_version"); |
| 286 | + pos += write_uint16(file + pos, 4); |
| 287 | + |
| 288 | + pos += write_meta_key(file + pos, "languages"); |
| 289 | + file[pos++] = (0 << 5) | 0; |
| 290 | + file[pos++] = 4; /* extended type: 7 + 4 = 11 (array) */ |
| 291 | + |
| 292 | + pos += write_meta_key(file + pos, "node_count"); |
| 293 | + pos += write_uint32(file + pos, node_count); |
| 294 | + |
| 295 | + pos += write_meta_key(file + pos, "record_size"); |
| 296 | + pos += write_uint16(file + pos, record_size); |
| 297 | + |
| 298 | + FILE *f = fopen(path, "wb"); |
| 299 | + if (!f) { |
| 300 | + free(file); |
| 301 | + BAIL_OUT("fopen failed"); |
| 302 | + } |
| 303 | + fwrite(file, 1, pos, f); |
| 304 | + fclose(f); |
| 305 | + free(file); |
| 306 | +} |
| 307 | + |
| 308 | +void test_bad_map_size_rejected(void) { |
| 309 | + const char *path = "/tmp/test_bad_map_size.mmdb"; |
| 310 | + create_bad_map_size_db(path); |
| 311 | + |
| 312 | + MMDB_s mmdb; |
| 313 | + int status = MMDB_open(path, MMDB_MODE_MMAP, &mmdb); |
| 314 | + cmp_ok(status, "==", MMDB_SUCCESS, "opened bad-map-size MMDB"); |
| 315 | + |
| 316 | + if (status != MMDB_SUCCESS) { |
| 317 | + diag("MMDB_open failed: %s", MMDB_strerror(status)); |
| 318 | + remove(path); |
| 319 | + return; |
| 320 | + } |
| 321 | + |
| 322 | + int gai_error, mmdb_error; |
| 323 | + MMDB_lookup_result_s result = |
| 324 | + MMDB_lookup_string(&mmdb, "1.2.3.4", &gai_error, &mmdb_error); |
| 325 | + |
| 326 | + cmp_ok(mmdb_error, "==", MMDB_SUCCESS, "lookup succeeded"); |
| 327 | + ok(result.found_entry, "entry found"); |
| 328 | + |
| 329 | + if (result.found_entry) { |
| 330 | + MMDB_entry_data_list_s *entry_data_list = NULL; |
| 331 | + status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); |
| 332 | + cmp_ok(status, |
| 333 | + "==", |
| 334 | + MMDB_INVALID_DATA_ERROR, |
| 335 | + "MMDB_get_entry_data_list returns INVALID_DATA_ERROR " |
| 336 | + "for map with size exceeding remaining data"); |
| 337 | + MMDB_free_entry_data_list(entry_data_list); |
| 338 | + } |
| 339 | + |
| 340 | + MMDB_close(&mmdb); |
| 341 | + remove(path); |
| 342 | +} |
| 343 | + |
| 344 | +int main(void) { |
| 345 | + plan(NO_PLAN); |
| 346 | + test_bad_data_size_rejected(); |
| 347 | + test_bad_map_size_rejected(); |
| 348 | + done_testing(); |
| 349 | +} |
0 commit comments