From f7114e1ef30fc62e7f2afe1d18e9da92c71b7e51 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 4 May 2026 16:00:54 +0000 Subject: [PATCH 1/2] Add test database for trailing metadata marker A minimal 14-byte file containing only the \xab\xcd\xefMaxMind.com metadata marker, with no metadata following. Readers should reject this as invalid metadata. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../libmaxminddb/libmaxminddb-metadata-marker-only.mmdb | 1 + pkg/writer/baddata.go | 1 + pkg/writer/rawmmdb.go | 8 ++++++++ 3 files changed, 10 insertions(+) create mode 100644 bad-data/libmaxminddb/libmaxminddb-metadata-marker-only.mmdb diff --git a/bad-data/libmaxminddb/libmaxminddb-metadata-marker-only.mmdb b/bad-data/libmaxminddb/libmaxminddb-metadata-marker-only.mmdb new file mode 100644 index 0000000..e37ad45 --- /dev/null +++ b/bad-data/libmaxminddb/libmaxminddb-metadata-marker-only.mmdb @@ -0,0 +1 @@ +«ÍïMaxMind.com \ No newline at end of file diff --git a/pkg/writer/baddata.go b/pkg/writer/baddata.go index 088d602..d789a66 100644 --- a/pkg/writer/baddata.go +++ b/pkg/writer/baddata.go @@ -31,6 +31,7 @@ func (w *Writer) WriteBadDataDBs(target string) error { {"libmaxminddb-corrupt-search-tree.mmdb", buildCorruptSearchTreeDB()}, {"libmaxminddb-empty-map-last-in-metadata.mmdb", buildEmptyMapLastInMetadataDB()}, {"libmaxminddb-empty-array-last-in-metadata.mmdb", buildEmptyArrayLastInMetadataDB()}, + {"libmaxminddb-metadata-marker-only.mmdb", buildMetadataMarkerOnlyDB()}, } { if err := writeRawDB(target, db.name, db.data); err != nil { return fmt.Errorf("writing %s: %w", db.name, err) diff --git a/pkg/writer/rawmmdb.go b/pkg/writer/rawmmdb.go index 5936b79..5fba80c 100644 --- a/pkg/writer/rawmmdb.go +++ b/pkg/writer/rawmmdb.go @@ -285,6 +285,14 @@ func buildEmptyMapLastInMetadataDB() []byte { return buildSimpleDB(writeMetadataBlockEmptyMapLast) } +// buildMetadataMarkerOnlyDB returns a file that contains only the metadata +// marker (\xab\xcd\xefMaxMind.com) with no metadata bytes following. +// Readers should reject this as invalid metadata rather than allowing a +// zero-length metadata section to reach the decoder. +func buildMetadataMarkerOnlyDB() []byte { + return []byte(metadataMarker) +} + // buildEmptyArrayLastInMetadataDB creates a valid MMDB where the metadata // map's last field is "languages" (an empty array []). Tests the array // validation path of the same off-by-one bug. From 1486d037d53617e3f111933753ac52f80e1b4b1c Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 4 May 2026 16:05:19 +0000 Subject: [PATCH 2/2] Add test databases with search-tree records pointing into the separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three minimal variants of MaxMind-DB-test-ipv4-24.mmdb where node 0 has a record value in the half-open range [node_count+1, node_count+16) — i.e., pointing into the 16-byte separator between the search tree and data section. Readers should reject these as corrupt search trees rather than exposing data entries with underflowed offsets. -min-left: left record = node_count + 1 (first separator byte) -min-right: right record = node_count + 1 (verifies right-side check) -max-left: left record = node_count + 15 (last separator byte) Co-Authored-By: Claude Opus 4.7 (1M context) --- ...ibmaxminddb-separator-record-max-left.mmdb | Bin 0 -> 219 bytes ...ibmaxminddb-separator-record-min-left.mmdb | Bin 0 -> 219 bytes ...bmaxminddb-separator-record-min-right.mmdb | Bin 0 -> 219 bytes pkg/writer/baddata.go | 3 + pkg/writer/rawmmdb.go | 73 ++++++++++++++++-- 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 bad-data/libmaxminddb/libmaxminddb-separator-record-max-left.mmdb create mode 100644 bad-data/libmaxminddb/libmaxminddb-separator-record-min-left.mmdb create mode 100644 bad-data/libmaxminddb/libmaxminddb-separator-record-min-right.mmdb diff --git a/bad-data/libmaxminddb/libmaxminddb-separator-record-max-left.mmdb b/bad-data/libmaxminddb/libmaxminddb-separator-record-max-left.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..b503bcde277406bbbb560991d6d0ac9459a823f8 GIT binary patch literal 219 zcmZ9FAr8VY6hJ@A1QHyl-~u=@!nv}noCo^HU(>Rt=Jk@V-Pj&jS~lQo{jDx8fw=68nU9{ zN(r(??#9t~xj5e-R~WEBPa?FIl68O06nW{`hCcg9+5Q&sszO~hSdxc0)kcw!rqSWr Q$3~eWX`B+CPt?Ke1Js2}{r~^~ literal 0 HcmV?d00001 diff --git a/bad-data/libmaxminddb/libmaxminddb-separator-record-min-left.mmdb b/bad-data/libmaxminddb/libmaxminddb-separator-record-min-left.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..4a097268c463dc30534fa7067b0b298e8205670e GIT binary patch literal 219 zcmZ9FAr8VY6hJ@A1Qy3BxB!mKuw@R1X#U#I_+)FFwj+$AAZNJ-iiA6WL~#Y`Aba*D z?*SwLGZ^t&vCRVcaD9AAJe91~LYnrytyrV$#m=}EgJ|(!oH&y6Z1e!hShN1uP*fc^ zN{}^jKaE>}<>d~g!hjWe5}~)0ZQ`|1bCSMc=Bm_!^_CaVuW!$p;qO!SElm{l$=% z4eyL1CwepbIA^CT6b2)f7)Zs|QM&A{xuKvvyV&2}Gj?A^ve==m8muTll36jNv@|we R`hY?1NsBis@R2&0yaCA6OYi^y literal 0 HcmV?d00001 diff --git a/pkg/writer/baddata.go b/pkg/writer/baddata.go index d789a66..1da599b 100644 --- a/pkg/writer/baddata.go +++ b/pkg/writer/baddata.go @@ -32,6 +32,9 @@ func (w *Writer) WriteBadDataDBs(target string) error { {"libmaxminddb-empty-map-last-in-metadata.mmdb", buildEmptyMapLastInMetadataDB()}, {"libmaxminddb-empty-array-last-in-metadata.mmdb", buildEmptyArrayLastInMetadataDB()}, {"libmaxminddb-metadata-marker-only.mmdb", buildMetadataMarkerOnlyDB()}, + {"libmaxminddb-separator-record-min-left.mmdb", buildSeparatorRecordMinLeftDB()}, + {"libmaxminddb-separator-record-min-right.mmdb", buildSeparatorRecordMinRightDB()}, + {"libmaxminddb-separator-record-max-left.mmdb", buildSeparatorRecordMaxLeftDB()}, } { if err := writeRawDB(target, db.name, db.data); err != nil { return fmt.Errorf("writing %s: %w", db.name, err) diff --git a/pkg/writer/rawmmdb.go b/pkg/writer/rawmmdb.go index 5fba80c..b91b34c 100644 --- a/pkg/writer/rawmmdb.go +++ b/pkg/writer/rawmmdb.go @@ -122,12 +122,18 @@ func writeEmptyArray(buf []byte) int { // writeSearchTree writes a 1-node search tree with 24-bit records, // both pointing to the data section. func writeSearchTree(buf []byte, recordValue uint32) int { - buf[0] = byte((recordValue >> 16) & 0xFF) - buf[1] = byte((recordValue >> 8) & 0xFF) - buf[2] = byte(recordValue & 0xFF) - buf[3] = byte((recordValue >> 16) & 0xFF) - buf[4] = byte((recordValue >> 8) & 0xFF) - buf[5] = byte(recordValue & 0xFF) + return writeSearchTreeRecords(buf, recordValue, recordValue) +} + +// writeSearchTreeRecords writes a 1-node search tree with 24-bit records +// where the left and right records can hold different values. +func writeSearchTreeRecords(buf []byte, leftRecord, rightRecord uint32) int { + buf[0] = byte((leftRecord >> 16) & 0xFF) + buf[1] = byte((leftRecord >> 8) & 0xFF) + buf[2] = byte(leftRecord & 0xFF) + buf[3] = byte((rightRecord >> 16) & 0xFF) + buf[4] = byte((rightRecord >> 8) & 0xFF) + buf[5] = byte(rightRecord & 0xFF) return 6 } @@ -293,6 +299,61 @@ func buildMetadataMarkerOnlyDB() []byte { return []byte(metadataMarker) } +// buildSeparatorRecordDB creates a complete 1-node MMDB where the left and +// right records of node 0 hold the given values. With nodeCount = 1, any +// record value in the half-open range [2, 17) points into the 16-byte +// separator between the search tree and data section. Readers should reject +// such records as a corrupt search tree rather than exposing them as data +// entries with underflowed offsets. +func buildSeparatorRecordDB(leftRecord, rightRecord uint32) []byte { + const nodeCount = 1 + const buildEpoch = 1_000_000_000 + + buf := make([]byte, 1024) + pos := 0 + + pos += writeSearchTreeRecords(buf[pos:], leftRecord, rightRecord) + + // 16-byte null separator + pos += dataSeparatorSize + + // Data section: a simple map so a valid record (nodeCount + 16) can + // resolve to a real entry. + pos += writeMap(buf[pos:], 1) + pos += writeString(buf[pos:], "ip") + pos += writeString(buf[pos:], "test") + + pos += writeMetadataBlock(buf[pos:], nodeCount, buildEpoch) + + return buf[:pos] +} + +// buildSeparatorRecordMinLeftDB creates an MMDB whose node 0 left record +// equals nodeCount + 1 (the first byte of the data section separator). +func buildSeparatorRecordMinLeftDB() []byte { + const nodeCount = 1 + const validRecord = nodeCount + dataSeparatorSize + return buildSeparatorRecordDB(nodeCount+1, validRecord) +} + +// buildSeparatorRecordMinRightDB creates an MMDB whose node 0 right record +// equals nodeCount + 1. The left record is valid, so this exercises the +// right-record-corruption path independently. +func buildSeparatorRecordMinRightDB() []byte { + const nodeCount = 1 + const validRecord = nodeCount + dataSeparatorSize + return buildSeparatorRecordDB(validRecord, nodeCount+1) +} + +// buildSeparatorRecordMaxLeftDB creates an MMDB whose node 0 left record +// equals nodeCount + 15 (the last byte of the data section separator), +// exercising the upper boundary of the invalid range. +func buildSeparatorRecordMaxLeftDB() []byte { + const nodeCount = 1 + const validRecord = nodeCount + dataSeparatorSize + return buildSeparatorRecordDB(nodeCount+dataSeparatorSize-1, validRecord) +} + // buildEmptyArrayLastInMetadataDB creates a valid MMDB where the metadata // map's last field is "languages" (an empty array []). Tests the array // validation path of the same off-by-one bug.