This commit is contained in:
James Tucker 2024-04-27 19:32:47 +00:00 committed by GitHub
commit 9976c3f178
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 157 additions and 93 deletions

View File

@ -2,20 +2,6 @@
# both PRs and merged commits, and for the latter reports failures to slack.
name: CI
env:
# Our fuzz job, powered by OSS-Fuzz, fails periodically because we upgrade to
# new Go versions very eagerly. OSS-Fuzz is a little more conservative, and
# ends up being unable to compile our code.
#
# When this happens, we want to disable the fuzz target until OSS-Fuzz catches
# up. However, we also don't want to forget to turn it back on when OSS-Fuzz
# can once again build our code.
#
# This variable toggles the fuzz job between two modes:
# - false: we expect fuzzing to be happy, and should report failure if it's not.
# - true: we expect fuzzing is broken, and should report failure if it start working.
TS_FUZZ_CURRENTLY_BROKEN: false
on:
push:
branches:
@ -405,63 +391,61 @@ jobs:
fuzz:
# This target periodically breaks (see TS_FUZZ_CURRENTLY_BROKEN at the top
# of the file), so it's more complex than usual: the 'build fuzzers' step
# might fail, and depending on the value of 'TS_FUZZ_CURRENTLY_BROKEN', that
# might or might not be fine. The steps after the build figure out whether
# the success/failure is expected, and appropriately pass/fail the job
# overall accordingly.
#
# Practically, this means that all steps after 'build fuzzers' must have an
# explicit 'if' condition, because the default condition for steps is
# 'success()', meaning "only run this if no previous steps failed".
if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04
strategy:
matrix:
include:
- pkg: ./disco
test: FuzzDisco
- pkg: ./net/dns/resolver
test: FuzzClampEDNSSize
- pkg: ./net/stun
test: FuzzStun
- pkg: ./util/deephash
test: FuzzTime
- pkg: ./util/deephash
test: FuzzAddr
- pkg: ./util/hashx
test: Fuzz
steps:
- name: build fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
# continue-on-error makes steps.build.conclusion be 'success' even if
# steps.build.outcome is 'failure'. This means this step does not
# contribute to the job's overall pass/fail evaluation.
continue-on-error: true
- name: checkout
uses: actions/checkout@v3
- name: Restore Build Cache
uses: actions/cache@v3
with:
oss-fuzz-project-name: 'tailscale'
dry-run: false
language: go
- name: report unexpectedly broken fuzz build
if: steps.build.outcome == 'failure' && env.TS_FUZZ_CURRENTLY_BROKEN != 'true'
run: |
echo "fuzzer build failed, see above for why"
echo "if the failure is due to OSS-Fuzz not being on the latest Go yet,"
echo "set TS_FUZZ_CURRENTLY_BROKEN=true in .github/workflows/test.yml"
echo "to temporarily disable fuzzing until OSS-Fuzz works again."
exit 1
- name: report unexpectedly working fuzz build
if: steps.build.outcome == 'success' && env.TS_FUZZ_CURRENTLY_BROKEN == 'true'
run: |
echo "fuzzer build succeeded, but we expect it to be broken"
echo "please set TS_FUZZ_CURRENTLY_BROKEN=false in .github/workflows/test.yml"
echo "to reenable fuzz testing"
exit 1
- name: run fuzzers
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-go-2-
- name: Restore fuzz corpus cache
uses: actions/cache@v3
with:
path: |
~/.cache/go-build/fuzz
# Uses an incrementing key to constantly collide and upload, but this
# cache action is kept separate from any build cache action.
key: fuzz-${{ matrix.pkg }}-${{ matrix.test }}-${{ github.run_id }}
restore-keys: |
fuzz-${{ matrix.pkg }}-${{ matrix.test }}-
- name: fuzz ${{matrix.pkg}} ${{matrix.test}}
id: run
# Run the fuzzers whenever they're able to build, even if we're going to
# report a failure because TS_FUZZ_CURRENTLY_BROKEN is set to the wrong
# value.
if: steps.build.outcome == 'success'
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'tailscale'
fuzz-seconds: 300
dry-run: false
language: go
run: ./tool/go test -fuzz=${{ matrix.test }} -fuzztime=60s ${{ matrix.pkg }}
- name: upload crash
uses: actions/upload-artifact@v3
if: steps.run.outcome != 'success' && steps.build.outcome == 'success'
if: steps.run.outcome != 'success'
with:
name: artifacts
path: ./out/artifacts
name: testdata
path: ./**/testdata
depaware:
runs-on: ubuntu-22.04

95
disco/disco_fuzz_test.go Normal file
View File

@ -0,0 +1,95 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package disco
import (
"bytes"
"reflect"
"testing"
"golang.org/x/exp/slices"
)
func FuzzDisco(f *testing.F) {
f.Fuzz(func(t *testing.T, data1 []byte) {
if data1 == nil {
return
}
data2 := make([]byte, 0, len(data1))
m1, e1 := Parse(data1)
if m1 == nil || reflect.ValueOf(m1).IsNil() {
if e1 == nil {
t.Fatal("nil message and nil error!")
}
t.Logf("message result is actually nil, can't be serialized again")
return
}
data2 = m1.AppendMarshal(data2)
m2, e2 := Parse(data2)
if m2 == nil || reflect.ValueOf(m2).IsNil() {
if e2 == nil {
t.Fatal("nil message and nil error!")
}
t.Errorf("second message result is actually nil!")
}
t.Logf("m1: %#v", m1)
t.Logf("m2: %#v", m1)
t.Logf("data1:\n%x", data1)
t.Logf("data2:\n%x", data2)
if e1 != nil && e2 != nil {
if e1.Error() != e2.Error() {
t.Errorf("error mismatch: %v != %v", e1, e2)
}
return
}
// Explicitly ignore the case where the fuzzer made a different version
// byte, it's not interesting.
data1[1] = v0
// The protocol doesn't have a length at this layer, and so it will
// ignore meaningless trailing data such as a key that is more than 0
// bytes, but less than keylen bytes.
if len(data2) < len(data1) {
data1 = data1[:len(data2)]
}
if !bytes.Equal(data1, data2) {
t.Errorf("data mismatch:\n%x\n%x", data1, data2)
}
switch t1 := m1.(type) {
case *Ping:
t2, ok := m2.(*Ping)
if !ok {
t.Errorf("m1 and m2 are not the same type")
}
if *t1 != *t2 {
t.Errorf("m1 and m2 are not the same")
}
case *Pong:
t2, ok := m2.(*Pong)
if !ok {
t.Errorf("m1 and m2 are not the same type")
}
if *t1 != *t2 {
t.Errorf("m1 and m2 are not the same")
}
case *CallMeMaybe:
t2, ok := m2.(*CallMeMaybe)
if !ok {
t.Errorf("m1 and m2 are not the same type")
}
if !slices.Equal(t1.MyNumber, t2.MyNumber) {
t.Errorf("m1 and m2 are not the same")
}
default:
t.Fatalf("unknown message type %T", m1)
}
})
}

View File

@ -1,17 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build gofuzz
package disco
func Fuzz(data []byte) int {
m, _ := Parse(data)
newBytes := m.AppendMarshal(data)
parsedMarshall, _ := Parse(newBytes)
if m != parsedMarshall {
panic("Parsing error")
}
return 1
}

View File

@ -0,0 +1,14 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package stun
import "testing"
func FuzzStun(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
_, _, _ = ParseResponse(data)
_, _ = ParseBindingRequest(data)
})
}

View File

@ -1,12 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build gofuzz
package stun
func FuzzStunParser(data []byte) int {
_, _, _ = ParseResponse(data)
_, _ = ParseBindingRequest(data)
return 1
}