diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-10 09:48:18 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-10 09:48:18 -0700 |
| commit | 21f24d24fd6f501b32f15a2bef41c89cc461f623 (patch) | |
| tree | ba4952fb82f3ef9c265228633f27536866e23931 /hotline/field_fuzz_test.go | |
| parent | ae44fb222ec73cae8441f5a5d9a21f8585fe587f (diff) | |
Overhaul regression testing: e2e suite, fuzzing, CI, and bug fixes
Add a protocol-level end-to-end suite (in-process fully wired server on
an ephemeral port pair, driven by hotline.Client over TCP) covering
handshake, login, public and private chat, message board, threaded
news, file list/download/upload, account admin, disconnect
notification, and shutdown broadcast. The harness retries on a fresh
port pair when another process steals a probed port before
ListenAndServe binds it, and Server gains WithConnectionRateLimit so
tests can disable the per-IP connection throttle.
Add native fuzz tests for Transaction, Field, and flattened file
object decoding, and fix the bugs the new tests surfaced:
- Transaction.Write panicked on out-of-range attacker-controlled size
fields, and transactionScanner's uint32 length addition could wrap
and yield a truncated token. The information fork size declared in
an untrusted fork header is now bounded too.
- Client keepalive read c.done unsynchronized while Disconnect
replaces it under the mutex.
- The shared Agreement's Seek+ReadAll login path raced concurrent
logins; the server now prefers an AgreementBytes() snapshot.
Fill unit-test gaps (main's config-copy helpers, file resume data,
ReloaderFunc, R2 error injection and env validation) and add a CI test
workflow (build/vet + race-enabled shuffled suite), fixed lint
workflow triggers with golangci-lint v2.6, and Makefile test/cover/
lint/fuzz targets.
Diffstat (limited to 'hotline/field_fuzz_test.go')
| -rw-r--r-- | hotline/field_fuzz_test.go | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/hotline/field_fuzz_test.go b/hotline/field_fuzz_test.go new file mode 100644 index 0000000..1af4f5e --- /dev/null +++ b/hotline/field_fuzz_test.go @@ -0,0 +1,76 @@ +package hotline + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// FuzzFieldScanner verifies the field-framing split func never panics or advances past its input. +func FuzzFieldScanner(f *testing.F) { + f.Add([]byte{0x00, 0x65, 0x00, 0x03, 0x68, 0x61, 0x69}) // FieldData "hai" + f.Add([]byte{0x00, 0x65, 0xff, 0xff}) // declared size larger than input + f.Add([]byte{0x00}) // shorter than the size field + + f.Fuzz(func(t *testing.T, data []byte) { + advance, token, err := FieldScanner(data, false) + if err != nil { + return + } + if advance == 0 { + assert.Nil(t, token, "no advance must produce no token") + return + } + require.LessOrEqual(t, advance, len(data), "scanner advanced past its input") + require.Len(t, token, advance, "token length must match advance") + }) +} + +// FuzzFieldWrite feeds raw untrusted bytes to the Field decoder. If decoding succeeds, +// re-encoding must reproduce exactly the bytes that were consumed. +func FuzzFieldWrite(f *testing.F) { + f.Add([]byte{0x00, 0x65, 0x00, 0x03, 0x68, 0x61, 0x69}) // FieldData "hai" + f.Add([]byte{0x00, 0x65, 0x00, 0x00}) // empty data + f.Add([]byte{0x00, 0x65, 0xff, 0xff, 0x00}) // declared size overruns buffer + + f.Fuzz(func(t *testing.T, data []byte) { + var field Field + n, err := field.Write(data) + if err != nil { + return + } + + encoded, err := io.ReadAll(&field) + require.NoError(t, err) + assert.Equal(t, data[:n], encoded, "re-encoding a decoded field must reproduce the consumed bytes") + }) +} + +// TestField_RoundTrip pins encode→decode symmetry for fields built with NewField. +func TestField_RoundTrip(t *testing.T) { + tests := []struct { + name string + field Field + }{ + {name: "with data", field: NewField(FieldData, []byte("hello"))}, + {name: "empty data", field: NewField(FieldUserPassword, []byte{})}, + {name: "binary data", field: NewField(FieldUserIconID, []byte{0x07, 0xd1})}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encoded, err := io.ReadAll(&tt.field) + require.NoError(t, err) + + var decoded Field + n, err := decoded.Write(encoded) + require.NoError(t, err) + assert.Equal(t, len(encoded), n) + assert.Equal(t, tt.field.Type, decoded.Type) + assert.Equal(t, tt.field.FieldSize, decoded.FieldSize) + assert.Equal(t, tt.field.Data, decoded.Data) + }) + } +} |