Skip to content

Commit ddee32e

Browse files
authored
Merge pull request #219 from maxmind/greg/eng-4239
Add crafted bad-data MMDB files for libmaxminddb
2 parents 1923710 + 2a0c110 commit ddee32e

File tree

11 files changed

+506
-4
lines changed

11 files changed

+506
-4
lines changed

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,30 @@ subnets (IPv4 or IPv6).
33

44
This repository contains the spec for that format as well as test databases.
55

6+
# Generating Test Data
7+
8+
The `write-test-data` command generates the MMDB test files under `test-data/`
9+
and `bad-data/`.
10+
11+
When run from anywhere inside this repository, it auto-detects the repo root
12+
and uses default paths:
13+
14+
```bash
15+
go run ./cmd/write-test-data
16+
```
17+
18+
You can override any path with flags:
19+
20+
```bash
21+
go run ./cmd/write-test-data \
22+
-source ./source-data \
23+
-target /tmp/test-out \
24+
-bad-data /tmp/bad-out
25+
```
26+
627
# Copyright and License
728

8-
This software is Copyright (c) 2013 - 2025 by MaxMind, Inc.
29+
This software is Copyright (c) 2013 - 2026 by MaxMind, Inc.
930

1031
This is free software, licensed under the [Apache License, Version
1132
2.0](LICENSE-APACHE) or the [MIT License](LICENSE-MIT), at your option.

bad-data/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
These are corrupt databases that have been know to cause problems such as
1+
These are corrupt databases that have been known to cause problems such as
22
segfaults or unhandled errors on one or more MaxMind DB reader
33
implementations. Implementations _should_ return an appropriate error
44
or raise an exception on these databases.
55

6+
Databases are organized into subdirectories named after the reader
7+
implementation that exposed the issue (e.g., `libmaxminddb/`).
8+
9+
Note: `libmaxminddb/libmaxminddb-uint64-max-epoch.mmdb` contains a valid
10+
database structure with `build_epoch` set to `UINT64_MAX`. It may not produce
11+
a reader error but can cause overflow in time type conversions.
12+
613
If you find a corrupt test-sized database that crashes a MMDB reader library,
714
please feel free to add it here by creating a pull request.
813 Bytes
Binary file not shown.
2.15 KB
Binary file not shown.
2.73 KB
Binary file not shown.
219 Bytes
Binary file not shown.
218 Bytes
Binary file not shown.
219 Bytes
Binary file not shown.

cmd/write-test-data/main.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,37 @@
22
package main
33

44
import (
5+
"bufio"
56
"flag"
67
"fmt"
78
"os"
9+
"path/filepath"
10+
"strings"
811

912
"github.com/maxmind/MaxMind-DB/pkg/writer"
1013
)
1114

15+
const moduleName = "github.com/maxmind/MaxMind-DB"
16+
1217
func main() {
13-
source := flag.String("source", "", "Source data directory")
14-
target := flag.String("target", "", "Destination directory for the generated mmdb files")
18+
var defaultSource, defaultTarget, defaultBadData string
19+
if root, err := findRepoRoot(); err == nil {
20+
defaultSource = filepath.Join(root, "source-data")
21+
defaultTarget = filepath.Join(root, "test-data")
22+
defaultBadData = filepath.Join(root, "bad-data", "libmaxminddb")
23+
}
24+
25+
source := flag.String("source", defaultSource, "Source data directory")
26+
target := flag.String(
27+
"target",
28+
defaultTarget,
29+
"Destination directory for the generated mmdb files",
30+
)
31+
badData := flag.String(
32+
"bad-data",
33+
defaultBadData,
34+
"Destination directory for generated bad mmdb files",
35+
)
1536

1637
flag.Parse()
1738

@@ -65,4 +86,54 @@ func main() {
6586
fmt.Printf("writing GeoIP2 test databases: %+v\n", err)
6687
os.Exit(1)
6788
}
89+
90+
if *badData != "" {
91+
if err := w.WriteBadDataDBs(*badData); err != nil {
92+
fmt.Printf("writing bad data test databases: %+v\n", err)
93+
os.Exit(1)
94+
}
95+
}
96+
}
97+
98+
// findRepoRoot walks up from the current working directory looking for a
99+
// go.mod that belongs to this module. It returns the directory containing
100+
// that go.mod, or an error if none is found.
101+
func findRepoRoot() (string, error) {
102+
dir, err := os.Getwd()
103+
if err != nil {
104+
return "", fmt.Errorf("getting working directory: %w", err)
105+
}
106+
107+
for {
108+
if hasModuleFile(dir) {
109+
return dir, nil
110+
}
111+
parent := filepath.Dir(dir)
112+
if parent == dir {
113+
return "", fmt.Errorf(
114+
"could not find go.mod for %s in any parent directory",
115+
moduleName,
116+
)
117+
}
118+
dir = parent
119+
}
120+
}
121+
122+
// hasModuleFile reports whether dir contains a go.mod whose first "module"
123+
// directive matches moduleName.
124+
func hasModuleFile(dir string) bool {
125+
f, err := os.Open(filepath.Clean(filepath.Join(dir, "go.mod")))
126+
if err != nil {
127+
return false
128+
}
129+
defer f.Close()
130+
131+
scanner := bufio.NewScanner(f)
132+
for scanner.Scan() {
133+
line := strings.TrimSpace(scanner.Text())
134+
if after, ok := strings.CutPrefix(line, "module "); ok {
135+
return strings.TrimSpace(after) == moduleName
136+
}
137+
}
138+
return false
68139
}

