Skip to content

Commit ec9ebd2

Browse files
committed
Add fuzzing tests for MMDB parsing
1 parent 6d5f7ab commit ec9ebd2

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed

fuzz_test.go

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
package maxminddb
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"net/netip"
7+
"os"
8+
"path/filepath"
9+
"strconv"
10+
"testing"
11+
12+
"github.com/oschwald/maxminddb-golang/v2/internal/decoder"
13+
)
14+
15+
// FuzzDatabase tests MMDB file parsing and IP address lookups.
16+
// This targets file format parsing, database initialization, and lookup operations.
17+
func FuzzDatabase(f *testing.F) {
18+
// Add all test MMDB files as seeds
19+
for _, filename := range getAllTestMMDBFiles() {
20+
if seedData, err := os.ReadFile(testFile(filename)); err == nil {
21+
f.Add(seedData)
22+
}
23+
}
24+
25+
// Add malformed data patterns
26+
f.Add([]byte("not an mmdb file"))
27+
f.Add([]byte{0x00, 0x01, 0x02, 0x03})
28+
f.Add(bytes.Repeat([]byte{0xFF}, 1024))
29+
f.Add([]byte{})
30+
31+
f.Fuzz(func(_ *testing.T, data []byte) {
32+
reader, err := FromBytes(data)
33+
if err != nil {
34+
return
35+
}
36+
defer func() { _ = reader.Close() }()
37+
38+
// Test IP lookup and data decoding
39+
result := reader.Lookup(netip.MustParseAddr("1.1.1.1"))
40+
if result.Err() == nil {
41+
var mapResult map[string]any
42+
_ = result.Decode(&mapResult)
43+
if mapResult != nil {
44+
var output any
45+
_ = result.DecodePath(&output, "country", "iso_code")
46+
}
47+
}
48+
})
49+
}
50+
51+
// FuzzLookup tests IP address lookups without decoding results.
52+
// This isolates the tree traversal and lookup logic from data decoding.
53+
func FuzzLookup(f *testing.F) {
54+
// Add test MMDB files as seeds to fuzz the databases
55+
for _, filename := range getAllTestMMDBFiles() {
56+
if seedData, err := os.ReadFile(testFile(filename)); err == nil {
57+
f.Add(seedData)
58+
}
59+
}
60+
61+
// Add malformed database patterns
62+
f.Add([]byte("not an mmdb file"))
63+
f.Add([]byte{0x00, 0x01, 0x02, 0x03})
64+
f.Add(bytes.Repeat([]byte{0xFF}, 512))
65+
f.Add([]byte{})
66+
67+
// Fixed test IP addresses to use for lookups
68+
testIPs := []netip.Addr{
69+
netip.MustParseAddr("1.1.1.1"),
70+
netip.MustParseAddr("216.160.83.56"), // Known test IP with data
71+
netip.MustParseAddr("2.125.160.216"), // Another known test IP
72+
netip.MustParseAddr("::1"), // IPv6
73+
netip.MustParseAddr("2001:218::"), // IPv6 with data
74+
}
75+
76+
f.Fuzz(func(_ *testing.T, data []byte) {
77+
reader, err := FromBytes(data)
78+
if err != nil {
79+
return
80+
}
81+
defer func() { _ = reader.Close() }()
82+
83+
if reader.Metadata.DatabaseType == "" {
84+
return
85+
}
86+
87+
// Test lookups with fixed IPs - focus on tree traversal logic
88+
for _, addr := range testIPs {
89+
result := reader.Lookup(addr)
90+
91+
// Check that we get a valid result (error or not)
92+
// Don't decode the data, just verify the lookup completed
93+
_ = result.Err()
94+
95+
// Also test that we can get basic result properties without decoding
96+
_ = result.Found()
97+
}
98+
})
99+
}
100+
101+
// FuzzDecodePath tests path-based decoding with fuzzed path segments.
102+
// This targets edge cases in path traversal logic.
103+
func FuzzDecodePath(f *testing.F) {
104+
// Use a complex test database with nested structures
105+
testDB := testFile("GeoIP2-City-Test.mmdb")
106+
reader, err := Open(testDB)
107+
if err != nil {
108+
f.Skip("Could not open test database")
109+
return
110+
}
111+
defer func() { _ = reader.Close() }()
112+
113+
// Use a known IP that has complex data
114+
result := reader.Lookup(netip.MustParseAddr("2.125.160.216"))
115+
if result.Err() != nil {
116+
f.Skip("Could not perform lookup")
117+
return
118+
}
119+
120+
// Add seed paths based on known data structure
121+
seedPaths := [][]string{
122+
{"country", "iso_code"},
123+
{"city", "names", "en"},
124+
{"location", "latitude"},
125+
{"location", "longitude"},
126+
{"postal", "code"},
127+
{"subdivisions", "0", "iso_code"},
128+
{"continent", "code"},
129+
{"registered_country", "iso_code"},
130+
{"country", "names", "en"},
131+
{"city", "geoname_id"},
132+
{"subdivisions", "0", "names", "en"},
133+
{"traits", "is_anonymous_proxy"},
134+
{"location", "accuracy_radius"},
135+
{"location", "metro_code"},
136+
{"location", "time_zone"},
137+
}
138+
139+
for _, path := range seedPaths {
140+
// Encode path as bytes with null separators
141+
pathBytes := make([]byte, 0)
142+
for i, segment := range path {
143+
if i > 0 {
144+
pathBytes = append(pathBytes, 0) // null separator
145+
}
146+
pathBytes = append(pathBytes, []byte(segment)...)
147+
}
148+
f.Add(pathBytes)
149+
}
150+
151+
// Add some edge case seeds
152+
f.Add([]byte("")) // empty path
153+
f.Add([]byte("nonexistent")) // single segment
154+
f.Add(bytes.Repeat([]byte("a"), 1000)) // very long segment
155+
f.Add([]byte("key\x00with\x00nulls")) // embedded nulls
156+
f.Add([]byte("123\x00456\x00789")) // numeric-looking paths
157+
f.Add([]byte("utf8\x00spëçîål")) // unicode characters
158+
159+
f.Fuzz(func(_ *testing.T, pathData []byte) {
160+
// Skip completely empty data
161+
if len(pathData) == 0 {
162+
return
163+
}
164+
165+
// Parse path data into segments using null byte separators
166+
segments := bytes.Split(pathData, []byte{0})
167+
if len(segments) == 0 {
168+
return
169+
}
170+
171+
// Convert byte segments to path elements
172+
var path []any
173+
for _, segment := range segments {
174+
// Skip empty segments
175+
if len(segment) == 0 {
176+
continue
177+
}
178+
179+
segmentStr := string(segment)
180+
// Try to convert numeric strings to integers for array indexing
181+
if num, isInt := parseSimpleInt(segmentStr); isInt {
182+
path = append(path, num)
183+
} else {
184+
path = append(path, segmentStr)
185+
}
186+
}
187+
188+
// Skip if we ended up with no path elements
189+
if len(path) == 0 {
190+
return
191+
}
192+
193+
// Try to decode with the fuzzed path
194+
var output any
195+
_ = result.DecodePath(&output, path...)
196+
197+
// Also test with different output types to exercise different decoding paths
198+
var stringOutput string
199+
_ = result.DecodePath(&stringOutput, path...)
200+
201+
var intOutput int
202+
_ = result.DecodePath(&intOutput, path...)
203+
204+
var mapOutput map[string]any
205+
_ = result.DecodePath(&mapOutput, path...)
206+
207+
var sliceOutput []any
208+
_ = result.DecodePath(&sliceOutput, path...)
209+
})
210+
}
211+
212+
// FuzzNetworks tests the Networks() iterator with malformed databases.
213+
// This focuses specifically on tree traversal and iteration logic.
214+
func FuzzNetworks(f *testing.F) {
215+
// Add test MMDB files as seeds
216+
for _, filename := range getAllTestMMDBFiles() {
217+
if seedData, err := os.ReadFile(testFile(filename)); err == nil {
218+
f.Add(seedData)
219+
}
220+
}
221+
222+
// Add malformed data patterns
223+
f.Add([]byte("not an mmdb file"))
224+
f.Add([]byte{0x00, 0x01, 0x02, 0x03})
225+
f.Add(bytes.Repeat([]byte{0xFF}, 512))
226+
227+
f.Fuzz(func(_ *testing.T, data []byte) {
228+
reader, err := FromBytes(data)
229+
if err != nil {
230+
return
231+
}
232+
defer func() { _ = reader.Close() }()
233+
234+
if reader.Metadata.DatabaseType == "" {
235+
return
236+
}
237+
238+
// Test Networks() iteration with conservative limits
239+
count := 0
240+
for result := range reader.Networks() {
241+
if result.Err() != nil || count >= 5 {
242+
break
243+
}
244+
count++
245+
var output any
246+
_ = result.Decode(&output)
247+
}
248+
})
249+
}
250+
251+
// FuzzDecode tests the ReflectionDecoder.Decode method with fuzzed data.
252+
// This targets data section parsing and reflection-based decoding logic.
253+
func FuzzDecode(f *testing.F) {
254+
// Add raw test data file as seed
255+
if rawData, err := os.ReadFile(testFile("maps-with-pointers.raw")); err == nil {
256+
f.Add(rawData)
257+
}
258+
259+
// Add validated test data from decoder tests
260+
testHexStrings := []string{
261+
// Float64 values
262+
"680000000000000000", // 0.0
263+
"683FE0000000000000", // 0.5
264+
"68400921FB54442EEA", // 3.14159265359
265+
"68405EC00000000000", // 123.0
266+
"6841D000000007F8F4", // 1073741824.12457
267+
"68BFE0000000000000", // -0.5
268+
"68C00921FB54442EEA", // -3.14159265359
269+
"68C1D000000007F8F4", // -1073741824.12457
270+
271+
// Float32 values
272+
"040800000000", // 0.0
273+
"04083F800000", // 1.0
274+
"04083F8CCCCD", // 1.1
275+
"04084048F5C3", // 3.14
276+
"0408461C3FF6", // 9999.99
277+
"0408BF800000", // -1.0
278+
"0408BF8CCCCD", // -1.1
279+
"0408C048F5C3", // -3.14
280+
"0408C61C3FF6", // -9999.99
281+
282+
// Integer values
283+
"0401ffffffff", // -1
284+
"0401ffffff01", // -255
285+
"020101f4", // 500
286+
287+
// Boolean values
288+
"0007", // false
289+
"0107", // true
290+
291+
// Maps
292+
"E0", // Empty map
293+
"e142656e43466f6f", // {"en": "Foo"}
294+
"e242656e43466f6f427a6843e4baba", // {"en": "Foo", "zh": "人"}
295+
"e1446e616d65e242656e43466f6f427a6843e4baba", // Nested map
296+
"e1496c616e677561676573020442656e427a68", // Map with array value
297+
298+
// Arrays
299+
"020442656e427a68", // ["en", "zh"]
300+
301+
// Strings
302+
"43466f6f", // "Foo"
303+
"42656e", // "en"
304+
"427a68", // "zh"
305+
}
306+
307+
for _, hexStr := range testHexStrings {
308+
if data, err := hex.DecodeString(hexStr); err == nil {
309+
f.Add(data)
310+
}
311+
}
312+
313+
// Add malformed data patterns
314+
f.Add([]byte{0xFF, 0xFF, 0xFF, 0xFF})
315+
f.Add([]byte{0x42, 0x48, 0x65, 0x6C, 0x6C, 0x6F})
316+
f.Add([]byte{0x60, 0x41, 0x61, 0x41, 0x62})
317+
f.Add([]byte{0xE1, 0x41, 0x61, 0x41, 0x62})
318+
319+
f.Fuzz(func(_ *testing.T, data []byte) {
320+
if len(data) == 0 {
321+
return
322+
}
323+
324+
reflectionDecoder := decoder.New(data)
325+
326+
// Test decoding into various types
327+
outputs := []any{
328+
new(map[string]any),
329+
new(string),
330+
new(int),
331+
new(uint32),
332+
new(float64),
333+
new(bool),
334+
new([]any),
335+
new([]string),
336+
new(map[string]string),
337+
new([]map[string]any),
338+
new(any),
339+
}
340+
341+
for _, output := range outputs {
342+
_ = reflectionDecoder.Decode(0, output)
343+
}
344+
345+
// Test different offsets
346+
for offset := uint(1); offset < uint(len(data)) && offset < 10; offset++ {
347+
var mapOutput map[string]any
348+
_ = reflectionDecoder.Decode(offset, &mapOutput)
349+
}
350+
})
351+
}
352+
353+
// parseSimpleInt converts numeric strings to integers with bounds checking.
354+
// Returns the integer and true if valid, or 0 and false if not a simple integer.
355+
func parseSimpleInt(s string) (int, bool) {
356+
num, err := strconv.Atoi(s)
357+
if err != nil || num < -1000 || num > 1000 {
358+
return 0, false
359+
}
360+
return num, true
361+
}
362+
363+
// getAllTestMMDBFiles returns smaller MMDB files from the test-data directory.
364+
// Large files are excluded to keep fuzzing fast and prevent timeouts.
365+
func getAllTestMMDBFiles() []string {
366+
testDataDir := filepath.Join("test-data", "test-data")
367+
entries, err := os.ReadDir(testDataDir)
368+
if err != nil {
369+
return nil
370+
}
371+
372+
var mmdbFiles []string
373+
for _, entry := range entries {
374+
if !entry.IsDir() && filepath.Ext(entry.Name()) == ".mmdb" {
375+
// Check file size - skip very large files for fuzzing performance
376+
if info, err := entry.Info(); err == nil && info.Size() < 5000 { // 5KB limit
377+
mmdbFiles = append(mmdbFiles, entry.Name())
378+
}
379+
}
380+
}
381+
return mmdbFiles
382+
}

0 commit comments

Comments
 (0)