]> Cypherpunks.ru repositories - gostls13.git/commitdiff
[dev.fuzz] internal/fuzz: notify coordinator for minimization
authorJay Conrod <jayconrod@google.com>
Fri, 21 May 2021 16:44:26 +0000 (12:44 -0400)
committerJay Conrod <jayconrod@google.com>
Wed, 2 Jun 2021 20:55:09 +0000 (20:55 +0000)
When a worker process finds a crasher, it now sends that result
directly to the coordinator without attempting to minimize it
first. The coordinator stops sending new inputs and sends the
unminimized crasher back to a worker (any worker) for minimization.

This prevents wasted work during minimization and will help us
implement -keepfuzzing later on. We may also be able to minimize
interesting inputs with this approach later.

Since panics are recoverable errors (they don't terminate worker
processes), we no longer attempt to minimize non-recoverable errors.
This didn't work too well before: we lost too much state.

Change-Id: Id142c7e91a33f64584170b0d42d22cb1f22a92d7
Reviewed-on: https://go-review.googlesource.com/c/go/+/321835
Trust: Jay Conrod <jayconrod@google.com>
Trust: Katie Hockman <katie@golang.org>
Reviewed-by: Katie Hockman <katie@golang.org>
src/cmd/go/testdata/script/test_fuzz_minimize.txt
src/internal/fuzz/fuzz.go
src/internal/fuzz/minimize.go
src/internal/fuzz/minimize_test.go
src/internal/fuzz/worker.go

index 7652759668479e19503bb92f3448ba4f768903a7..215ce04dbc92292872ebf03b9f76611740e23e0c 100644 (file)
@@ -6,12 +6,8 @@
 # We clean the fuzz cache during this test. Don't clean the user's cache.
 env GOCACHE=$WORK/gocache
 
-# TODO(b/181800488): remove -parallel=1, here and below. For now, when a
-# crash is found, all workers keep running, wasting resources and reducing
-# the number of executions available to the minimizer, increasing flakiness.
-
 # Test that minimization is working for recoverable errors.
-! go test -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -fuzztime=100x -fuzzminimizetime=10000x -parallel=1 minimizer_test.go
+! go test -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -fuzztime=100x -fuzzminimizetime=10000x minimizer_test.go
 ! stdout '^ok'
 stdout 'got the minimum size!'
 stdout 'contains a non-zero byte'
@@ -25,11 +21,10 @@ go run check_testdata.go FuzzMinimizerRecoverable 50
 rm testdata
 
 # Test that minimization is working for non-recoverable errors.
-! go test -fuzz=FuzzMinimizerNonrecoverable -run=FuzzMinimizerNonrecoverable -fuzztime=100x -fuzzminimizetime=10000x -parallel=1 minimizer_test.go
+! go test -fuzz=FuzzMinimizerNonrecoverable -run=FuzzMinimizerNonrecoverable -fuzztime=100x -fuzzminimizetime=10000x minimizer_test.go
 ! stdout '^ok'
-# TODO(jayconrod): fix in CL 321835.
 stdout 'found a crash, minimizing'
-stdout 'fuzzing process terminated unexpectedly while minimizing: exit status 99'
+stdout 'fuzzing process terminated unexpectedly while minimizing: exit status 99'
 stdout FAIL
 
 # Check that re-running the value causes a crash.
@@ -42,11 +37,10 @@ go clean -fuzzcache
 
 # Test that minimization can be cancelled by fuzzminimizetime and the latest
 # crash will still be logged and written to testdata.
-! go test -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -parallel=1 -fuzztime=100x -fuzzminimizetime=1x minimizer_test.go
+! go test -fuzz=FuzzMinimizerRecoverable -run=FuzzMinimizerRecoverable -fuzztime=100x -fuzzminimizetime=1x minimizer_test.go
 ! stdout '^ok'
 stdout 'testdata[/\\]corpus[/\\]FuzzMinimizerRecoverable[/\\]'
-# TODO(jayconrod): implement -fuzzminimizetime in 321835.
-# ! stdout 'got the minimum size!'  # it shouldn't have had enough time to minimize it
+! stdout 'got the minimum size!'  # it shouldn't have had enough time to minimize it
 stdout FAIL
 
 # Test that re-running the unminimized value causes a crash.
index 553086b20a3826ee036e1c7a895600da779f7ad6..4bcfbeec331c82f892387627f07cafc5f31722ee 100644 (file)
@@ -90,6 +90,13 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
                // Don't start more workers than we need.
                opts.Parallel = int(opts.Limit)
        }
+       canMinimize := false
+       for _, t := range opts.Types {
+               if isMinimizable(t) {
+                       canMinimize = true
+                       break
+               }
+       }
 
        c, err := newCoordinator(opts)
        if err != nil {
@@ -168,6 +175,9 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
                w := workers[i]
                go func() {
                        err := w.coordinate(fuzzCtx)
+                       if fuzzCtx.Err() != nil || isInterruptError(err) {
+                               err = nil
+                       }
                        cleanErr := w.cleanup()
                        if err == nil {
                                err = cleanErr
@@ -187,6 +197,7 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
        statTicker := time.NewTicker(3 * time.Second)
        defer statTicker.Stop()
        defer c.logStats()
+       crashMinimizing := false
        crashWritten := false
 
        for {
@@ -204,21 +215,30 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
                        }
 
                        if result.crasherMsg != "" {
-                               // Found a crasher. Write it to testdata and return it.
-                               if crashWritten {
-                                       break
-                               }
-                               fileName, err := writeToCorpus(result.entry.Data, opts.CorpusDir)
-                               if err == nil {
-                                       crashWritten = true
-                                       err = &crashError{
-                                               name: filepath.Base(fileName),
-                                               err:  errors.New(result.crasherMsg),
+                               if canMinimize && !result.minimized {
+                                       // Found a crasher but haven't yet attempted to minimize it.
+                                       // Send it back to a worker for minimization. Disable inputC so
+                                       // other workers don't continue fuzzing.
+                                       if crashMinimizing {
+                                               break
                                        }
+                                       crashMinimizing = true
+                                       inputC = nil
+                                       fmt.Fprintf(c.opts.Log, "found a crash, minimizing...\n")
+                                       c.minimizeC <- result
+                               } else if !crashWritten {
+                                       // Found a crasher that's either minimized or not minimizable.
+                                       // Write to corpus and stop.
+                                       fileName, err := writeToCorpus(result.entry.Data, opts.CorpusDir)
+                                       if err == nil {
+                                               crashWritten = true
+                                               err = &crashError{
+                                                       name: filepath.Base(fileName),
+                                                       err:  errors.New(result.crasherMsg),
+                                               }
+                                       }
+                                       stop(err)
                                }
-                               // TODO(jayconrod,katiehockman): if -keepfuzzing, report the error to
-                               // the user and restart the crashed worker.
-                               stop(err)
                        } else if result.coverageData != nil {
                                foundNew := c.updateCoverage(result.coverageData)
                                if foundNew && !c.coverageOnlyRun() {
@@ -248,11 +268,11 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
                                        }
                                }
                        }
-                       if inputC == nil && !stopping && !c.coverageOnlyRun() {
-                               // inputC was disabled earlier because we hit the limit on the number
-                               // of inputs to fuzz (nextInput returned false).
-                               // Workers can do less work than requested though, so we might be
-                               // below the limit now. Call nextInput again and re-enable inputC if so.
+                       if inputC == nil && !crashMinimizing && !stopping && !c.coverageOnlyRun() {
+                               // Re-enable inputC if it was disabled earlier because we hit the limit
+                               // on the number of inputs to fuzz (nextInput returned false). Workers
+                               // can do less work than requested, so after receiving a result above,
+                               // we might be below the limit now.
                                if input, ok = c.nextInput(); ok {
                                        inputC = c.inputC
                                }
@@ -317,6 +337,9 @@ type corpus struct {
 // packages, but testing can't import this package directly, and we don't want
 // to export this type from testing. Instead, we use the same struct type and
 // use a type alias (not a defined type) for convenience.
+//
+// TODO: split marshalled and unmarshalled types. In most places, we only need
+// one or the other.
 type CorpusEntry = struct {
        // Name is the name of the corpus file, if the entry was loaded from the
        // seed corpus. It can be used with -run. For entries added with f.Add and
@@ -358,6 +381,10 @@ type fuzzResult struct {
        // crasherMsg is an error message from a crash. It's "" if no crash was found.
        crasherMsg string
 
+       // minimized is true if a worker attempted to minimize entry.
+       // Minimization may not have actually been completed.
+       minimized bool
+
        // coverageData is set if the worker found new coverage.
        coverageData []byte
 
@@ -382,9 +409,13 @@ type coordinator struct {
        startTime time.Time
 
        // inputC is sent values to fuzz by the coordinator. Any worker may receive
-       // values from this channel.
+       // values from this channel. Workers send results to resultC.
        inputC chan fuzzInput
 
+       // minimizeC is sent values to minimize by the coordinator. Any worker may
+       // receive values from this channel. Workers send results to resultC.
+       minimizeC chan fuzzResult
+
        // resultC is sent results of fuzzing by workers. The coordinator
        // receives these. Multiple types of messages are allowed.
        resultC chan fuzzResult
@@ -443,6 +474,7 @@ func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) {
                opts:          opts,
                startTime:     time.Now(),
                inputC:        make(chan fuzzInput),
+               minimizeC:     make(chan fuzzResult),
                resultC:       make(chan fuzzResult),
                corpus:        corpus,
                covOnlyInputs: covOnlyInputs,
index c5533bd91742bf302adbb75efca7ac2ecdb92ffa..5164c348458e37bcb1133532249d677a25f0ff5b 100644 (file)
@@ -5,11 +5,20 @@
 package fuzz
 
 import (
-       "context"
        "math"
+       "reflect"
 )
 
-func minimizeBytes(ctx context.Context, v []byte, stillCrashes func(interface{}) bool, shouldStop func() bool) {
+func isMinimizable(t reflect.Type) bool {
+       for _, v := range zeroVals {
+               if t == reflect.TypeOf(v) {
+                       return true
+               }
+       }
+       return false
+}
+
+func minimizeBytes(v []byte, stillCrashes func(interface{}) bool, shouldStop func() bool) {
        // First, try to cut the tail.
        for n := 1024; n != 0; n /= 2 {
                for len(v) > n {
@@ -67,7 +76,7 @@ func minimizeBytes(ctx context.Context, v []byte, stillCrashes func(interface{})
        return
 }
 
-func minimizeInteger(ctx context.Context, v uint, stillCrashes func(interface{}) bool, shouldStop func() bool) {
+func minimizeInteger(v uint, stillCrashes func(interface{}) bool, shouldStop func() bool) {
        // TODO(rolandshoemaker): another approach could be either unsetting/setting all bits
        // (depending on signed-ness), or rotating bits? When operating on cast signed integers
        // this would probably be more complex though.
@@ -84,7 +93,7 @@ func minimizeInteger(ctx context.Context, v uint, stillCrashes func(interface{})
        return
 }
 
-func minimizeFloat(ctx context.Context, v float64, stillCrashes func(interface{}) bool, shouldStop func() bool) {
+func minimizeFloat(v float64, stillCrashes func(interface{}) bool, shouldStop func() bool) {
        if math.IsNaN(v) {
                return
        }
index 500ff431b457799ec8f02c971e0bc04903f4f209..d786cf809e5eff446d5f51a99f1f379ecefd112f 100644 (file)
@@ -190,28 +190,19 @@ func TestMinimizeInput(t *testing.T) {
                })
        }
 
-       sm, err := sharedMemTempFile(workerSharedMemSize)
-       if err != nil {
-               t.Fatalf("failed to create temporary shared memory file: %s", err)
-       }
-       defer sm.Close()
-
        for _, tc := range cases {
                ws := &workerServer{
                        fuzzFn: tc.fn,
                }
                count := int64(0)
-               err = ws.minimizeInput(context.Background(), tc.input, sm, &count, 0)
+               vals := tc.input
+               err := ws.minimizeInput(context.Background(), vals, &count, 0)
                if err == nil {
                        t.Error("minimizeInput didn't fail")
                }
                if expected := fmt.Sprintf("bad %v", tc.input[0]); err.Error() != expected {
                        t.Errorf("unexpected error: got %s, want %s", err, expected)
                }
-               vals, err := unmarshalCorpusFile(sm.valueCopy())
-               if err != nil {
-                       t.Fatalf("failed to unmarshal values from shared memory file: %s", err)
-               }
                if !reflect.DeepEqual(vals, tc.expected) {
                        t.Errorf("unexpected results: got %v, want %v", vals, tc.expected)
                }
index 33727a543858a170fe94a95ae1940cbd61a60fe5..2bfd9fce771111cf4490bb814a92dabdcc3f7a0c 100644 (file)
@@ -70,39 +70,28 @@ func (w *worker) cleanup() error {
 
 // coordinate runs the test binary to perform fuzzing.
 //
-// coordinate loops until ctx is cancelled or a fatal error is encountered. While
-// looping, coordinate receives inputs from w.coordinator.inputC, then passes
-// those on to the worker process.
+// coordinate loops until ctx is cancelled or a fatal error is encountered.
+// If a test process terminates unexpectedly while fuzzing, coordinate will
+// attempt to restart and continue unless the termination can be attributed
+// to an interruption (from a timer or the user).
+//
+// While looping, coordinate receives inputs from the coordinator, passes
+// those inputs to the worker process, then passes the results back to
+// the coordinator.
 func (w *worker) coordinate(ctx context.Context) error {
-       // Start the process.
-       if err := w.start(); err != nil {
-               // We couldn't start the worker process. We can't do anything, and it's
-               // likely that other workers can't either, so don't try to restart.
-               return err
-       }
-
-       // Send the worker a message to make sure it can respond.
-       // Errors that occur before we get a response likely indicate that
-       // the worker did not call F.Fuzz or called F.Fail first.
-       // We don't record crashers for these errors.
-       if err := w.client.ping(ctx); err != nil {
-               w.stop()
-               if ctx.Err() != nil {
-                       return ctx.Err()
-               }
-               if isInterruptError(err) {
-                       // User may have pressed ^C before worker responded.
-                       return nil
-               }
-               return fmt.Errorf("fuzzing process terminated without fuzzing: %w", err)
-       }
-
        // interestingCount starts at -1, like the coordinator does, so that the
        // worker client's coverage data is updated after a coverage-only run.
        interestingCount := int64(-1)
 
        // Main event loop.
        for {
+               // Start or restart the worker if it's not running.
+               if !w.isRunning() {
+                       if err := w.startAndPing(ctx); err != nil {
+                               return err
+                       }
+               }
+
                select {
                case <-ctx.Done():
                        // Worker was told to stop.
@@ -170,33 +159,10 @@ func (w *worker) coordinate(ctx context.Context) error {
                                        // Since we expect I/O errors around interrupts, ignore this error.
                                        return nil
                                }
-                               // Unexpected termination. Attempt to minimize, then inform the
-                               // coordinator about the crash.
-                               // TODO(jayconrod,katiehockman): if -keepfuzzing, restart worker.
-                               // TODO(jayconrod,katiehockman): consider informing the
-                               // coordinator that this worker is minimizing, in order to block
-                               // the other workers from receiving more inputs.
-                               message := fmt.Sprintf("fuzzing process terminated unexpectedly: %v", w.waitErr)
-                               err = w.waitErr
-                               var result fuzzResult
-                               var minimized bool
-                               if !input.coverageOnly {
-                                       var minErr error
-                                       result, minimized, minErr = w.minimize(ctx)
-                                       if minErr != nil {
-                                               err = minErr
-                                       }
-                               }
-                               if !minimized {
-                                       // Minimization did not find a smaller crashing value, so
-                                       // return the one we already found.
-                                       result.entry = CorpusEntry{Data: value}
-                                       result.crasherMsg = message
-                               }
-                               w.coordinator.resultC <- result
-                               return err
+                               // Unexpected termination. Set error message and fall through.
+                               // We'll restart the worker on the next iteration.
+                               resp.Err = fmt.Sprintf("fuzzing process terminated unexpectedly: %v", w.waitErr)
                        }
-
                        result := fuzzResult{
                                countRequested: input.countRequested,
                                count:          resp.Count,
@@ -210,82 +176,99 @@ func (w *worker) coordinate(ctx context.Context) error {
                                result.coverageData = resp.CoverageData
                        }
                        w.coordinator.resultC <- result
+
+               case crasher := <-w.coordinator.minimizeC:
+                       // Received input to minimize from coordinator.
+                       minRes, err := w.minimize(ctx, crasher)
+                       if err != nil {
+                               // Failed to minimize. Send back the original crash.
+                               fmt.Fprintln(w.coordinator.opts.Log, err)
+                               minRes = crasher
+                               minRes.minimized = true
+                       }
+                       w.coordinator.resultC <- minRes
                }
        }
 }
 
-// minimize asks a workerServer to attempt to minimize a value that caused an
-// unexpected termination of the worker process. The value must be in shared
-// memory, and the worker must be stopped. The execution count in shared memory
-// is reset once before restarting the worker.
-func (w *worker) minimize(ctx context.Context) (res fuzzResult, minimized bool, retErr error) {
-       if w.coordinator.opts.MinimizeTimeout != 0 {
-               fmt.Fprintf(w.coordinator.opts.Log, "found a crash, minimizing for up to %v\n", w.coordinator.opts.MinimizeTimeout)
-       } else if w.coordinator.opts.MinimizeLimit != 0 {
-               fmt.Fprintf(w.coordinator.opts.Log, "found a crash, minimizing for up to %d execs\n", w.coordinator.opts.MinimizeLimit)
-       } else {
-               fmt.Fprintf(w.coordinator.opts.Log, "found a crash, minimizing...\n")
-       }
-       start := time.Now()
+// minimize tells a worker process to attempt to find a smaller value that
+// causes an error. minimize may restart the worker repeatedly if the error
+// causes (or already caused) the worker process to terminate.
+//
+// TODO: support minimizing inputs that expand coverage in a specific way,
+// for example, by ensuring that an input activates a specific set of counters.
+func (w *worker) minimize(ctx context.Context, input fuzzResult) (min fuzzResult, err error) {
        if w.coordinator.opts.MinimizeTimeout != 0 {
                var cancel func()
                ctx, cancel = context.WithTimeout(ctx, w.coordinator.opts.MinimizeTimeout)
                defer cancel()
        }
-       defer func() {
-               w.stop()
-               if retErr == nil {
-                       retErr = w.waitErr
-               }
-       }()
 
-       mem := <-w.memMu
-       mem.header().count = 0
-       w.memMu <- mem
+       min = input
+       min.minimized = true
 
-       for {
-               if ctx.Err() != nil {
-                       return res, minimized, retErr
-               }
-               // Restart the worker.
-               if err := w.start(); err != nil {
-                       return res, minimized, err
-               }
-               if err := w.client.ping(ctx); err != nil {
-                       return res, minimized, err
-               }
-               args := minimizeArgs{Limit: w.coordinator.opts.MinimizeLimit}
-               if w.coordinator.opts.MinimizeTimeout != 0 {
-                       elapsed := time.Now().Sub(start)
-                       args.Timeout = w.coordinator.opts.MinimizeTimeout - elapsed
-                       if args.Timeout < 0 {
-                               return res, minimized, retErr
-                       }
-               }
-               value, err := w.client.minimize(ctx, args)
-               if err == nil {
-                       // Minimization finished successfully, meaning that it
-                       // couldn't find any smaller inputs that caused a crash,
-                       // so stop trying.
-                       return res, minimized, nil
-               }
+       args := minimizeArgs{
+               Limit:   w.coordinator.opts.MinimizeLimit,
+               Timeout: w.coordinator.opts.MinimizeTimeout,
+       }
+       value, resp, err := w.client.minimize(ctx, input.entry.Data, args)
+       if err != nil {
+               // Error communicating with worker.
                w.stop()
-               // Minimization will return an error for a non-recoverable problem, so
-               // a non-nil error is expected. However, make sure it didn't fail for
-               // some other reason which should cause us to stop minimizing.
                if ctx.Err() != nil || w.interrupted || isInterruptError(w.waitErr) {
-                       return res, minimized, nil
+                       // Worker was interrupted, possibly by the user pressing ^C.
+                       // Normally, workers can handle interrupts and timeouts gracefully and
+                       // will return without error. An error here indicates the worker
+                       // may not have been in a good state, but the error won't be meaningful
+                       // to the user. Just return the original crasher without logging anything.
+                       return min, nil
                }
+               return fuzzResult{}, fmt.Errorf("fuzzing process terminated unexpectedly while minimizing: %w", w.waitErr)
+       }
+
+       if resp.Err == "" {
+               // Minimization did not find a smaller input that caused a crash.
+               return min, nil
+       }
+       min.crasherMsg = resp.Err
+       min.count = resp.Count
+       min.duration = resp.Duration
+       min.entry.Data = value
+       return min, nil
+}
+
+func (w *worker) isRunning() bool {
+       return w.cmd != nil
+}
 
-               // The bytes in memory caused a legitimate crash, so stop the worker and
-               // save this value and error message.
-               message := fmt.Sprintf("fuzzing process terminated unexpectedly: %v", w.waitErr)
-               res = fuzzResult{
-                       entry:      CorpusEntry{Data: value},
-                       crasherMsg: message,
+// startAndPing starts the worker process and sends it a message to make sure it
+// can communicate.
+//
+// startAndPing returns an error if any part of this didn't work, including if
+// the context is expired or the worker process was interrupted before it
+// responded. Errors that happen after start but before the ping response
+// likely indicate that the worker did not call F.Fuzz or called F.Fail first.
+// We don't record crashers for these errors.
+func (w *worker) startAndPing(ctx context.Context) error {
+       if ctx.Err() != nil {
+               return ctx.Err()
+       }
+       if err := w.start(); err != nil {
+               return err
+       }
+       if err := w.client.ping(ctx); err != nil {
+               w.stop()
+               if ctx.Err() != nil {
+                       return ctx.Err()
                }
-               minimized = true
+               if isInterruptError(err) {
+                       // User may have pressed ^C before worker responded.
+                       return err
+               }
+               // TODO: record and return stderr.
+               return fmt.Errorf("fuzzing process terminated without fuzzing: %w", err)
        }
+       return nil
 }
 
 // start runs a new worker process.
@@ -299,7 +282,7 @@ func (w *worker) minimize(ctx context.Context) (res fuzzResult, minimized bool,
 // When the process terminates, w.waitErr is set to the error (if any), and
 // w.termC is closed.
 func (w *worker) start() (err error) {
-       if w.cmd != nil {
+       if w.isRunning() {
                panic("worker already started")
        }
        w.waitErr = nil
@@ -477,7 +460,18 @@ type minimizeArgs struct {
 }
 
 // minimizeResponse contains results from workerServer.minimize.
-type minimizeResponse struct{}
+type minimizeResponse struct {
+       // Err is the error string caused by the value in shared memory.
+       // If Err is empty, minimize was unable to find any shorter values that
+       // caused errors, and the value in shared memory is the original value.
+       Err string
+
+       // Duration is the time spent minimizing, not including starting or cleaning up.
+       Duration time.Duration
+
+       // Count is the number of values tested.
+       Count int64
+}
 
 // fuzzArgs contains arguments to workerServer.fuzz. The value to fuzz is
 // passed in shared memory.
@@ -634,8 +628,8 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
        defer cancel()
        mem := <-ws.memMu
        defer func() {
-               ws.memMu <- mem
                resp.Count = mem.header().count
+               ws.memMu <- mem
        }()
 
        vals, err := unmarshalCorpusFile(mem.valueCopy())
@@ -662,16 +656,6 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
                        ws.m.mutate(vals, cap(mem.valueRef()))
                        writeToMem(vals, mem)
                        if err := ws.fuzzFn(CorpusEntry{Values: vals}); err != nil {
-                               // TODO(jayconrod,katiehockman): report unminimized input to coordinator
-                               // immediately so it can stop other workers.
-                               // TODO(jayconrod,katiehockman): use -fuzzminimizetime to limit time or
-                               // iterations spent on minimization.
-                               minCtx, minCancel := context.WithTimeout(ctx, time.Minute)
-                               defer minCancel()
-                               if minErr := ws.minimizeInput(minCtx, vals, mem, &mem.header().count, args.Limit); minErr != nil {
-                                       // Minimization found a different error, so use that one.
-                                       err = minErr
-                               }
                                resp.Err = err.Error()
                                if resp.Err == "" {
                                        resp.Err = "fuzz function failed with no output"
@@ -693,6 +677,8 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
 }
 
 func (ws *workerServer) minimize(ctx context.Context, args minimizeArgs) (resp minimizeResponse) {
+       start := time.Now()
+       defer func() { resp.Duration = time.Now().Sub(start) }()
        mem := <-ws.memMu
        defer func() { ws.memMu <- mem }()
        vals, err := unmarshalCorpusFile(mem.valueCopy())
@@ -704,7 +690,15 @@ func (ws *workerServer) minimize(ctx context.Context, args minimizeArgs) (resp m
                ctx, cancel = context.WithTimeout(ctx, args.Timeout)
                defer cancel()
        }
-       ws.minimizeInput(ctx, vals, mem, &mem.header().count, args.Limit)
+
+       // Minimize the values in vals, then write to shared memory. We only write
+       // to shared memory after completing minimization. If the worker terminates
+       // unexpectedly before then, the coordinator will use the original input.
+       err = ws.minimizeInput(ctx, vals, &mem.header().count, args.Limit)
+       writeToMem(vals, mem)
+       if err != nil {
+               resp.Err = err.Error()
+       }
        return resp
 }
 
@@ -714,10 +708,7 @@ func (ws *workerServer) minimize(ctx context.Context, args minimizeArgs) (resp m
 // mem just in case an unrecoverable error occurs. It uses the context to
 // determine how long to run, stopping once closed. It returns the last error it
 // found.
-func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, mem *sharedMem, count *int64, limit int64) error {
-       // Make sure the last crashing value is written to mem.
-       defer writeToMem(vals, mem)
-
+func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, count *int64, limit int64) error {
        shouldStop := func() bool {
                return ctx.Err() != nil || (limit > 0 && *count >= limit)
        }
@@ -779,7 +770,6 @@ func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, m
                default:
                        panic("impossible")
                }
-               writeToMem(vals, mem)
                err := ws.fuzzFn(CorpusEntry{Values: vals})
                if err != nil {
                        retErr = err
@@ -798,43 +788,43 @@ func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, m
                case bool:
                        continue // can't minimize
                case float32:
-                       minimizeFloat(ctx, float64(v), tryMinimized, shouldStop)
+                       minimizeFloat(float64(v), tryMinimized, shouldStop)
                case float64:
-                       minimizeFloat(ctx, v, tryMinimized, shouldStop)
+                       minimizeFloat(v, tryMinimized, shouldStop)
                case uint:
-                       minimizeInteger(ctx, v, tryMinimized, shouldStop)
+                       minimizeInteger(v, tryMinimized, shouldStop)
                case uint8:
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case uint16:
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case uint32:
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case uint64:
                        if uint64(uint(v)) != v {
                                // Skip minimizing a uint64 on 32 bit platforms, since we'll truncate the
                                // value when casting
                                continue
                        }
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case int:
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case int8:
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case int16:
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case int32:
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case int64:
                        if int64(int(v)) != v {
                                // Skip minimizing a int64 on 32 bit platforms, since we'll truncate the
                                // value when casting
                                continue
                        }
-                       minimizeInteger(ctx, uint(v), tryMinimized, shouldStop)
+                       minimizeInteger(uint(v), tryMinimized, shouldStop)
                case string:
-                       minimizeBytes(ctx, []byte(v), tryMinimized, shouldStop)
+                       minimizeBytes([]byte(v), tryMinimized, shouldStop)
                case []byte:
-                       minimizeBytes(ctx, v, tryMinimized, shouldStop)
+                       minimizeBytes(v, tryMinimized, shouldStop)
                default:
                        panic("unreachable")
                }
@@ -844,7 +834,6 @@ func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, m
 
 func writeToMem(vals []interface{}, mem *sharedMem) {
        b := marshalCorpusFile(vals...)
-       mem.setValueLen(len(b))
        mem.setValue(b)
 }
 
@@ -907,21 +896,29 @@ var errSharedMemClosed = errors.New("internal error: shared memory was closed an
 
 // minimize tells the worker to call the minimize method. See
 // workerServer.minimize.
-func (wc *workerClient) minimize(ctx context.Context, args minimizeArgs) (valueOut []byte, err error) {
+func (wc *workerClient) minimize(ctx context.Context, valueIn []byte, args minimizeArgs) (valueOut []byte, resp minimizeResponse, err error) {
        wc.mu.Lock()
        defer wc.mu.Unlock()
 
-       var resp minimizeResponse
+       mem, ok := <-wc.memMu
+       if !ok {
+               return nil, minimizeResponse{}, errSharedMemClosed
+       }
+       mem.header().count = 0
+       mem.setValue(valueIn)
+       wc.memMu <- mem
+
        c := call{Minimize: &args}
        err = wc.call(ctx, c, &resp)
-       mem, ok := <-wc.memMu
+       mem, ok = <-wc.memMu
        if !ok {
-               return nil, errSharedMemClosed
+               return nil, minimizeResponse{}, errSharedMemClosed
        }
        valueOut = mem.valueCopy()
+       resp.Count = mem.header().count
        wc.memMu <- mem
 
-       return valueOut, err
+       return valueOut, resp, err
 }
 
 // fuzz tells the worker to call the fuzz method. See workerServer.fuzz.
@@ -944,6 +941,7 @@ func (wc *workerClient) fuzz(ctx context.Context, valueIn []byte, args fuzzArgs)
                return nil, fuzzResponse{}, errSharedMemClosed
        }
        valueOut = mem.valueCopy()
+       resp.Count = mem.header().count
        wc.memMu <- mem
 
        return valueOut, resp, err