mirror of
https://github.com/prometheus/prometheus.git
synced 2026-02-03 20:39:32 -05:00
Merge pull request #17393 from roidelapluie/roidelapluie/fuzzing
Fuzzing: Move to go fuzzing
This commit is contained in:
commit
6b0b93d2d3
7 changed files with 463 additions and 19 deletions
55
.github/workflows/fuzzing.yml
vendored
55
.github/workflows/fuzzing.yml
vendored
|
|
@ -1,30 +1,47 @@
|
|||
name: CIFuzz
|
||||
name: fuzzing
|
||||
on:
|
||||
workflow_call:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
fuzzing:
|
||||
name: Run Go Fuzz Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
fuzz_test: [FuzzParseMetricText, FuzzParseOpenMetric, FuzzParseMetricSelector, FuzzParseExpr]
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
id: build
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@4bf20ff8dfda18ad651583ebca9fb17a7ce1940a # master
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
oss-fuzz-project-name: "prometheus"
|
||||
dry-run: false
|
||||
- name: Run Fuzzers
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@4bf20ff8dfda18ad651583ebca9fb17a7ce1940a # master
|
||||
# Note: Regularly check for updates to the pinned commit hash at:
|
||||
# https://github.com/google/oss-fuzz/tree/master/infra/cifuzz/actions/run_fuzzers
|
||||
persist-credentials: false
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
oss-fuzz-project-name: "prometheus"
|
||||
fuzz-seconds: 600
|
||||
dry-run: false
|
||||
- name: Upload Crash
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
go-version: 1.25.x
|
||||
- name: Run Fuzzing
|
||||
run: go test -fuzz=${{ matrix.fuzz_test }}$ -fuzztime=5m ./util/fuzzing
|
||||
continue-on-error: true
|
||||
id: fuzz
|
||||
- name: Upload Crash Artifacts
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: failure()
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
name: fuzz-artifacts-${{ matrix.fuzz_test }}
|
||||
path: promql/testdata/fuzz/${{ matrix.fuzz_test }}
|
||||
fuzzing_status:
|
||||
# This status check aggregates the individual matrix jobs of the fuzzing
|
||||
# step into a final status. Fails if a single matrix job fails, succeeds if
|
||||
# all matrix jobs succeed.
|
||||
name: Fuzzing
|
||||
runs-on: ubuntu-latest
|
||||
needs: [fuzzing]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Successful fuzzing
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) && !(contains(needs.*.result, 'cancelled')) }}
|
||||
run: exit 0
|
||||
- name: Failing or cancelled fuzzing
|
||||
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
||||
run: exit 1
|
||||
|
|
|
|||
5
Makefile
5
Makefile
|
|
@ -220,3 +220,8 @@ check-node-version:
|
|||
bump-go-version:
|
||||
@echo ">> bumping Go minor version"
|
||||
@./scripts/bump_go_version.sh
|
||||
|
||||
.PHONY: generate-fuzzing-seed-corpus
|
||||
generate-fuzzing-seed-corpus:
|
||||
@echo ">> Generating fuzzing seed corpus"
|
||||
@$(GO) generate -tags fuzzing ./util/fuzzing/corpus_gen
|
||||
|
|
|
|||
|
|
@ -113,6 +113,39 @@ func NewTestEngineWithOpts(tb testing.TB, opts promql.EngineOpts) *promql.Engine
|
|||
return ng
|
||||
}
|
||||
|
||||
// GetBuiltInExprs returns all the eval statement expressions from the built-in test files.
|
||||
func GetBuiltInExprs() ([]string, error) {
|
||||
files, err := fs.Glob(testsFs, "*/*.test")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var exprs []string
|
||||
for _, fn := range files {
|
||||
content, err := fs.ReadFile(testsFs, fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a minimal test struct just for parsing
|
||||
testInstance := &test{
|
||||
cmds: []testCommand{},
|
||||
}
|
||||
if err := testInstance.parse(string(content)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract expressions from eval commands
|
||||
for _, cmd := range testInstance.cmds {
|
||||
if evalCmd, ok := cmd.(*evalCmd); ok {
|
||||
exprs = append(exprs, evalCmd.expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return exprs, nil
|
||||
}
|
||||
|
||||
// RunBuiltinTests runs an acceptance test suite against the provided engine.
|
||||
func RunBuiltinTests(t TBRun, engine promql.QueryEngine) {
|
||||
RunBuiltinTestsWithStorage(t, engine, newTestStorage)
|
||||
|
|
|
|||
1
util/fuzzing/.gitignore
vendored
Normal file
1
util/fuzzing/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fuzz*_seed_corpus.zip
|
||||
122
util/fuzzing/corpus.go
Normal file
122
util/fuzzing/corpus.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// 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.
|
||||
|
||||
package fuzzing
|
||||
|
||||
import (
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
"github.com/prometheus/prometheus/promql/promqltest"
|
||||
)
|
||||
|
||||
// GetCorpusForFuzzParseMetricText returns the seed corpus for FuzzParseMetricText.
|
||||
func GetCorpusForFuzzParseMetricText() [][]byte {
|
||||
return [][]byte{
|
||||
[]byte(""),
|
||||
[]byte("metric_name 1.0"),
|
||||
[]byte("# HELP metric_name help text\n# TYPE metric_name counter\nmetric_name 1.0"),
|
||||
[]byte("o { quantile = \"1.0\", a = \"b\" } 8.3835e-05"),
|
||||
[]byte("# HELP api_http_request_count The total number of HTTP requests.\n# TYPE api_http_request_count counter\nhttp_request_count{method=\"post\",code=\"200\"} 1027 1395066363000"),
|
||||
[]byte("msdos_file_access_time_ms{path=\"C:\\\\DIR\\\\FILE.TXT\",error=\"Cannot find file:\\n\\\"FILE.TXT\\\"\"} 1.234e3"),
|
||||
[]byte("metric_without_timestamp_and_labels 12.47"),
|
||||
[]byte("something_weird{problem=\"division by zero\"} +Inf -3982045"),
|
||||
[]byte("http_request_duration_seconds_bucket{le=\"+Inf\"} 144320"),
|
||||
[]byte("go_gc_duration_seconds{ quantile=\"0.9\", a=\"b\"} 8.3835e-05"),
|
||||
[]byte("go_gc_duration_seconds{ quantile=\"1.0\", a=\"b\" } 8.3835e-05"),
|
||||
[]byte("go_gc_duration_seconds{ quantile = \"1.0\", a = \"b\" } 8.3835e-05"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetCorpusForFuzzParseOpenMetric returns the seed corpus for FuzzParseOpenMetric.
|
||||
func GetCorpusForFuzzParseOpenMetric() [][]byte {
|
||||
return [][]byte{
|
||||
[]byte(""),
|
||||
[]byte("# TYPE metric_name counter\nmetric_name_total 1.0"),
|
||||
[]byte("# HELP metric_name help text\n# TYPE metric_name counter\nmetric_name_total 1.0\n# EOF"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetCorpusForFuzzParseMetricSelector returns the seed corpus for FuzzParseMetricSelector.
|
||||
func GetCorpusForFuzzParseMetricSelector() []string {
|
||||
return []string{
|
||||
"",
|
||||
"metric_name",
|
||||
`metric_name{label="value"}`,
|
||||
`{label="value"}`,
|
||||
`metric_name{label=~"val.*"}`,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCorpusForFuzzParseExpr returns the seed corpus for FuzzParseExpr.
|
||||
func GetCorpusForFuzzParseExpr() ([]string, error) {
|
||||
// Enable experimental features to parse all test expressions.
|
||||
parser.EnableExperimentalFunctions = true
|
||||
parser.ExperimentalDurationExpr = true
|
||||
parser.EnableExtendedRangeSelectors = true
|
||||
defer func() {
|
||||
parser.EnableExperimentalFunctions = false
|
||||
parser.ExperimentalDurationExpr = false
|
||||
parser.EnableExtendedRangeSelectors = false
|
||||
}()
|
||||
|
||||
// Get built-in test expressions.
|
||||
builtInExprs, err := promqltest.GetBuiltInExprs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add additional seed corpus.
|
||||
additionalExprs := []string{
|
||||
"",
|
||||
"1",
|
||||
"metric_name",
|
||||
`"str"`,
|
||||
// Numeric literals
|
||||
".5",
|
||||
"5.",
|
||||
"123.4567",
|
||||
"5e3",
|
||||
"5e-3",
|
||||
"+5.5e-3",
|
||||
"0xc",
|
||||
"0755",
|
||||
"-0755",
|
||||
"+Inf",
|
||||
"-Inf",
|
||||
// Basic binary operations
|
||||
"1 + 1",
|
||||
"1 - 1",
|
||||
"1 * 1",
|
||||
"1 / 1",
|
||||
"1 % 1",
|
||||
// Comparison operators
|
||||
"1 == 1",
|
||||
"1 != 1",
|
||||
"1 > 1",
|
||||
"1 >= 1",
|
||||
"1 < 1",
|
||||
"1 <= 1",
|
||||
// Operations with identifiers
|
||||
"foo == 1",
|
||||
"foo * bar",
|
||||
"2.5 / bar",
|
||||
"foo and bar",
|
||||
"foo or bar",
|
||||
// Complex expressions
|
||||
"+1 + -2 * 1",
|
||||
"1 + 2/(3*1)",
|
||||
// Comment
|
||||
"#comment",
|
||||
}
|
||||
|
||||
return append(builtInExprs, additionalExprs...), nil
|
||||
}
|
||||
116
util/fuzzing/corpus_gen/main.go
Normal file
116
util/fuzzing/corpus_gen/main.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// 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"
|
||||
|
||||
"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.")
|
||||
}
|
||||
|
||||
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 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))
|
||||
|
||||
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)
|
||||
}
|
||||
150
util/fuzzing/fuzz_test.go
Normal file
150
util/fuzzing/fuzz_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// 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.
|
||||
|
||||
package fuzzing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/model/textparse"
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
// Input size above which we know that Prometheus would consume too much
|
||||
// memory. The recommended way to deal with it is check input size.
|
||||
// https://google.github.io/oss-fuzz/getting-started/new-project-guide/#input-size
|
||||
maxInputSize = 10240
|
||||
)
|
||||
|
||||
// Use package-scope symbol table to avoid memory allocation on every fuzzing operation.
|
||||
var symbolTable = labels.NewSymbolTable()
|
||||
|
||||
// FuzzParseMetricText fuzzes the metric parser with "text/plain" content type.
|
||||
//
|
||||
// Note that this is not the parser for the text-based exposition-format; that
|
||||
// lives in github.com/prometheus/client_golang/text.
|
||||
func FuzzParseMetricText(f *testing.F) {
|
||||
// Add seed corpus
|
||||
for _, corpus := range GetCorpusForFuzzParseMetricText() {
|
||||
f.Add(corpus)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, in []byte) {
|
||||
p, warning := textparse.New(in, "text/plain", symbolTable, textparse.ParserOptions{})
|
||||
if p == nil || warning != nil {
|
||||
// An invalid content type is being passed, which should not happen
|
||||
// in this context.
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
var err error
|
||||
for {
|
||||
_, err = p.Next()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
// We don't care about errors, just that we don't panic.
|
||||
_ = err
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzParseOpenMetric fuzzes the metric parser with "application/openmetrics-text" content type.
|
||||
func FuzzParseOpenMetric(f *testing.F) {
|
||||
// Add seed corpus
|
||||
for _, corpus := range GetCorpusForFuzzParseOpenMetric() {
|
||||
f.Add(corpus)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, in []byte) {
|
||||
p, warning := textparse.New(in, "application/openmetrics-text", symbolTable, textparse.ParserOptions{})
|
||||
if p == nil || warning != nil {
|
||||
// An invalid content type is being passed, which should not happen
|
||||
// in this context.
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
var err error
|
||||
for {
|
||||
_, err = p.Next()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
// We don't care about errors, just that we don't panic.
|
||||
_ = err
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzParseMetricSelector fuzzes the metric selector parser.
|
||||
func FuzzParseMetricSelector(f *testing.F) {
|
||||
// Add seed corpus
|
||||
for _, corpus := range GetCorpusForFuzzParseMetricSelector() {
|
||||
f.Add(corpus)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, in string) {
|
||||
if len(in) > maxInputSize {
|
||||
t.Skip()
|
||||
}
|
||||
_, err := parser.ParseMetricSelector(in)
|
||||
// We don't care about errors, just that we don't panic.
|
||||
_ = err
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzParseExpr fuzzes the expression parser.
|
||||
func FuzzParseExpr(f *testing.F) {
|
||||
parser.EnableExperimentalFunctions = true
|
||||
parser.ExperimentalDurationExpr = true
|
||||
parser.EnableExtendedRangeSelectors = true
|
||||
f.Cleanup(func() {
|
||||
parser.EnableExperimentalFunctions = false
|
||||
parser.ExperimentalDurationExpr = false
|
||||
parser.EnableExtendedRangeSelectors = false
|
||||
})
|
||||
|
||||
// Add seed corpus from built-in test expressions
|
||||
corpus, err := GetCorpusForFuzzParseExpr()
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
if len(corpus) < 1000 {
|
||||
f.Fatalf("loading exprs is likely broken: got %d expressions, expected at least 1000", len(corpus))
|
||||
}
|
||||
|
||||
for _, expr := range corpus {
|
||||
f.Add(expr)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, in string) {
|
||||
if len(in) > maxInputSize {
|
||||
t.Skip()
|
||||
}
|
||||
_, err := parser.ParseExpr(in)
|
||||
// We don't care about errors, just that we don't panic.
|
||||
_ = err
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue