burterm/internal/fuzzer/mutate.go
2026-09-14 10:55:07 +03:00

149 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package fuzzer
import (
"encoding/binary"
"math/rand"
)
// mutateFunc — одна стратегия мутации: берёт данные, возвращает новую
// (не модифицирует исходный срез — воркеры работают конкурентно с общим
// корпусом, in-place мутация была бы гонкой данных).
type mutateFunc func(rng *rand.Rand, data []byte) []byte
var mutators = []mutateFunc{
mutateBitFlip,
mutateByteFlip,
mutateArith,
mutateInteresting,
mutateBlockDelete,
mutateBlockDuplicate,
}
func mutateBitFlip(rng *rand.Rand, data []byte) []byte {
if len(data) == 0 {
return data
}
out := append([]byte(nil), data...)
idx := rng.Intn(len(out))
bit := rng.Intn(8)
out[idx] ^= 1 << bit
return out
}
func mutateByteFlip(rng *rand.Rand, data []byte) []byte {
if len(data) == 0 {
return data
}
out := append([]byte(nil), data...)
out[rng.Intn(len(out))] = byte(rng.Intn(256))
return out
}
// interestingBytes/16/32 — классический AFL-набор "граничных" значений:
// нули, минус единица, границы знакового/беззнакового диапазона. Такие
// значения статистически чаще ломают проверки длины/индексов, чем
// случайный байт.
var (
interestingBytes = []byte{0x00, 0x01, 0x7f, 0x80, 0xff}
interesting16 = []int16{0, 1, -1, 0x7fff, -0x8000, 0xff, 0x100}
interesting32 = []int32{0, 1, -1, 0x7fffffff, -0x80000000, 0xffff, 0x10000}
)
func mutateInteresting(rng *rand.Rand, data []byte) []byte {
if len(data) == 0 {
return data
}
out := append([]byte(nil), data...)
switch rng.Intn(3) {
case 0:
out[rng.Intn(len(out))] = interestingBytes[rng.Intn(len(interestingBytes))]
case 1:
if len(out) >= 2 {
idx := rng.Intn(len(out) - 1)
v := interesting16[rng.Intn(len(interesting16))]
binary.LittleEndian.PutUint16(out[idx:], uint16(v))
}
default:
if len(out) >= 4 {
idx := rng.Intn(len(out) - 3)
v := interesting32[rng.Intn(len(interesting32))]
binary.LittleEndian.PutUint32(out[idx:], uint32(v))
}
}
return out
}
// mutateArith — прибавляет/отнимает небольшое значение (1..35, тот же
// диапазон, что ARITH_MAX в AFL) от случайного байта. Ловит off-by-one
// и похожие арифметические баги, которые чистый bit-flip обычно не находит.
func mutateArith(rng *rand.Rand, data []byte) []byte {
if len(data) == 0 {
return data
}
out := append([]byte(nil), data...)
idx := rng.Intn(len(out))
delta := byte(rng.Intn(35) + 1)
if rng.Intn(2) == 0 {
out[idx] += delta
} else {
out[idx] -= delta
}
return out
}
func mutateBlockDelete(rng *rand.Rand, data []byte) []byte {
if len(data) < 2 {
return data
}
start := rng.Intn(len(data))
length := 1 + rng.Intn(len(data)-start)
out := append([]byte(nil), data[:start]...)
out = append(out, data[start+length:]...)
return out
}
func mutateBlockDuplicate(rng *rand.Rand, data []byte) []byte {
if len(data) == 0 {
return data
}
start := rng.Intn(len(data))
length := 1 + rng.Intn(len(data)-start)
block := data[start : start+length]
insertAt := rng.Intn(len(data) + 1)
out := append([]byte(nil), data[:insertAt]...)
out = append(out, block...)
out = append(out, data[insertAt:]...)
return out
}
// splice скрещивает два входа в случайной точке — классическая техника
// "склеить два интересных сэмпла и посмотреть, что получится".
func splice(rng *rand.Rand, a, b []byte) []byte {
if len(a) == 0 || len(b) == 0 {
return append([]byte(nil), a...)
}
n := len(a)
if len(b) < n {
n = len(b)
}
cut := rng.Intn(n)
out := append([]byte(nil), a[:cut]...)
out = append(out, b[cut:]...)
return out
}
// havoc — стадия из классического AFL, которая на практике находит
// больше всего крашей: несколько случайных мутаций подряд за один проход,
// иногда вперемешку со splice по другому сэмплу из корпуса.
func havoc(rng *rand.Rand, data []byte, corpus [][]byte) []byte {
out := append([]byte(nil), data...)
rounds := 1 + rng.Intn(8)
for i := 0; i < rounds; i++ {
if len(corpus) > 1 && rng.Intn(4) == 0 {
out = splice(rng, out, corpus[rng.Intn(len(corpus))])
continue
}
out = mutators[rng.Intn(len(mutators))](rng, out)
}
return out
}