pkg/writer/baddata.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package writer
2+
3+
import (
4+
"fmt"
5+
"net/netip"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/maxmind/mmdbwriter"
10+
"github.com/maxmind/mmdbwriter/mmdbtype"
11+
"go4.org/netipx"
12+
)
13+
14+
// WriteBadDataDBs writes intentionally corrupt or extreme MMDB databases
15+
// for testing error handling in reader implementations.
16+
func (w *Writer) WriteBadDataDBs(target string) error {
17+
//nolint:gosec // not security sensitive.
18+
if err := os.MkdirAll(target, os.ModePerm); err != nil {
19+
return fmt.Errorf("creating bad-data directory: %w", err)
20+
}
21+
22+
// Raw binary databases — can't use mmdbwriter because the data is
23+
// intentionally invalid or uses values mmdbwriter can't represent.
24+
for _, db := range []struct {
25+
name string
26+
data []byte
27+
}{
28+
{"libmaxminddb-oversized-array.mmdb", buildOversizedArrayDB()},
29+
{"libmaxminddb-oversized-map.mmdb", buildOversizedMapDB()},
30+
{"libmaxminddb-uint64-max-epoch.mmdb", buildUint64MaxEpochDB()},
31+
{"libmaxminddb-corrupt-search-tree.mmdb", buildCorruptSearchTreeDB()},
32+
} {
33+
if err := writeRawDB(target, db.name, db.data); err != nil {
34+
return fmt.Errorf("writing %s: %w", db.name, err)
35+
}
36+
}
37+
38+
// Deep nesting uses mmdbwriter — structurally valid, just 600 levels deep.
39+
if err := writeDeepNestingDB(target); err != nil {
40+
return fmt.Errorf("writing deep nesting database: %w", err)
41+
}
42+
43+
if err := writeDeepArrayNestingDB(target); err != nil {
44+
return fmt.Errorf("writing deep array nesting database: %w", err)
45+
}
46+
47+
return nil
48+
}
49+
50+
func writeRawDB(dir, name string, data []byte) error {
51+
path := filepath.Join(dir, name)
52+
if err := os.WriteFile(path, data, 0o644); err != nil {
53+
return fmt.Errorf("writing file: %w", err)
54+
}
55+
return nil
56+
}
57+
58+
// writeDeepArrayNestingDB creates an MMDB with 600 levels of nested arrays.
59+
// This exceeds libmaxminddb's MAXIMUM_DATA_STRUCTURE_DEPTH (512) and
60+
// should trigger MMDB_INVALID_DATA_ERROR during data extraction.
61+
func writeDeepArrayNestingDB(dir string) (retErr error) {
62+
dbWriter, err := mmdbwriter.New(
63+
mmdbwriter.Options{
64+
DatabaseType: "Test",
65+
BuildEpoch: 1_000_000_000,
66+
IPVersion: 4,
67+
RecordSize: 24,
68+
},
69+
)
70+
if err != nil {
71+
return fmt.Errorf("creating mmdbwriter: %w", err)
72+
}
73+
74+
// Build 600-level nested arrays: [[[... "x" ...]]]
75+
const depth = 600
76+
var value mmdbtype.DataType = mmdbtype.String("x")
77+
for range depth {
78+
value = mmdbtype.Slice{value}
79+
}
80+
81+
for _, cidr := range []string{"0.0.0.0/1", "128.0.0.0/1"} {
82+
prefix, err := netip.ParsePrefix(cidr)
83+
if err != nil {
84+
return fmt.Errorf("parsing prefix %s: %w", cidr, err)
85+
}
86+
if err := dbWriter.Insert(netipx.PrefixIPNet(prefix), value); err != nil {
87+
return fmt.Errorf("inserting %s: %w", cidr, err)
88+
}
89+
}
90+
91+
path := filepath.Join(dir, "libmaxminddb-deep-array-nesting.mmdb")
92+
outputFile, err := os.Create(filepath.Clean(path))
93+
if err != nil {
94+
return fmt.Errorf("creating file: %w", err)
95+
}
96+
defer func() {
97+
if err := outputFile.Close(); err != nil && retErr == nil {
98+
retErr = fmt.Errorf("closing file: %w", err)
99+
}
100+
}()
101+
102+
if _, err := dbWriter.WriteTo(outputFile); err != nil {
103+
return fmt.Errorf("writing file: %w", err)
104+
}
105+
106+
return nil
107+
}
108+
109+
// writeDeepNestingDB creates an MMDB with 600 levels of nested maps.
110+
// This exceeds libmaxminddb's MAXIMUM_DATA_STRUCTURE_DEPTH (512) and
111+
// should trigger MMDB_INVALID_DATA_ERROR during data extraction.
112+
func writeDeepNestingDB(dir string) (retErr error) {
113+
dbWriter, err := mmdbwriter.New(
114+
mmdbwriter.Options{
115+
DatabaseType: "Test",
116+
BuildEpoch: 1_000_000_000,
117+
IPVersion: 4,
118+
RecordSize: 24,
119+
},
120+
)
121+
if err != nil {
122+
return fmt.Errorf("creating mmdbwriter: %w", err)
123+
}
124+
125+
// Build 600-level nested structure: {"a": {"a": ... "x" ...}}
126+
const depth = 600
127+
var value mmdbtype.DataType = mmdbtype.String("x")
128+
for range depth {
129+
value = mmdbtype.Map{"a": value}
130+
}
131+
132+
// Insert for 0.0.0.0/1 and 128.0.0.0/1 to cover all IPv4 addresses.
133+
for _, cidr := range []string{"0.0.0.0/1", "128.0.0.0/1"} {
134+
prefix, err := netip.ParsePrefix(cidr)
135+
if err != nil {
136+
return fmt.Errorf("parsing prefix %s: %w", cidr, err)
137+
}
138+
if err := dbWriter.Insert(netipx.PrefixIPNet(prefix), value); err != nil {
139+
return fmt.Errorf("inserting %s: %w", cidr, err)
140+
}
141+
}
142+
143+
path := filepath.Join(dir, "libmaxminddb-deep-nesting.mmdb")
144+
outputFile, err := os.Create(filepath.Clean(path))
145+
if err != nil {
146+
return fmt.Errorf("creating file: %w", err)
147+
}
148+
defer func() {
149+
if err := outputFile.Close(); err != nil && retErr == nil {
150+
retErr = fmt.Errorf("closing file: %w", err)
151+
}
152+
}()
153+
154+
if _, err := dbWriter.WriteTo(outputFile); err != nil {
155+
return fmt.Errorf("writing file: %w", err)
156+
}
157+
158+
return nil
159+
}

0 commit comments

Comments
 (0)