Skip to content

Commit 521f13e

Browse files
oschwaldclaude
andcommitted
Add test databases with empty map/array last in metadata
These databases reproduce the off-by-one bug in libmaxminddb's get_entry_data_list() validation. When a 0-length map or array is the last field in metadata, offset_to_next equals data_section_size exactly, which the >= check incorrectly rejects. ENG-4322 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 535644b commit 521f13e

File tree

4 files changed

+136
-0
lines changed

4 files changed

+136
-0
lines changed
Binary file not shown.
219 Bytes
Binary file not shown.

pkg/writer/baddata.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ func (w *Writer) WriteBadDataDBs(target string) error {
2929
{"libmaxminddb-oversized-map.mmdb", buildOversizedMapDB()},
3030
{"libmaxminddb-uint64-max-epoch.mmdb", buildUint64MaxEpochDB()},
3131
{"libmaxminddb-corrupt-search-tree.mmdb", buildCorruptSearchTreeDB()},
32+
{"libmaxminddb-empty-map-last-in-metadata.mmdb", buildEmptyMapLastInMetadataDB()},
33+
{"libmaxminddb-empty-array-last-in-metadata.mmdb", buildEmptyArrayLastInMetadataDB()},
3234
} {
3335
if err := writeRawDB(target, db.name, db.data); err != nil {
3436
return fmt.Errorf("writing %s: %w", db.name, err)

pkg/writer/rawmmdb.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,140 @@ func buildUint64MaxEpochDB() []byte {
208208
return buf[:pos]
209209
}
210210

211+
// writeMetadataBlockEmptyMapLast writes a metadata block where the last field
212+
// is "description" (an empty map). This triggers the off-by-one bug where
213+
// offset_to_next == data_section_size for a 0-length container.
214+
func writeMetadataBlockEmptyMapLast(buf []byte, nodeCount uint32, buildEpoch uint64) int {
215+
pos := 0
216+
217+
copy(buf[pos:], metadataMarker)
218+
pos += len(metadataMarker)
219+
220+
pos += writeMap(buf[pos:], 9)
221+
222+
pos += writeMetaKey(buf[pos:], "binary_format_major_version")
223+
pos += writeUint16(buf[pos:], 2)
224+
225+
pos += writeMetaKey(buf[pos:], "binary_format_minor_version")
226+
pos += writeUint16(buf[pos:], 0)
227+
228+
pos += writeMetaKey(buf[pos:], "build_epoch")
229+
pos += writeUint64(buf[pos:], buildEpoch)
230+
231+
pos += writeMetaKey(buf[pos:], "database_type")
232+
pos += writeString(buf[pos:], "Test")
233+
234+
pos += writeMetaKey(buf[pos:], "ip_version")
235+
pos += writeUint16(buf[pos:], 4)
236+
237+
pos += writeMetaKey(buf[pos:], "languages")
238+
pos += writeEmptyArray(buf[pos:])
239+
240+
pos += writeMetaKey(buf[pos:], "node_count")
241+
pos += writeUint32(buf[pos:], nodeCount)
242+
243+
pos += writeMetaKey(buf[pos:], "record_size")
244+
pos += writeUint16(buf[pos:], 24)
245+
246+
// description last — empty map at the very end of the data section
247+
pos += writeMetaKey(buf[pos:], "description")
248+
pos += writeMap(buf[pos:], 0)
249+
250+
return pos
251+
}
252+
253+
// writeMetadataBlockEmptyArrayLast writes a metadata block where the last
254+
// field is "languages" (an empty array).
255+
func writeMetadataBlockEmptyArrayLast(buf []byte, nodeCount uint32, buildEpoch uint64) int {
256+
pos := 0
257+
258+
copy(buf[pos:], metadataMarker)
259+
pos += len(metadataMarker)
260+
261+
pos += writeMap(buf[pos:], 9)
262+
263+
pos += writeMetaKey(buf[pos:], "binary_format_major_version")
264+
pos += writeUint16(buf[pos:], 2)
265+
266+
pos += writeMetaKey(buf[pos:], "binary_format_minor_version")
267+
pos += writeUint16(buf[pos:], 0)
268+
269+
pos += writeMetaKey(buf[pos:], "build_epoch")
270+
pos += writeUint64(buf[pos:], buildEpoch)
271+
272+
pos += writeMetaKey(buf[pos:], "database_type")
273+
pos += writeString(buf[pos:], "Test")
274+
275+
pos += writeMetaKey(buf[pos:], "description")
276+
pos += writeMap(buf[pos:], 0)
277+
278+
pos += writeMetaKey(buf[pos:], "ip_version")
279+
pos += writeUint16(buf[pos:], 4)
280+
281+
pos += writeMetaKey(buf[pos:], "node_count")
282+
pos += writeUint32(buf[pos:], nodeCount)
283+
284+
pos += writeMetaKey(buf[pos:], "record_size")
285+
pos += writeUint16(buf[pos:], 24)
286+
287+
// languages last — empty array at the very end of the data section
288+
pos += writeMetaKey(buf[pos:], "languages")
289+
pos += writeEmptyArray(buf[pos:])
290+
291+
return pos
292+
}
293+
294+
// buildEmptyMapLastInMetadataDB creates a valid MMDB where the metadata
295+
// map's last field is "description" (an empty map {}). This reproduces the
296+
// off-by-one bug in get_entry_data_list() where offset == data_section_size
297+
// is incorrectly rejected for 0-length containers.
298+
func buildEmptyMapLastInMetadataDB() []byte {
299+
const nodeCount = 1
300+
const recordValue = nodeCount + 16
301+
302+
buf := make([]byte, 1024)
303+
pos := 0
304+
305+
pos += writeSearchTree(buf[pos:], recordValue)
306+
307+
// 16-byte null separator
308+
pos += dataSeparatorSize
309+
310+
// Data: a simple map with one string entry
311+
pos += writeMap(buf[pos:], 1)
312+
pos += writeString(buf[pos:], "ip")
313+
pos += writeString(buf[pos:], "test")
314+
315+
pos += writeMetadataBlockEmptyMapLast(buf[pos:], nodeCount, 1_000_000_000)
316+
317+
return buf[:pos]
318+
}
319+
320+
// buildEmptyArrayLastInMetadataDB creates a valid MMDB where the metadata
321+
// map's last field is "languages" (an empty array []). Tests the array
322+
// validation path of the same off-by-one bug.
323+
func buildEmptyArrayLastInMetadataDB() []byte {
324+
const nodeCount = 1
325+
const recordValue = nodeCount + 16
326+
327+
buf := make([]byte, 1024)
328+
pos := 0
329+
330+
pos += writeSearchTree(buf[pos:], recordValue)
331+
332+
// 16-byte null separator
333+
pos += dataSeparatorSize
334+
335+
// Data: a simple map with one string entry
336+
pos += writeMap(buf[pos:], 1)
337+
pos += writeString(buf[pos:], "ip")
338+
pos += writeString(buf[pos:], "test")
339+
340+
pos += writeMetadataBlockEmptyArrayLast(buf[pos:], nodeCount, 1_000_000_000)
341+
342+
return buf[:pos]
343+
}
344+
211345
// buildCorruptSearchTreeDB creates a complete MMDB where the metadata claims
212346
// node_count = 100 but the actual search tree has only 1 node worth of real
213347
// data (6 bytes for 24-bit records). The file is padded so MMDB_open

0 commit comments

Comments
 (0)