fuzzfp

package
v1.54.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

Documentation

Overview

Package fuzzfp watches the arguments a fuzz target hands to a builtin and reports the mutation that no builtin may ever perform: reaching inside an LNative to change the Go value a host handed in.

It used to report a second one -- rewriting a value's source location -- and that half is gone; see "# Source: removed on this branch" below for why, and for what covers it now.

Why only that one

A blanket "the arguments came back unchanged" assertion is not available here, because several callables mutate an argument BY DESIGN -- `append!` grows a vector in place, `assoc!` writes a sorted-map, and MacroCall writes to the nodes a macro returns. A guard that fired on those would be turned off within a week. So this package asserts only properties that hold for every callable in the library, and says so in one place rather than leaving each target to decide.

Source: removed on this branch

This package used to watch a second property -- that no builtin rewrites a value's source location -- and issue #318's tracker recorded that as its headline result. It is gone, and it is not coming back in this form.

The rule read LVal.Source as an exported field, and its whole premise was that every value the interpreter builds without a parser behind it points at ONE process-wide token.Location (lisp.nativeSource), so an in-place edit relocated a large fraction of the process. Both halves of that premise were deleted by the sealing work: #362 removed the shared native-source singleton and synthesizes locations on demand (9edf260), and LVal.Source became an unexported field behind a value-copy accessor (69be094). There is no shared Location left to corrupt and no exported pointer left to corrupt it through, so the rule cannot be expressed here and would have nothing to catch.

What replaced it is stronger, not weaker. The sealed-AST fingerprint (lisp/sealfp.go) hashes source CONTENTS as part of each sealed argument's fingerprint, so a location rewrite on any sealed argument is caught by assertion 5 in lisp/lisplib/fuzz_test.go -- and caught as a seal violation, which names the corruption class directly. The one case the old rule covered and the new one does not is a location rewrite on an UNSEALED argument (vectors, maps, natives); that is runtime storage the kernel may legitimately rework, which is why it is unsealed in the first place.

LNative

The tracker's stated reason for recording only an LNative's dynamic type was that formatting an arbitrary Go value can traverse a randomised map. That is true of fmt, and it is a property of fmt rather than of the values: a walk that sorts map entries by their own fingerprint is deterministic over exactly the same inputs. [fingerprintGo] is that walk. It reads unexported fields through reflect without ever calling Value.Interface (which would panic on them), so it sees inside time.Time and *regexp.Regexp, and it breaks cycles on pointer identity so a self-referential native terminates.

Deterministic here means "twice in one process gives the same answer", which is what a before/after comparison needs and is also the strong form: Go randomises map iteration per range statement per run, so two ranges in one process already disagree. TestFingerprintIsDeterministic exercises that directly, and TestGuardIsDeterministicOverGeneratedValues does it over the whole generated corpus.

What is deliberately NOT watched

  • Cells, Str, Int, Float, Quoted, Spliced. Legitimately mutated; see above.
  • The Native of an LFun (an *LFunData, holding an *LEnv), an LError (a call stack) or an LBytes (a *[]byte). Only nodes whose Type is LNative have their Native fingerprinted, which is what keeps the walk away from an entire environment and from the deliberately-mutable byte slice.
  • Meta. Only populated in format-preserving mode, which no builtin target runs in.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Fingerprint

func Fingerprint(v any) string

Fingerprint returns the structural fingerprint of a Go value. Exported for the determinism tests; the guard itself uses it internally.

Types

type Guard

type Guard struct {
	// contains filtered or unexported fields
}

Guard is a set of watched nodes. Create one with Watch before the call and call Guard.Check after it.

func Watch

func Watch(v *lisp.LVal) *Guard

Watch records every LVal reachable from v.

Nodes are recorded BY POINTER, not by position, so a builtin that legitimately reshapes the tree -- appends a cell, replaces one, sorts in place -- does not knock the before and after records out of alignment and produce a spurious report. A node that is dropped from the tree entirely is still watched: whoever dropped it may still be holding it.

func (*Guard) Check

func (g *Guard) Check() string

Check re-derives every watched property and reports the first violation, or "" when there is none.

One violation rather than all of them: a fuzz failure is read by a human looking for the smallest reproducer, and a builtin that relocates one node has almost always relocated a thousand.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL