After working on the
gomarkdown Markdown parser, I wanted to check that unexpected input would not crash it or make it hang. Fuzzing found bugs that my hand-written tests had missed.
The original investigation used go-fuzz. Since Go 1.18, fuzzing is built into go test, so no separate fuzzing tool or build tag is needed.
What is fuzzing?
A fuzzer mutates inputs and uses coverage feedback to explore paths through a program. A test can report an error or panic when it finds a problem. For a parser, even the simple property “arbitrary input must not panic” is useful. Fuzzing can uncover bugs; it does not prove that a parser is correct or secure.
Write a fuzz test
In an existing Go module, add the parser dependency:
go get github.com/gomarkdown/markdown
Save this as markdown_fuzz_test.go in a directory containing package markdowncheck (or change the package name to match your code):
package markdowncheck
import (
"testing"
"github.com/gomarkdown/markdown"
)
func FuzzMarkdown(f *testing.F) {
f.Add([]byte("# Heading\n\nA paragraph with *emphasis*.\n"))
f.Add([]byte("[link](https://example.com)\n"))
f.Add([]byte{})
f.Fuzz(func(t *testing.T, data []byte) {
markdown.Parse(data, nil)
})
}
Each call gets a fresh parser. Keep fuzz targets deterministic and avoid sharing mutable state across inputs. If you also want to exercise HTML rendering, call markdown.ToHTML(data, nil, nil) in the target.
Run it
From the package directory:
go test -run=^$ -fuzz=^FuzzMarkdown$ -fuzztime=30s
Omit -fuzztime to keep searching until you stop the process. Run one fuzz target at a time; -fuzz must match exactly one target in one package.
Go saves a failing input under testdata/fuzz/FuzzMarkdown/ and prints a command to reproduce it. Commit that file with the fix: ordinary go test runs the seed corpus, including saved regression cases. Coverage-expanding inputs are also retained in the build cache for later fuzzing runs.
Seed files in that directory use Go’s fuzz corpus encoding. Do not copy raw Markdown files there. To reuse existing fixtures, read them before f.Fuzz and pass their bytes to f.Add.
The original results
My parser descended from the widely used Blackfriday library, which itself descended from a C library. Even so, the original go-fuzz run found three separate issues that caused a crash or an infinite loop.
Those bugs were fixed in
this change,
this change, and
this change, with regression tests. These are historical results, not bugs found by a new run of the example above.