Julien Pivotto efe305b898 fuzzing: generate libFuzzer dictionary for FuzzParseExpr
Export parser.Keywords() and add GetDictForFuzzParseExpr() so that
the corpus generator can produce a stable fuzzParseExpr.dict file
derived directly from the PromQL grammar rather than maintained by hand.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
2026-04-08 17:38:23 +02:00

238 lines
8.4 KiB
Go

// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build fuzzing
//go:generate go run -tags fuzzing .
package main
import (
"archive/zip"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"github.com/prometheus/prometheus/util/fuzzing"
)
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println("Successfully generated all seed corpus ZIP files and dictionary files.")
}
func run() error {
// Generate FuzzParseExpr seed corpus.
exprs, err := fuzzing.GetCorpusForFuzzParseExpr()
if err != nil {
return fmt.Errorf("failed to get corpus for FuzzParseExpr: %w", err)
}
if err := generateZipFromStrings("fuzzParseExpr", exprs); err != nil {
return fmt.Errorf("failed to generate FuzzParseExpr_seed_corpus.zip: %w", err)
}
fmt.Printf("Generated fuzzParseExpr_seed_corpus.zip with %d entries.\n", len(exprs))
// Generate FuzzParseExpr dictionary.
dict := fuzzing.GetDictForFuzzParseExpr()
if err := generateDictFile("fuzzParseExpr", dict); err != nil {
return fmt.Errorf("failed to generate fuzzParseExpr.dict: %w", err)
}
fmt.Printf("Generated fuzzParseExpr.dict with %d entries.\n", len(dict))
// Generate FuzzParseMetricSelector seed corpus.
selectors := fuzzing.GetCorpusForFuzzParseMetricSelector()
if err := generateZipFromStrings("fuzzParseMetricSelector", selectors); err != nil {
return fmt.Errorf("failed to generate FuzzParseMetricSelector_seed_corpus.zip: %w", err)
}
fmt.Printf("Generated fuzzParseMetricSelector_seed_corpus.zip with %d entries.\n", len(selectors))
// Generate FuzzParseMetricText seed corpus.
metrics := fuzzing.GetCorpusForFuzzParseMetricText()
if err := generateZipFromBytes("fuzzParseMetricText", metrics); err != nil {
return fmt.Errorf("failed to generate FuzzParseMetricText_seed_corpus.zip: %w", err)
}
fmt.Printf("Generated fuzzParseMetricText_seed_corpus.zip with %d entries.\n", len(metrics))
// Generate FuzzParseOpenMetric seed corpus.
openMetrics := fuzzing.GetCorpusForFuzzParseOpenMetric()
if err := generateZipFromBytes("fuzzParseOpenMetric", openMetrics); err != nil {
return fmt.Errorf("failed to generate FuzzParseOpenMetric_seed_corpus.zip: %w", err)
}
fmt.Printf("Generated fuzzParseOpenMetric_seed_corpus.zip with %d entries.\n", len(openMetrics))
// Generate FuzzXORChunk seed corpus.
xorSeeds := fuzzing.GetCorpusForFuzzXORChunk()
if err := generateZipFromXORChunkSeeds("fuzzXORChunk", xorSeeds); err != nil {
return fmt.Errorf("failed to generate fuzzXORChunk_seed_corpus.zip: %w", err)
}
fmt.Printf("Generated fuzzXORChunk_seed_corpus.zip with %d entries.\n", len(xorSeeds))
// Generate FuzzXOR2Chunk seed corpus.
xor2Seeds := fuzzing.GetCorpusForFuzzXOR2Chunk()
if err := generateZipFromXOR2ChunkSeeds("fuzzXOR2Chunk", xor2Seeds); err != nil {
return fmt.Errorf("failed to generate fuzzXOR2Chunk_seed_corpus.zip: %w", err)
}
fmt.Printf("Generated fuzzXOR2Chunk_seed_corpus.zip with %d entries.\n", len(xor2Seeds))
// Generate FuzzParseProtobuf seed corpus.
protobufSeeds, err := fuzzing.GetCorpusForFuzzParseProtobuf()
if err != nil {
return fmt.Errorf("failed to get corpus for FuzzParseProtobuf: %w", err)
}
if err := generateZipFromProtobufSeeds("fuzzParseProtobuf", protobufSeeds); err != nil {
return fmt.Errorf("failed to generate fuzzParseProtobuf_seed_corpus.zip: %w", err)
}
fmt.Printf("Generated fuzzParseProtobuf_seed_corpus.zip with %d entries.\n", len(protobufSeeds))
return nil
}
// generateZipFromBytes creates a seed corpus ZIP file from a slice of byte slices.
func generateZipFromBytes(fuzzName string, corpus [][]byte) error {
// Sort corpus deterministically.
sorted := make([][]byte, len(corpus))
copy(sorted, corpus)
sort.Slice(sorted, func(i, j int) bool {
return string(sorted[i]) < string(sorted[j])
})
// Create ZIP file in parent directory.
zipPath := filepath.Join("..", fuzzName+"_seed_corpus.zip")
zipFile, err := os.Create(zipPath)
if err != nil {
return fmt.Errorf("failed to create zip file: %w", err)
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// Add each corpus entry as a file.
for i, entry := range sorted {
fileName := fmt.Sprintf("expr%d", i)
writer, err := zipWriter.Create(fileName)
if err != nil {
return fmt.Errorf("failed to create zip entry %s: %w", fileName, err)
}
if _, err := writer.Write(entry); err != nil {
return fmt.Errorf("failed to write zip entry %s: %w", fileName, err)
}
}
return nil
}
// generateZipFromStrings creates a seed corpus ZIP file from a slice of strings.
func generateZipFromStrings(fuzzName string, corpus []string) error {
// Convert []string to [][]byte and delegate to generateZipFromBytes.
byteCorpus := make([][]byte, len(corpus))
for i, s := range corpus {
byteCorpus[i] = []byte(s)
}
return generateZipFromBytes(fuzzName, byteCorpus)
}
// generateZipFromSeedEntries creates a seed corpus ZIP file from pre-serialised
// Go fuzz corpus entries. Entries are sorted deterministically before writing.
func generateZipFromSeedEntries(fuzzName string, entries [][]byte) error {
sort.Slice(entries, func(i, j int) bool {
return string(entries[i]) < string(entries[j])
})
zipPath := filepath.Join("..", fuzzName+"_seed_corpus.zip")
zipFile, err := os.Create(zipPath)
if err != nil {
return fmt.Errorf("failed to create zip file: %w", err)
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
for i, entry := range entries {
fileName := fmt.Sprintf("seed%d", i)
writer, err := zipWriter.Create(fileName)
if err != nil {
return fmt.Errorf("failed to create zip entry %s: %w", fileName, err)
}
if _, err := writer.Write(entry); err != nil {
return fmt.Errorf("failed to write zip entry %s: %w", fileName, err)
}
}
return nil
}
// generateZipFromXORChunkSeeds creates a seed corpus ZIP file for fuzz functions
// with signature (int64, uint8, uint64), using the Go fuzz corpus file format.
func generateZipFromXORChunkSeeds(fuzzName string, seeds []fuzzing.ChunkFuzzSeed) error {
entries := make([][]byte, len(seeds))
for i, s := range seeds {
entries[i] = []byte(fmt.Sprintf("go test fuzz v1\nint64(%d)\nuint8(%d)\nuint64(%d)\n", s.Seed, s.N, s.NaNMask))
}
return generateZipFromSeedEntries(fuzzName, entries)
}
// generateZipFromXOR2ChunkSeeds creates a seed corpus ZIP file for FuzzXOR2Chunk.
func generateZipFromXOR2ChunkSeeds(fuzzName string, seeds []fuzzing.XOR2ChunkFuzzSeed) error {
entries := make([][]byte, len(seeds))
for i, s := range seeds {
entries[i] = []byte(fmt.Sprintf("go test fuzz v1\nint64(%d)\nuint8(%d)\nuint64(%d)\nuint8(%d)\n", s.Seed, s.N, s.NaNMask, s.STMode))
}
return generateZipFromSeedEntries(fuzzName, entries)
}
// generateZipFromProtobufSeeds creates a seed corpus ZIP file for FuzzParseProtobuf.
func generateZipFromProtobufSeeds(fuzzName string, seeds []fuzzing.ProtobufCorpusSeed) error {
entries := make([][]byte, len(seeds))
for i, s := range seeds {
entries[i] = []byte(fmt.Sprintf(
"go test fuzz v1\n[]byte(%s)\nbool(%v)\nbool(%v)\nbool(%v)\nbool(%v)\n",
strconv.Quote(string(s.Data)),
s.IgnoreNative,
s.ParseClassic,
s.ConvertNHCB,
s.TypeAndUnit,
))
}
return generateZipFromSeedEntries(fuzzName, entries)
}
// generateDictFile writes a libFuzzer dictionary file to the parent directory.
// Each token is written as a quoted string on its own line, sorted
// deterministically so the output is stable across runs.
func generateDictFile(fuzzName string, tokens []string) error {
sorted := make([]string, len(tokens))
copy(sorted, tokens)
sort.Strings(sorted)
dictPath := filepath.Join("..", fuzzName+".dict")
f, err := os.Create(dictPath)
if err != nil {
return fmt.Errorf("failed to create dict file: %w", err)
}
defer f.Close()
for _, token := range sorted {
if _, err := fmt.Fprintf(f, "%q\n", token); err != nil {
return fmt.Errorf("failed to write dict entry %q: %w", token, err)
}
}
return nil
}