mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-16 16:04:30 -04:00
Prior to this change there were two problems with our fuzzing for oss-fuzz: 1. There was an issue if the fuzzing spanned two files (mingled with the testing). 2. The fuzzing needs to be part of the implementation package (no _test packages). This change fixes that by moving all package fuzzing into a common `fuzz_test.go` within the package. Updates https://github.com/tailscale/corp/issues/46608 Change-Id: I0b95edcd0df946f723eea32f575c679214c0b202 Signed-off-by: Mike Jensen <mikej@tailscale.com>
264 lines
9.0 KiB
Go
264 lines
9.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cobs
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"math/rand/v2"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
var x = func(s string) []byte { return must.Get(hex.DecodeString(s)) }
|
|
var n = func(s string, n int) string { return strings.Repeat(s, n) }
|
|
var tests = []struct {
|
|
decoded []byte
|
|
encoded []byte
|
|
errDecoded error // non-nil implies that encode tests are skipped
|
|
padEncoded bool // whether there is a trailing empty block after full block
|
|
}{
|
|
{decoded: x(n("00", 1)), encoded: x(n("01", 2))},
|
|
{decoded: x("11" + n("00", 8)), encoded: x("02" + "11" + n("01", 8))},
|
|
{decoded: x("11" + n("00", 8) + "22"), encoded: x("02" + "11" + n("01", 7) + "02" + "22")},
|
|
{decoded: x(n("00", 8) + "11" + n("00", 8)), encoded: x(n("01", 8) + "02" + "11" + n("01", 8))},
|
|
{decoded: x(n("01", 253)), encoded: x("FE" + n("01", 253))},
|
|
{decoded: x(n("01", 254)), encoded: x("FF" + n("01", 254))},
|
|
{decoded: x(n("01", 254)), encoded: x("FF" + n("01", 254) + "01"), padEncoded: true},
|
|
{decoded: x(n("01", 255)), encoded: x("FF" + n("01", 254) + "02" + "01")},
|
|
{decoded: x(n("01", 254) + "00"), encoded: x("FF" + n("01", 254) + "0101")},
|
|
{decoded: x(n("01", 508)), encoded: x("FF" + n("01", 254) + "FF" + n("01", 254))},
|
|
{decoded: x(n("01", 509)), encoded: x("FF" + n("01", 254) + "FF" + n("01", 254) + "02" + "01")},
|
|
{decoded: x(n("01", 253) + "FF"), encoded: x("FF" + n("01", 253) + "FF")},
|
|
|
|
// Test examples from https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing.
|
|
{decoded: x(""), encoded: x("01")},
|
|
{decoded: x("00"), encoded: x("0101")},
|
|
{decoded: x("01"), encoded: x("0201")},
|
|
{decoded: x("0000"), encoded: x("010101")},
|
|
{decoded: x("001100"), encoded: x("01021101")},
|
|
{decoded: x("11220033"), encoded: x("0311220233")},
|
|
{decoded: x("11223344"), encoded: x("0511223344")},
|
|
{decoded: x("11000000"), encoded: x("0211010101")},
|
|
{decoded: x("010203" + n("FF", 249) + "FDFE"), encoded: x("FF010203" + n("FF", 249) + "FDFE")},
|
|
{decoded: x("010203" + n("FF", 249) + "FDFE"), encoded: x("FF010203" + n("FF", 249) + "FDFE01"), padEncoded: true},
|
|
{decoded: x("000102" + n("FF", 249) + "FCFDFE"), encoded: x("01FF0102" + n("FF", 249) + "FCFDFE")},
|
|
{decoded: x("010203" + n("FF", 249) + "FDFEFF"), encoded: x("FF010203" + n("FF", 249) + "FDFE02FF")},
|
|
{decoded: x("020304" + n("FF", 249) + "FEFF00"), encoded: x("FF020304" + n("FF", 249) + "FEFF0101")},
|
|
{decoded: x("030405" + n("FF", 249) + "FF0001"), encoded: x("FE030405" + n("FF", 249) + "FF0201")},
|
|
|
|
// Test boundary conditions of optimization for consecutive zeros.
|
|
{decoded: x(n("00", 7)), encoded: x(n("01", 7) + "01")},
|
|
{decoded: x(n("00", 7) + "FF"), encoded: x(n("01", 7) + "02FF")},
|
|
{decoded: x(n("00", 8)), encoded: x(n("01", 8) + "01")},
|
|
{decoded: x(n("00", 8) + "FF"), encoded: x(n("01", 8) + "02FF")},
|
|
{decoded: x(n("00", 9)), encoded: x(n("01", 9) + "01")},
|
|
{decoded: x(n("00", 9) + "FF"), encoded: x(n("01", 9) + "02FF")},
|
|
{decoded: x(n("00", 15)), encoded: x(n("01", 15) + "01")},
|
|
{decoded: x(n("00", 15) + "FF"), encoded: x(n("01", 15) + "02FF")},
|
|
{decoded: x(n("00", 16)), encoded: x(n("01", 16) + "01")},
|
|
{decoded: x(n("00", 16) + "FF"), encoded: x(n("01", 16) + "02FF")},
|
|
{decoded: x(n("00", 17)), encoded: x(n("01", 17) + "01")},
|
|
{decoded: x(n("00", 17) + "FF"), encoded: x(n("01", 17) + "02FF")},
|
|
|
|
// Test detection of invalid COBS-encoded inputs.
|
|
{decoded: x(n("00", 1000)), encoded: x(n("01", 1001))},
|
|
{decoded: x(""), encoded: x(""), errDecoded: errUnexpectedEOF},
|
|
{decoded: x(""), encoded: x("02"), errDecoded: errUnexpectedEOF},
|
|
{decoded: x("0100"), encoded: x("020102"), errDecoded: errUnexpectedEOF},
|
|
{decoded: x(""), encoded: x("0301"), errDecoded: errUnexpectedEOF},
|
|
{decoded: x(""), encoded: x("00"), errDecoded: errUnexpectedNull},
|
|
{decoded: x("00"), encoded: x("0100"), errDecoded: errUnexpectedNull},
|
|
{decoded: x(""), encoded: x("0200"), errDecoded: errUnexpectedNull},
|
|
{decoded: x("0100"), encoded: x("020100"), errDecoded: errUnexpectedNull},
|
|
}
|
|
|
|
func Test(t *testing.T) {
|
|
for _, tt := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
t.Run("MaxEncodedLen", func(t *testing.T) {
|
|
if tt.errDecoded != nil || tt.padEncoded {
|
|
t.SkipNow() // padded encodings are not produced by AppendEncode
|
|
}
|
|
got := MaxEncodedLen(len(tt.decoded))
|
|
if got < len(tt.encoded) {
|
|
t.Errorf("MaxEncodedLen(%d) = %d, want >= %d", len(tt.decoded), got, len(tt.encoded))
|
|
}
|
|
})
|
|
|
|
t.Run("MinDecodedLen", func(t *testing.T) {
|
|
if tt.errDecoded != nil {
|
|
t.SkipNow()
|
|
}
|
|
got := MinDecodedLen(len(tt.encoded))
|
|
if got > len(tt.decoded) {
|
|
t.Errorf("MinDecodedLen(%d) = %d, want <= %d", len(tt.encoded), got, len(tt.decoded))
|
|
}
|
|
})
|
|
|
|
t.Run("numOverhead", func(t *testing.T) {
|
|
if tt.errDecoded != nil {
|
|
t.SkipNow()
|
|
}
|
|
got := numOverhead(tt.decoded)
|
|
if tt.padEncoded {
|
|
got++
|
|
}
|
|
want := len(tt.encoded) - len(tt.decoded)
|
|
if got != want {
|
|
t.Errorf("numOverhead = %d, want %d", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("AppendEncode", func(t *testing.T) {
|
|
if tt.errDecoded != nil {
|
|
t.SkipNow()
|
|
}
|
|
for _, prefix := range []string{"", "prefix"} {
|
|
t.Run("Prefix:"+prefix, func(t *testing.T) {
|
|
for _, overlap := range []bool{false, true} {
|
|
t.Run("Overlap:"+strconv.FormatBool(overlap), func(t *testing.T) {
|
|
dst := []byte(prefix)
|
|
src := tt.decoded
|
|
if overlap {
|
|
dst = append(dst, src...)
|
|
src = dst[len(prefix):]
|
|
}
|
|
dst = AppendEncode(dst[:len(prefix)], src)
|
|
if tt.padEncoded {
|
|
dst = append(dst, 0x01)
|
|
}
|
|
if d := cmp.Diff(dst, append([]byte(prefix), tt.encoded...)); d != "" {
|
|
t.Errorf("AppendEncode mismatch (-got +want):\n%s", d)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("AppendDecode", func(t *testing.T) {
|
|
for _, prefix := range []string{"", "prefix"} {
|
|
t.Run("Prefix:"+prefix, func(t *testing.T) {
|
|
for _, overlap := range []bool{false, true} {
|
|
t.Run("Overlap:"+strconv.FormatBool(overlap), func(t *testing.T) {
|
|
dst := []byte(prefix)
|
|
src := tt.encoded
|
|
if overlap {
|
|
dst = append(dst, src...)
|
|
src = dst[len(prefix):]
|
|
}
|
|
dst, err := AppendDecode(dst[:len(prefix)], src)
|
|
if d := cmp.Diff(dst, append([]byte(prefix), tt.decoded...)); d != "" {
|
|
t.Errorf("AppendDecode mismatch (-got +want):\n%s", d)
|
|
}
|
|
if err != tt.errDecoded {
|
|
t.Errorf("AppendDecode error = %v, want %v", err, tt.errDecoded)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// encodeNaive is a straightforward translation of COBS from the C code
|
|
// in the appendix of the paper by Stuart Cheshire and Mary Baker.
|
|
//
|
|
// This is used as the reference implementation by fuzzing to ensure
|
|
// that [appendEncodeForward] and [appendEncodeReverse] are consistent.
|
|
func encodeNaive(src []byte) (dst []byte) {
|
|
codeIdxPrev := len(dst)
|
|
codeIdxCurr := len(dst)
|
|
code := 0x01
|
|
dst = append(dst, 0) // placeholder for first block's code byte
|
|
|
|
finishBlock := func() {
|
|
dst[codeIdxCurr] = byte(code)
|
|
codeIdxPrev = codeIdxCurr
|
|
codeIdxCurr = len(dst)
|
|
code = 0x01
|
|
dst = append(dst, 0) // placeholder for next block's code byte
|
|
}
|
|
|
|
for _, b := range src {
|
|
if b == 0 {
|
|
finishBlock()
|
|
} else {
|
|
dst = append(dst, b)
|
|
code++
|
|
if code == 0xff {
|
|
finishBlock()
|
|
}
|
|
}
|
|
}
|
|
dst[codeIdxCurr] = byte(code) // final block, no trailing placeholder needed
|
|
|
|
// Optional space optimization: We can elide a final empty block
|
|
// if the previous block was a full group of non-zeros.
|
|
// The paper does not implement this part, but this is necessary
|
|
// since [appendEncodeForward] and [appendEncodeReverse] do this.
|
|
if code == 0x01 && dst[codeIdxPrev] == 0xff {
|
|
dst = dst[:len(dst)-1]
|
|
}
|
|
|
|
return dst
|
|
}
|
|
|
|
func Benchmark(b *testing.B) {
|
|
const length = 1 << 20
|
|
out := make([]byte, MaxEncodedLen(length))
|
|
testdata := []struct {
|
|
name string
|
|
encoded []byte
|
|
decoded []byte
|
|
}{{
|
|
name: "Zeros",
|
|
decoded: bytes.Repeat([]byte{0x00}, length),
|
|
}, {
|
|
name: "NonZeros",
|
|
decoded: bytes.Repeat([]byte{0xFF}, length),
|
|
}, {
|
|
name: "Random",
|
|
decoded: func() []byte {
|
|
b := make([]byte, length)
|
|
must.Get(new(rand.ChaCha8).Read(b))
|
|
return b
|
|
}(),
|
|
}}
|
|
for _, tt := range testdata {
|
|
tt.encoded = AppendEncode(nil, tt.decoded)
|
|
if string(must.Get(AppendDecode(out[:0], tt.encoded))) != string(tt.decoded) {
|
|
b.Fatal("Decode(Encode(...)) roundtrip mismatch")
|
|
}
|
|
b.Run("EncodeForward/"+tt.name, func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
b.SetBytes(length)
|
|
for b.Loop() {
|
|
out = appendEncodeForward(out[:0], tt.decoded)
|
|
}
|
|
})
|
|
b.Run("EncodeReverse/"+tt.name, func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
b.SetBytes(length)
|
|
for b.Loop() {
|
|
out = appendEncodeReverse(out[:0], tt.decoded)
|
|
}
|
|
})
|
|
b.Run("DecodeForward/"+tt.name, func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
b.SetBytes(length)
|
|
for b.Loop() {
|
|
out = must.Get(AppendDecode(out[:0], tt.encoded))
|
|
}
|
|
})
|
|
}
|
|
}
|