Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

324 lines
12 KiB
Go

package parser
// Fuzz targets for the Antigravity wire-walk (GitHub #648).
//
// The Antigravity format is an undocumented, unversioned protobuf
// schema that has changed across dot releases; fixtures only cover
// payload shapes we have happened to observe. These targets assert
// output INVARIANTS instead of expected values, so corner cases we
// cannot enumerate still have pinned properties:
//
// - agProtoParse: never panics, returns a structurally sound
// field tree bounded by the input, and is deterministic. The
// total-fields allocation budget (each two-byte wire field
// decodes into a ~100-byte struct, so unbudgeted parses
// amplify memory two orders of magnitude) is asserted here
// but pinned deterministically by TestAgProtoFieldBudget:
// mutated inputs stay far too small to probe it.
// - decodeAntigravityStep: content is non-empty valid UTF-8 with
// no NUL bytes, roles are in the enum, timestamps are zero or
// inside the 2000..2100 plausibility window.
// - extractModelName: empty or printable with at least one letter
// (the 2026-06-11 incident: a nested protobuf fragment whose
// bytes are all < 0x80 is valid UTF-8, leaked into
// messages.model, and broke pg push with SQLSTATE 22021).
// - extractTokenUsage: counts are non-negative and bounded by
// maxPlausibleTokens, including the input+output sum used as a
// combined plausibility guard. Reasoning is always 0 (no per-field
// breakdown available in gen_metadata).
//
// Plain `go test` runs the f.Add seeds plus any regression inputs
// committed under testdata/fuzz/<Target>/, so the corpus is enforced
// on every CI run; `go test -fuzz=<Target>` explores from there.
import (
"bytes"
"testing"
"time"
"unicode"
"unicode/utf8"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// agIncidentFragment is the real gen_metadata fragment (hex
// 080020022A0201024001) that extractModelName persisted verbatim as
// a model name before isPlausibleModelName existed: every byte is
// < 0x80, so it passes utf8.Valid while containing a NUL.
var agIncidentFragment = []byte{
0x08, 0x00, 0x20, 0x02, 0x2A, 0x02, 0x01, 0x02, 0x40, 0x01,
}
// fuzzAgTimestamp encodes a google.protobuf.Timestamp-shaped message.
func fuzzAgTimestamp(sec, nanos uint64) []byte {
return encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: sec},
{num: 2, wire: pbWireVarint, varint: nanos},
})
}
// fuzzAgWireSeeds returns payloads exercising the walker: realistic
// step and gen_metadata shapes, the incident fragment, and malformed
// edges (truncated varints, oversized lengths, deep nesting, group
// wire types, window-boundary timestamps).
func fuzzAgWireSeeds() [][]byte {
userStep := encodePB([]pbField{
{num: 3, wire: pbWireBytes, bytes: fuzzAgTimestamp(1_779_326_586, 959_000_000)},
{num: 17, wire: pbWireBytes, bytes: []byte(
"Please refactor the sync engine to retry failed pushes.",
)},
})
nulStep := encodePB([]pbField{
{num: 17, wire: pbWireBytes, bytes: []byte(
"abcdefghij\x00klmnopqrstuvwxyz now twenty-plus runes",
)},
})
// f2=5529 (uncached input), f3=72 (total output), f5=38885 (cache-read).
// Remapped field semantics cross-validated against sidecar ground truth.
usageBlock := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1020},
{num: 2, wire: pbWireVarint, varint: 5529},
{num: 3, wire: pbWireVarint, varint: 72},
{num: 5, wire: pbWireVarint, varint: 38885},
})
genMeta := encodePB([]pbField{
{num: 17, wire: pbWireBytes, bytes: usageBlock},
{num: 21, wire: pbWireBytes, bytes: []byte("gemini-3.5-flash")},
})
nestedFragmentModel := encodePB([]pbField{
{num: 2, wire: pbWireBytes, bytes: encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: agIncidentFragment},
})},
})
// Decoy: f2 input above cap.
usageDecoyLatency := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1020},
{num: 2, wire: pbWireVarint, varint: 2_000_001},
{num: 3, wire: pbWireVarint, varint: 1},
})
// Decoy: f3 output with wrong wire type.
usageDecoyWire := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1020},
{num: 2, wire: pbWireVarint, varint: 5},
{num: 3, wire: pbWireBytes, bytes: []byte("xx")},
})
// Decoy: f2+f3 sum above cap (each individually plausible).
usageDecoyFoldedCap := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1020},
{num: 2, wire: pbWireVarint, varint: 2_000_000},
{num: 3, wire: pbWireVarint, varint: 2_000_000},
})
onion := []byte("core")
for range agProtoMaxDepth + 4 {
onion = encodePB([]pbField{
{num: 1, wire: pbWireBytes, bytes: onion},
})
}
// Step-shaped payloads (nested timestamp + a collectable
// string) so the window-boundary values actually reach the
// timestamp assertions in FuzzDecodeAntigravityStep: bare
// timestamp messages decode to no content and bail before
// any invariant is checked.
stepWithTS := func(sec, nanos uint64) []byte {
return encodePB([]pbField{
{num: 3, wire: pbWireBytes, bytes: fuzzAgTimestamp(sec, nanos)},
{num: 17, wire: pbWireBytes, bytes: []byte(
"Boundary probe prompt with enough runes to keep.",
)},
})
}
return [][]byte{
nil,
{0x00}, // zero field number
{0x80}, // truncated tag varint
{0x08}, // varint field, missing value
{0x0A, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F}, // length far past input
{0x09, 0x01}, // short fixed64
{0x0D, 0x01, 0x02}, // short fixed32
{0x0B, 0x0C}, // start/end group pair
{0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01}, // 10-byte varint
agIncidentFragment,
userStep,
nulStep,
genMeta,
nestedFragmentModel,
usageDecoyLatency,
usageDecoyWire,
usageDecoyFoldedCap,
onion,
bytes.Repeat([]byte{0x08, 0x00}, 64), // dense minimal fields
fuzzAgTimestamp(1_779_326_586, 4_000_000_000), // nanos out of range
// Out-of-range nanos behind collectable content: the
// timestamp is rejected, so the decoded message keeps a
// zero Timestamp instead of a shifted one.
stepWithTS(1_779_326_586, 4_000_000_000),
stepWithTS(946_684_800, 0), // window lower bound (excluded)
stepWithTS(946_684_801, 999_999_999), // first inside window
stepWithTS(4_102_444_799, 999_999_999), // last inside window
stepWithTS(4_102_444_800, 0), // window upper bound (excluded)
encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: []byte(" ")},
}), // whitespace-only model candidate
encodePB([]pbField{
{num: 19, wire: pbWireBytes, bytes: []byte("123-456_789")},
}), // letterless model candidate
}
}
// checkAgProtoFieldTree asserts the structural invariants of a
// decoded field tree against the buffer it was parsed from: positive
// field numbers, wire types from the wire-format enum, fixed payloads
// of exactly 4/8 bytes, length-delimited payloads no longer than the
// enclosing buffer, and nesting bounded by the recursion cap. Returns
// the total number of fields in the tree so callers can assert the
// allocation budget.
func checkAgProtoFieldTree(
t *testing.T, fields []agProtoField, buf []byte, depth int,
) int {
t.Helper()
require.LessOrEqual(t, depth, agProtoMaxDepth,
"nested past the recursion cap")
total := len(fields)
for _, f := range fields {
assert.GreaterOrEqual(t, f.Number, 1, "field number")
switch f.Wire {
case pbWireVarint:
assert.Nil(t, f.Fixed, "varint field carries fixed bytes")
assert.Nil(t, f.Bytes, "varint field carries payload")
case pbWireFixed64:
assert.Len(t, f.Fixed, 8, "fixed64 payload")
case pbWireFixed32:
assert.Len(t, f.Fixed, 4, "fixed32 payload")
case pbWireBytes:
assert.LessOrEqual(t, len(f.Bytes), len(buf),
"payload longer than enclosing buffer")
if f.Nested != nil {
total += checkAgProtoFieldTree(
t, f.Nested, f.Bytes, depth+1,
)
}
case pbWireStartGroup, pbWireEndGroup:
assert.Nil(t, f.Fixed, "group field carries fixed bytes")
assert.Nil(t, f.Bytes, "group field carries payload")
default:
assert.Failf(t, "unknown wire type",
"field %d wire %d", f.Number, f.Wire)
}
}
return total
}
func FuzzAgProtoParse(f *testing.F) {
for _, seed := range fuzzAgWireSeeds() {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
fields, err := agProtoParse(data)
if err != nil {
return
}
total := checkAgProtoFieldTree(t, fields, data, 0)
assert.LessOrEqual(t, total, agProtoMaxFields,
"decoded fields exceed the allocation budget")
again, err := agProtoParse(data)
require.NoError(t, err, "second parse of accepted input")
assert.Equal(t, fields, again, "parse is not deterministic")
})
}
func FuzzAgProtoLooksLikePrefix(f *testing.F) {
for _, seed := range fuzzAgWireSeeds() {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
// Panic safety only: the prefix sniff runs on untrusted
// decryption candidates, but shares no output contract
// with the full parse (it bounds field numbers, the
// parser does not).
first := agProtoLooksLikePrefix(data)
assert.Equal(t, first, agProtoLooksLikePrefix(data),
"prefix sniff is not deterministic")
})
}
func FuzzDecodeAntigravityStep(f *testing.F) {
for i, seed := range fuzzAgWireSeeds() {
f.Add(i, 14, seed)
f.Add(i, 2, seed)
}
windowMin := time.Unix(946_684_801, 0)
windowMax := time.Unix(4_102_444_800, 0)
f.Fuzz(func(t *testing.T, idx, stepType int, payload []byte) {
msg, ok := decodeAntigravityStep(idx, stepType, payload)
if !ok {
return
}
require.NotEmpty(t, msg.Content, "decoded message without content")
assert.True(t, utf8.ValidString(msg.Content),
"content is not valid UTF-8")
assert.NotContains(t, msg.Content, "\x00",
"content contains a NUL byte")
assert.Equal(t, len(msg.Content), msg.ContentLength)
wantRole := RoleAssistant
if stepType == 14 {
wantRole = RoleUser
}
assert.Equal(t, wantRole, msg.Role)
if !msg.Timestamp.IsZero() {
assert.False(t, msg.Timestamp.Before(windowMin),
"timestamp %v before plausibility window", msg.Timestamp)
assert.False(t, !msg.Timestamp.Before(windowMax),
"timestamp %v after plausibility window", msg.Timestamp)
}
})
}
func FuzzExtractModelName(f *testing.F) {
for _, seed := range fuzzAgWireSeeds() {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
model := extractModelName(data)
if model == "" {
return
}
hasLetter := false
for _, r := range model {
assert.True(t, unicode.IsPrint(r),
"model %q contains non-printable rune %q", model, r)
if unicode.IsLetter(r) {
hasLetter = true
}
}
assert.True(t, hasLetter, "model %q has no letters", model)
})
}
func FuzzExtractTokenUsage(f *testing.F) {
for _, seed := range fuzzAgWireSeeds() {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
block, ok := extractTokenUsage(data)
if !ok {
assert.Zero(t, block.UncachedInput)
assert.Zero(t, block.TotalOutput)
assert.Zero(t, block.CacheRead)
return
}
for name, v := range map[string]int{
"uncachedInput": block.UncachedInput, "totalOutput": block.TotalOutput, "cacheRead": block.CacheRead,
} {
assert.GreaterOrEqual(t, v, 0, "%s tokens negative", name)
assert.LessOrEqual(t, v, maxPlausibleTokens,
"%s tokens above plausibility cap", name)
}
// The caller uses input+output as a combined plausibility guard
// (each independently bounded, but decoys with both near the cap
// are implausible for a single generation).
assert.LessOrEqual(t, block.UncachedInput+block.TotalOutput, maxPlausibleTokens,
"input+output above plausibility cap")
})
}