Repository navigation
Expand file tree
/
Copy pathvp8_decoder_decode_fuzz_test.go
More file actions
184 lines (174 loc) · 5.6 KB
/
Copy pathvp8_decoder_decode_fuzz_test.go
File metadata and controls
184 lines (174 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package govpx_test
import (
"errors"
"testing"
"github.com/thesyncim/govpx"
)
// FuzzVP8DecoderDecode is the non-oracle valid-input variant of the VP8
// decoder fuzz: it builds a tiny YUV image from the fuzz []byte, encodes
// it as a real VP8 keyframe via the public encoder, then decodes the
// resulting packet and asserts the roundtrip preserves dimensions and
// produces a non-empty visible frame.
//
// The complementary FuzzVP8DecoderMalformedPackets fuzzes the rejection
// path; this one fuzzes the happy path and pins the post-decode shape
// so a regression on output strides, plane sizing, or FrameInfo
// dimensions surfaces immediately.
func FuzzVP8DecoderDecode(f *testing.F) {
seeds := [][]byte{
nil,
{},
{0x00},
{0xff, 0x00, 0x80, 0x40, 0x20, 0x10},
// A short repeating pattern so the encoder receives flat luma.
{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80},
// Mixed-noise seed.
{0x10, 0x42, 0x87, 0xab, 0xc0, 0x55, 0xaa, 0xff, 0x00, 0x33, 0x66, 0x99},
}
for _, seed := range seeds {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("decode-roundtrip fuzz panicked on %d-byte input: %v", len(data), r)
}
}()
const width, height = 32, 32
src := vp8FuzzYUVNoiseImage(width, height, data)
packet, ok := vp8FuzzEncodeOnce(t, width, height, src)
if !ok {
// Encoder declined this seed (e.g. rate control dropped the
// frame, or the encoder rejected the config). Not interesting
// here; the test is still considered a pass.
return
}
d, err := govpx.NewVP8Decoder(govpx.DecoderOptions{MaxWidth: width, MaxHeight: height})
if err != nil {
t.Fatalf("NewVP8Decoder: %v", err)
}
defer func() {
_ = d.Close()
}()
dst := newVP8FacadeImage(width, height)
info, err := d.DecodeInto(packet, &dst)
if err != nil {
// A valid encoder output decoded by a matching decoder must
// succeed. If it fails, only the documented sentinels are
// acceptable; anything else (or a non-sentinel) is a bug.
if !errors.Is(err, govpx.ErrInvalidData) &&
!errors.Is(err, govpx.ErrInvalidConfig) &&
!errors.Is(err, govpx.ErrFrameRejected) {
t.Fatalf("DecodeInto on freshly-encoded keyframe returned unexpected error: %v", err)
}
return
}
if info.Width != width || info.Height != height {
t.Fatalf("FrameInfo dimensions = %dx%d, want %dx%d",
info.Width, info.Height, width, height)
}
if !info.KeyFrame {
t.Fatalf("first encoded packet not reported as keyframe; FrameInfo=%+v", info)
}
if !info.ShowFrame {
t.Fatalf("first encoded packet not reported as visible; FrameInfo=%+v", info)
}
// Roundtrip must produce a usable I420 frame: visible dimensions
// match, plane lengths cover the visible region, and the Y plane
// is not entirely zero (would indicate the decoder wrote nothing).
if dst.Width != width || dst.Height != height {
t.Fatalf("dst dimensions changed: %dx%d (want %dx%d)",
dst.Width, dst.Height, width, height)
}
if len(dst.Y) < width*height {
t.Fatalf("dst.Y too short: len=%d want>=%d", len(dst.Y), width*height)
}
uvSize := ((width + 1) >> 1) * ((height + 1) >> 1)
if len(dst.U) < uvSize || len(dst.V) < uvSize {
t.Fatalf("dst chroma too short: U=%d V=%d want>=%d",
len(dst.U), len(dst.V), uvSize)
}
nonZero := false
for _, b := range dst.Y[:width*height] {
if b != 0 {
nonZero = true
break
}
}
if !nonZero {
// All-zero Y can be legitimate if the fuzz input happened to
// produce a pure-black input — flag only when the source was
// non-zero so we don't false-positive on intentional black.
srcNonZero := false
for _, b := range src.Y[:width*height] {
if b != 0 {
srcNonZero = true
break
}
}
if srcNonZero {
t.Fatalf("decoded Y plane is all zero but source had non-zero samples")
}
}
})
}
// vp8FuzzYUVNoiseImage builds a width×height I420 image whose plane samples
// are derived from data via a simple deterministic XOR/rotate scheme. Empty
// data produces a 128-grey image.
func vp8FuzzYUVNoiseImage(width, height int, data []byte) govpx.Image {
img := newVP8FacadeImage(width, height)
if len(data) == 0 {
for i := range img.Y {
img.Y[i] = 128
}
for i := range img.U {
img.U[i] = 128
}
for i := range img.V {
img.V[i] = 128
}
return img
}
step := len(data)
for i := range img.Y {
img.Y[i] = data[i%step] ^ byte(i)
}
for i := range img.U {
img.U[i] = data[i%step] ^ byte(i*3)
}
for i := range img.V {
img.V[i] = data[i%step] ^ byte(i*5)
}
return img
}
// vp8FuzzEncodeOnce encodes a single frame at fixed CBR/realtime settings
// suitable for the fuzz harness. Returns (packet, true) on a non-empty
// emitted keyframe, (nil, false) otherwise. This keeps the fuzz body
// focused on the decode-side assertions.
func vp8FuzzEncodeOnce(t *testing.T, width, height int, src govpx.Image) ([]byte, bool) {
t.Helper()
e, err := govpx.NewVP8Encoder(govpx.EncoderOptions{
Width: width,
Height: height,
FPS: 30,
RateControlMode: govpx.RateControlCBR,
TargetBitrateKbps: 500,
MinQuantizer: 4,
MaxQuantizer: 56,
Deadline: govpx.DeadlineRealtime,
CpuUsed: 8,
KeyFrameInterval: 120,
BufferSizeMs: 600,
BufferInitialSizeMs: 400,
BufferOptimalSizeMs: 500,
})
if err != nil {
return nil, false
}
dst := make([]byte, width*height*4+1024)
result, err := e.EncodeInto(dst, src, 0, 1, govpx.EncodeForceKeyFrame)
if err != nil || result.Dropped || len(result.Data) == 0 {
return nil, false
}
return append([]byte(nil), result.Data...), true
}