-
Notifications
You must be signed in to change notification settings - Fork 68
Expand file tree
/
Copy pathfuzz_test.go
More file actions
443 lines (422 loc) · 13.4 KB
/
Copy pathfuzz_test.go
File metadata and controls
443 lines (422 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package strfmt
import (
"database/sql"
"encoding"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/go-openapi/testify/v2/require"
)
// Systematic fuzzing of every registered format parser.
//
// Each built-in format is reachable generically through the [Default] registry,
// which knows the format's name, concrete type and validator.
//
// A single harness ([fuzzFormat]) driven by the format name therefore
// exercises the whole parsing surface of every type:
//
// - Validates(name, input) — the registered validator
// - Parse(name, input) — UnmarshalText path
// - Scan(input) / Scan([]byte) — sql.Scanner path
// - UnmarshalJSON("input") — json.Unmarshaler path
// - String() / MarshalText() — marshaling of a parsed value
// - re-Parse(String()) — round-trip against the parser's own output
//
// The only property asserted is that none of these panic.
// That contract is universally true, even for parsers that normalize their input or that intentionally store
// raw strings and validate separately (URI, Email, Password, colors, ...).
//
// We deliberately do not assert "Validates ⇔ Parse succeeds": several types store the raw string in UnmarshalText and
// validate only through the separate Validator, so such an assertion would produce false failures rather
// than real findings.
//
// To keep the CI fuzz matrix small (it schedules one job per Fuzz target), formats are grouped into parser-family
// buckets (see [fuzzBuckets]), each exposed as a single Fuzz target that selects a format within its bucket from
// a fuzzed selector byte.
//
// Every registered format must have an entry in [fuzzFormatCorpus] and belong to a bucket;
// [TestFuzzFormatCoverage] fails otherwise, so this generalization cannot silently fall behind the registry as
// new formats are added.
func FuzzUUIDs(f *testing.F) { fuzzBucket(f, fuzzBuckets()["uuids"]) }
func FuzzNetwork(f *testing.F) { fuzzBucket(f, fuzzBuckets()["network"]) }
func FuzzNumeric(f *testing.F) { fuzzBucket(f, fuzzBuckets()["numeric"]) }
func FuzzTemporal(f *testing.F) { fuzzBucket(f, fuzzBuckets()["temporal"]) }
func FuzzMisc(f *testing.F) { fuzzBucket(f, fuzzBuckets()["misc"]) }
// fuzzBucket fuzzes a family of formats through a single target.
//
// The fuzzer picks the format within the bucket from a selector byte, and the input string is fed
// to the shared [fuzzFormat] harness.
// Seeds carry the selector of the format they belong to so every format's valid branch is primed.
func fuzzBucket(f *testing.F, formats []string) {
f.Helper()
for i, name := range formats {
sel := byte(i)
for _, seed := range fuzzFormatCorpus[name] {
f.Add(sel, seed)
}
}
// structural seeds are format-agnostic: add them once
// (the fuzzer mutates the selector to spread them across the bucket).
for _, seed := range fuzzCommonSeeds {
f.Add(byte(0), seed)
}
f.Fuzz(func(t *testing.T, sel byte, input string) {
name := formats[int(sel)%len(formats)]
fuzzFormat(t, name, input)
})
}
// fuzzBuckets groups the registered formats by parser family.
//
// Each bucket is exposed as a single Fuzz target, so CI schedules one fuzz job per bucket rather than one per format.
// Grouping by family keeps the inputs a bucket explores related, so the coverage-guided engine is not diluted the way
// a single all-formats target would be.
//
// Every registered format must belong to exactly one bucket; TestFuzzFormatCoverage enforces this.
func fuzzBuckets() map[string][]string {
return map[string][]string{
"uuids": {"uuid", "uuid3", "uuid4", "uuid5", "uuid7"},
"network": {"uri", "email", "hostname", "ipv4", "ipv6", "cidr", "mac"},
"numeric": {"isbn", "isbn10", "isbn13", "creditcard", "ssn"},
"temporal": {"date", "datetime", "durationiso8601", "durationhuman"},
"misc": {"bsonobjectid", "hexcolor", "rgbcolor", "password", "byte", "ulid", "currency", "country"},
}
}
// fuzzFormat exercises the whole parsing surface of a registered format against
// a single input and asserts that none of it panics. It is the shared harness
// behind every per-format Fuzz target.
func fuzzFormat(t *testing.T, name, input string) {
t.Helper()
// 1. the registered validator
require.NotPanicsf(t, func() { _ = Default.Validates(name, input) },
"Validates(%q, %q)", name, input)
// 2. the registry Parse path (UnmarshalText)
var parsed any
var perr error
require.NotPanicsf(t, func() { parsed, perr = Default.Parse(name, input) },
"Parse(%q, %q)", name, input)
// 3. the sql.Scanner path, from both string and []byte sources
require.NotPanicsf(t, func() {
if sc, ok := newFormatValue(name).(sql.Scanner); ok {
_ = sc.Scan(input)
}
}, "Scan(string) %q, %q", name, input)
require.NotPanicsf(t, func() {
if sc, ok := newFormatValue(name).(sql.Scanner); ok {
_ = sc.Scan([]byte(input))
}
}, "Scan([]byte) %q, %q", name, input)
// 4. the json.Unmarshaler path, with input as a JSON string
require.NotPanicsf(t, func() {
if ju, ok := newFormatValue(name).(json.Unmarshaler); ok {
quoted, err := json.Marshal(input)
if err != nil {
return
}
_ = ju.UnmarshalJSON(quoted)
}
}, "UnmarshalJSON(%q) for %q", input, name)
if perr != nil {
return
}
// 5. round-trip of a successful parse: marshaling never panics and the
// parser accepts its own String() output without panicking.
require.NotPanicsf(t, func() {
if s, ok := parsed.(fmt.Stringer); ok {
_, _ = Default.Parse(name, s.String())
}
}, "re-Parse(String()) for %q, %q", name, input)
require.NotPanicsf(t, func() {
if m, ok := parsed.(encoding.TextMarshaler); ok {
_, _ = m.MarshalText()
}
}, "MarshalText() for %q, %q", name, input)
}
// newFormatValue returns a fresh pointer to the concrete type registered under
// name (e.g. *UUID for "uuid"), or nil when the format is unknown.
func newFormatValue(name string) any {
tpe, ok := Default.GetType(name)
if !ok {
return nil
}
return reflect.New(tpe).Interface()
}
// TestFuzzFormatCoverage guards the generalization.
//
// Every format registered in the default registry must have a fuzz corpus AND belong to a fuzz bucket (and
// therefore be fuzzed by one of the Fuzz targets below). Adding a new format without wiring it up here fails this test.
//
// The special "testformat" registered by format_test.go's init is a test artifact, not a real format, and is skipped.
func TestFuzzFormatCoverage(t *testing.T) {
registry, ok := Default.(*defaultFormats)
require.TrueT(t, ok)
// each format appears in exactly one bucket
bucketOf := make(map[string]string)
for bucket, formats := range fuzzBuckets() {
for _, name := range formats {
if prev, dup := bucketOf[name]; dup {
require.Failf(t, "duplicate bucket assignment",
"format %q is in both buckets %q and %q", name, prev, bucket)
}
bucketOf[name] = bucket
}
}
for _, kf := range registry.data {
if kf.Name == "testformat" {
continue // test-only format registered by format_test.go
}
_, hasCorpus := fuzzFormatCorpus[kf.Name]
require.Truef(t, hasCorpus,
"format %q is registered but has no fuzz corpus in fuzz_test.go", kf.Name)
_, hasBucket := bucketOf[kf.Name]
require.Truef(t, hasBucket,
"format %q is registered but is not assigned to a fuzz bucket in fuzz_test.go", kf.Name)
}
// the reverse direction: no stale or mistyped corpus / bucket entries
for name := range fuzzFormatCorpus {
require.Truef(t, registry.ContainsName(name),
"fuzz corpus references %q, which is not a registered format", name)
}
for name := range bucketOf {
require.Truef(t, registry.ContainsName(name),
"fuzz bucket references %q, which is not a registered format", name)
_, hasCorpus := fuzzFormatCorpus[name]
require.Truef(t, hasCorpus,
"format %q is bucketed but has no fuzz corpus", name)
}
}
// fuzzCommonSeeds are structural inputs applied to every fuzz target, independently of the format-specific corpus.
//
// They prime the corpus with the kind of cruft that breaks naive parsers.
//
//nolint:gochecknoglobals // shared, read-only seed corpus for the fuzz targets
var fuzzCommonSeeds = []string{
"",
" ",
" ",
"\x00",
"a\x00b",
"\t\n\r",
"\ufeff", // BOM
"\xff\xfe", // invalid UTF-8
"-",
"+",
"0",
"null",
"[]",
"字", //nolint:gosmopolitan // deliberately a non-ASCII / non-Latin seed
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", // long-ish
}
// fuzzFormatCorpus maps each registered format name to seed inputs.
// Seeds mix valid, borderline and known-tricky invalid values so the fuzzer
// starts from both sides of every validator.
//
//nolint:gochecknoglobals // shared, read-only seed corpus for the fuzz targets
var fuzzFormatCorpus = map[string][]string{
"bsonobjectid": {
"507f1f77bcf86cd799439011",
"507F1F77BCF86CD799439011",
"507f1f77bcf86cd79943901", // too short
"507f1f77bcf86cd7994390111", // too long
"zzzzzzzzzzzzzzzzzzzzzzzz", // non-hex
},
"date": {
"2014-12-15",
"0001-01-01",
"2014-12-15T00:00:00Z", // date-time, not date
"2014-13-15", // invalid month
"15-12-2014", // wrong order
},
"datetime": {
"2011-08-18T18:03:37.123Z",
"2011-08-18T19:03:37.123+01:00",
"2011-08-18T19:03:37.123+0100",
"2014-12-15T08:00:00.000Z",
"2014-12-15T08:00", // missing seconds/zone
"2014-12-15", // date only
},
"durationhuman": {
"1s",
"300ms",
"-1.5h",
"2h45m",
".5 week",
"2 minutes 45 seconds",
"300 ms",
" 1s ",
"x",
"1s1s1s 1s 1s ",
},
"durationiso8601": {
// strict-valid
"P1Y2M3DT4H5M6S",
"P2W",
"P1DT12H",
"PT0S",
"P4Y",
"PT1H0M5S",
// leniency the strict handle must reject
"PT0.5S", // fraction
"-P1D", // sign
" P1D", // surrounding space
"P1Y2D", // gap (non-contiguous)
"P1Y2W", // W combined with other components
"P२Y", // non-ASCII (Devanagari) digit
"P", // no components
"PT", // no components after T
"P1", // value without designator
// overflow boundaries (the bug the fraction path once had, and the uint64 / int64 ceilings)
"PT18446744073.9S", // fraction path near the 1<<64 gap
"PT18446744073709551616S", // > MaxUint64
"PT9223372036854775807S", // ~ MaxInt64 seconds (overflows nanoseconds)
"P9999999999999999999Y",
},
"uri": {
"http://foo.bar/baz?q=1#frag",
"mailto:a@b.c",
"urn:isbn:0451450523",
"//no-scheme",
"http://[::1]:8080/",
},
"email": {
"somebody@somewhere.com",
"a@b.c",
"somebody@somewhere@com",
"no-at-sign",
"“weird”@example.com",
},
"hostname": {
"www.example.com",
"localhost",
"www.xn--1b4c3d.london",
"-leadinghyphen.com",
"toolonglabel-" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
},
"ipv4": {
"192.168.254.1",
"0.0.0.0",
"255.255.255.255",
"198.168.254.2.2",
"::1", // v6, not v4
},
"ipv6": {
"::1",
"2001:db8:a0b:12f0::1",
"fe80::1%eth0",
"127.0.0.1", // v4, not v6
"::g",
},
"cidr": {
"192.0.2.1/24",
"2001:db8:a0b:12f0::1/32",
"198.168.254.2", // missing prefix
"192.0.2.1/33", // prefix out of range
},
"mac": {
"01:02:03:04:05:06",
"01-02-03-04-05-06",
"0102.0304.0506",
"01:02:03:04:05", // too short
},
"uuid": {
"6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"6ba7b8109dad11d180b400c04fd430c8",
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"not-a-uuid",
"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
},
"uuid3": {
"6fa459ea-ee8a-3ca4-894e-db77e160355e",
"6fa459eaee8a3ca4894edb77e160355e",
"f47ac10b-58cc-4372-a567-0e02b2c3d479", // v4, not v3
},
"uuid4": {
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"f47ac10b58cc4372a5670e02b2c3d479",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8", // v1, not v4
},
"uuid5": {
"886313e1-3b8a-5372-9b90-0c9aee199e5d",
"886313e13b8a53729b900c9aee199e5d",
"f47ac10b-58cc-4372-a567-0e02b2c3d479", // v4, not v5
},
"uuid7": {
"01890a5d-ac96-774b-bcce-b302099a8057",
"01890a5dac96774bbcceb302099a8057",
"f47ac10b-58cc-4372-a567-0e02b2c3d479", // v4, not v7
},
"isbn": {
"0321751043",
"978-0321751041",
"836217463", // bad checksum
},
"isbn10": {
"0321751043",
"0-321-75104-3",
"836217463", // bad checksum
},
"isbn13": {
"978-0321751041",
"9780321751041",
"978-0321751042", // bad checksum
},
"creditcard": {
"4111-1111-1111-1111",
"4111111111111111",
"9999-9999-9999-999", // bad
},
"ssn": {
"111-11-1111",
"078-05-1120",
"111111111", // missing separators
},
"hexcolor": {
"#FFFFFF",
"#fff",
"#000000",
"#fffffffz", // invalid
"FFFFFF", // missing hash
},
"rgbcolor": {
"rgb(255,255,255)",
"rgb(0,0,0)",
"rgb(300,0,0)", // out of range
"rgb(1,2)", // missing component
},
"password": {
"super secret stuff here",
"",
"éàü",
},
"byte": {
"aGVsbG8=", // "hello", std/url compatible
"VGhpcyBpcyBhIHRlc3Q=", // "This is a test"
"ZWxpemFiZXRocG9zZXk", // missing pad char
"_-_-", // url-safe alphabet
"++//", // std alphabet (not url-safe)
},
"ulid": {
"01EYXZVGBHG26MFTG4JWR4K558",
"00000000000000000000000000",
"7ZZZZZZZZZZZZZZZZZZZZZZZZZ",
"80000000000000000000000000", // overflows timestamp
"0123456789ABCDEFGHILMNOPQR", // contains excluded letters
},
"currency": {
"",
"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
" Z ",
"01",
"EUR",
},
"country": {
"",
"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
" Z ",
"01",
"XYZ",
"AUS",
},
}