skip to content

In Go fuzzing, which argument types may the f.Fuzz callback take, and how must f.Add match them?

level: middleimportance: nice to knowfreq 30%

answer

  1. only what the engine can mutate
  2. no structs, no maps
  3. exactly one slice type is allowed
  4. aliases count: rune and byte
  5. seeds must match types exactly

basics

~20 s

After the leading *testing.T, only string, []byte, bool, float32, float64 and the signed and unsigned integer types are allowed - no structs, maps or other slices. Every f.Add call must pass exactly those types, in order.

solid answer

~40 s

The callback's first parameter is `*testing.T`; every parameter after it must come from a fixed list the engine knows how to mutate and store: `string`, `[]byte`, `bool`, `float32`, `float64`, and the integer types — `int`, `int8`, `int16`, `int32` (so `rune`), `int64`, `uint`, `uint8` (so `byte`), `uint16`, `uint32`, `uint64`. Structs, maps, slices other than `[]byte`, pointers and interfaces are all rejected. Every `f.Add` call must supply exactly those types, in the same order and with the same count, and because `f.Add` takes `...any` there is no implicit conversion — `f.Add(10)` stores an `int`, which will not satisfy an `int64` parameter. To fuzz something structured, either take one scalar parameter per field, or take a single `[]byte` and decode it inside the callback with a cheap total decoder.

code

go · 9 lines
go
// Not allowed: f.Fuzz(func(t *testing.T, req Request) { ... })
// Take one scalar parameter per field instead.
f.Add("name = \"ada\"", 10, false)
f.Fuzz(func(t *testing.T, query string, limit int, strict bool) {
	req := Request{Query: query, Limit: limit, Strict: strict}
	if _, err := ParseRequest(req); err != nil {
		return
	}
})

go deeper

for a junior

Remember the shape rather than the whole list: scalars plus string and []byte, first parameter *testing.T, and seeds that mirror the callback's parameters one for one.

for a middle

Be able to name the allowed types and explain why the set is closed — the engine must both mutate a value and write it to a corpus file in a stable encoding.

for a senior

Show how you fuzz structured input anyway: one parameter per field, or a byte slice carved by a total decoder, and explain why a strict format like JSON wastes the search budget.

for a principal

Frame it as an interface design question — what a team should expose as a fuzzable seam so the mutation budget lands on real parsing logic rather than on decoding scaffolding.

## The allowed set A fuzz callback is passed to `f.Fuzz` as an `any` and checked with reflection when the target starts. Its signature must be `func(*testing.T, ...)` where each parameter after the first has one of these types: - `[]byte` - `string` - `bool` - `float32`, `float64` - `int`, `int8`, `int16`, `int32`, `int64` - `uint`, `uint8`, `uint16`, `uint32`, `uint64` `rune` is an alias for `int32` and `byte` is an alias for `uint8`, so both are allowed and are indistinguishable from their underlying types as far as the engine is concerned. Anything else — a struct, a map, an `[]int`, a `*T`, an interface, a channel, a function — is rejected, and the target fails immediately rather than at some later iteration. ## Why the list is that short The engine has two jobs the type must support. First, **mutation**: it must be able to take a value and produce a plausibly interesting neighbour, which means flipping bytes in a string, nudging an integer towards a boundary such as 0, 1, or the type's maximum, or truncating and splicing a byte slice. Second, **serialisation**: every input the engine decides to keep, and every input that fails, has to be written to a corpus file in a stable textual encoding and read back later. A general Go value graph has neither a natural mutation strategy nor a total, version-stable encoding; a flat list of scalars has both. ## Matching seeds to the signature `f.Add` is declared as `Add(args ...any)`. That means the compiler cannot check your seeds against the callback — the check happens at run time, and it is strict: - the **count** must equal the number of callback parameters after `*testing.T`; - the **order** must match; - the **dynamic types** must match exactly. The exact-type rule is the one that bites. An untyped constant in a call to a `...any` parameter takes its default type, so `f.Add("q", 10)` supplies a `string` and an `int`. If the callback declares `func(t *testing.T, q string, limit int64)`, that seed is a type mismatch and the target fails. Write `f.Add("q", int64(10))`. The same is true of the file-based corpus entries the engine reads and writes, which is why the encoding is typed rather than positional. ## Fuzzing something structured Two idioms cover almost everything. **One parameter per field.** If a request has a query string, a limit and a strictness flag, declare `func(t *testing.T, query string, limit int, strict bool)` and build the struct inside the callback. The engine mutates each field independently and its coverage feedback stays meaningful. **One `[]byte`, decoded by hand.** For anything genuinely variable-shaped, take a single `[]byte` and write a small, total decoder that carves it into the pieces you need — the first byte selects a variant, the next two are a length, the rest is payload — never failing, always producing *some* value. This is the standard trick for fuzzing state machines and command sequences. Decoding the bytes with a strict format such as JSON also compiles, but most mutations then fail to decode and return early, so the search spends its budget on the decoder instead of on your parser. ## Things that trip people up - **The callback cannot be variadic.** The parameter list is fixed and inspected by reflection. - **`f.Fuzz` may be called only once** per target, so you cannot register two differently shaped callbacks. - **More parameters is not free.** Each one is another dimension the engine has to explore; two or three well-chosen ones usually beat six. - **A `[]byte` and a `string` behave differently at the boundary.** A `string` parameter can hold arbitrary bytes including invalid UTF-8, so do not assume the engine hands you well-formed text — that assumption is itself a bug worth fuzzing for.

  • You want to fuzz a request struct with three fields. What are your options?
    Either declare one scalar callback parameter per field — `func(t *testing.T, query string, limit int, strict bool)` — and build the struct inside, or take a single `[]byte` and split it with a small decoder that never fails. Both keep the engine mutating things it understands. Decoding the bytes as JSON works too, but most mutations then fail to decode and the budget goes into the decoder rather than your parser.
  • What happens if f.Add passes an int where the callback declares an int64?
    The target fails with a type mismatch. `f.Add` takes `...any`, so the untyped constant in `f.Add("q", 10)` becomes an `int`, and seed arguments must match the callback's parameter types exactly, in order and in count. There is no implicit widening. Write `int64(10)` explicitly.
  • Why is a string parameter not guaranteed to be valid UTF-8?
    A Go string is an immutable sequence of bytes, not a sequence of runes, and the engine mutates it at the byte level. It will happily hand you a lone continuation byte or a truncated multi-byte sequence. Code that indexes, slices or converts such a string without checking is exactly the code fuzzing is meant to find.

saying these in an interview costs you the question

  • Claims any struct can be a fuzz callback parameter
  • Thinks maps or []int are allowed argument types
  • Passes seed values whose types differ from the parameters
  • Believes the fuzz callback may be variadic
  • Assumes a fuzzed string is always valid UTF-8