design/48815-custom-fuzz-input-types.md: revise from feedback
* Add rationale explaining why the `customMutator` interface is not
exported.
* Switch `Mutate` to take a `context.Context`, and expand the
associated rationale.
* Minor readability tweaks.
* Delete the open issues. (They don't describe problems that lack a
solution; they are more like discussion points that should be
addressed in the linked issue or in merge requests.)
For golang/go#48815.
Change-Id: I704ab930710d1b82a42b7923dbabc4e10543f5ca
Reviewed-on: https://go-review.googlesource.com/c/proposal/+/501537
Reviewed-by: Ian Lance Taylor <iant@google.com>
diff --git a/design/48815-custom-fuzz-input-types.md b/design/48815-custom-fuzz-input-types.md
index 25141db..c1a5051 100644
--- a/design/48815-custom-fuzz-input-types.md
+++ b/design/48815-custom-fuzz-input-types.md
@@ -2,7 +2,7 @@
Author: Richard Hansen <rhansen@rhansen.org>
-Last updated: 2023-05-10
+Last updated: 2023-06-08
Discussion at https://go.dev/issue/48815.
@@ -74,9 +74,9 @@
// previously returned from a call to MarshalBinary.
UnmarshalBinary([]byte) error
// Mutate pseudo-randomly transforms the customMutator's value. The mutation
- // must be deterministic: every call to Mutate with the same starting value
- // and seed must result in the same transformed value.
- Mutate(seed int64) error
+ // must be repeatable: every call to Mutate with the same starting value and
+ // seed must result in the same transformed value.
+ Mutate(ctx context.Context, seed int64) error
}
```
@@ -106,7 +106,7 @@
func (v *fuzzInput) MarshalBinary() ([]byte, error) { return json.Marshal(v) }
func (v *fuzzInput) UnmarshalBinary(d []byte) error { return json.Unmarshal(d, v) }
-func (v *fuzzInput) Mutate(seed int64) error {
+func (v *fuzzInput) Mutate(ctx context.Context, seed int64) error {
v.Word = loremipsum.NewWithSeed(seed).Word()
return nil
}
@@ -130,6 +130,29 @@
## Rationale
+### Private interface
+
+The `customMutator` interface is not exported for a few reasons:
+
+ * Exporting is not strictly required because it does not appear anywhere
+ outside of internal logic.
+ * It can be easily exported in the future if needed. The opposite is not true:
+ un-exporting requires a major version change.
+ * [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it): Users are
+ unlikely to want to declare anything with that type. One possible exception
+ is a compile-time type check such as the following:
+
+ ```go
+ var _ testing.CustomMutator = (*myType)(nil)
+ ```
+
+ Such a check is unlikely to have much value: the code is likely being
+ compiled because tests are about to run, and `testing.F.Fuzz`'s runtime
+ check will immediately catch the bug.
+ * Exporting now would add friction to extending `testing.F.Fuzz` again in the
+ future. Should the new interface be exported even if doing so doesn't add
+ much value beyond consistency?
+
### `MarshalBinary`, `UnmarshalBinary` methods
`Marshal` and `Unmarshal` would be shorter to type than `MarshalBinary` and
@@ -171,31 +194,41 @@
for holding random bits, because that is what
[`math/rand.NewSource`](https://pkg.go.dev/math/rand#NewSource) takes.
-The `Mutate` method must be deterministic to avoid violating [an assumption in
-the coordinator–worker
+The `Mutate` method must be repeatable to avoid violating [an assumption in the
+coordinator–worker
protocol](https://github.com/golang/go/blob/0a9875c5c809fa70ae6662b8a38f5f86f648badd/src/internal/fuzz/worker.go#L702-L705).
This may be relaxed in the future by revising the protocol.
-`Mutate` is expected to always succeed. Always returning `nil` helps ensure
-repeatability, which is necessary for the coordinator–worker protocol assumption
-linked above. Despite this, `Mutate` returns an error for a couple of reasons:
+Some alternatives for the `Mutate` method were considered:
- * It discourages the use of `panic`. Panicking is problematic for the reasons
- described in the `MarshalBinary` rationale above.
+ * `Mutate()`: Simplest, but the lack of a seed parameter makes it difficult
+ to satisfy the repeatability requirement.
+ * `Mutate(seed int64)`: Simple. Naturally hints to developers that the method
+ is expected to be fast, repeatable, and error-free, which increases the
+ effectiveness of fuzzing. Adding a context parameter or error return value
+ (or both) might be YAGNI, but their absence makes complex mutation
+ operations more difficult to implement. The lack of an error return value
+ encourages the use of `panic`, which is problematic for the reasons
+ discussed in the `MarshalBinary` rationale above.
+ * `Mutate(seed int64) error`: The error return value discourages the use of
+ `panic`, and enables better dev UX when debugging complex mutation
+ operations.
+ * `Mutate(ctx context.Context, seed int64) error`: The context makes this more
+ future-proof by enabling advanced techniques once the repeatability
+ requirement is removed. For example, `Mutate` could send an RPC to a service
+ that feeds automatic crash report data to fuzzing tasks to increase the
+ likelihood of encountering an interesting value. The context parameter and
+ error return value might be YAGNI, but the added implementation complexity
+ and developer cognitive load is believed to be minor enough to not worry
+ about it (they can be ignored in most use cases).
+ * Accept both `Mutate(seed int64)` and `Mutate(ctx context.Context, seed
+ int64) error`: The second of the two can be added later after accumulating
+ additional feedback from developers. Supporting both might result in
+ unnecessary complexity.
- * It may enable advanced use cases when combined with a future removal of the
- determinism requirement. A hypothetical example: The `Mutate` method could
- call out to a service that coordinates multiple fuzzing tasks to avoid
- duplicated effort or employ advanced techniques for exploring the input
- space. Such queries could regularly fail; enabling the coordinator to
- gracefully handle the errors improves UX.
-
-Alternatively, the error return value could be omitted for now, and `F.Fuzz`
-extended again in the future to accept another custom input type whose `Mutate`
-method does return an error. This was rejected because the end result is
-unnecessarily messy for little immediate benefit, and any existing custom input
-types that call `panic` would have to be updated to take advantage of the
-improved error handling.
+Because mutation operations on custom types are expected to be somewhat complex
+(otherwise a basic type would probably suffice), the `Mutate(ctx
+context.Context, seed int64) error` option is believed to be the best choice.
### Minimization
@@ -226,25 +259,8 @@
See https://go.dev/cl/493304 for an initial attempt.
-For the initial implementation, a worker will simply panic if one of the custom
+For the initial implementation, a worker can simply panic if one of the custom
type's methods returns an error. A future change can improve UX by plumbing the
error.
No particular Go release is targeted.
-
-## Open issues
-
- * What is the best way to obtain a stable, globally unique, and marshalable
- identifier from a `reflect.Type`? The `reflect.Type.String` method does not
- guarantee global uniqueness. See https://go.dev/cl/493304 for an initial
- attempt.
-
- * Should `MarshalBinary` not return an error, forcing devs to call `panic` on
- error? We can always add support for a returned error in the future if
- desired.
-
- * Should `Mutate` not return an error, forcing devs to call `panic` on error?
- We can always add support for a returned error in the future if desired.
-
- * Should `Mutate` take a `context.Context` in case it wants to be cancelable?
- (Maybe it wants to send RPCs, or otherwise do something expensive.)