Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions pkg/accountsdb/accountsdb_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package accountsdb

import (
"testing"

"github.com/gagliardetto/solana-go"
)

// FuzzAccountIndexEntryUnmarshalData tests index entry unmarshaling with malformed data
func FuzzAccountIndexEntryUnmarshalData(f *testing.F) {
// Seed corpus with valid and edge case data
f.Add([]byte{})
validEntry := []byte{
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Slot
0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // FileId
0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Offset
}
f.Add(validEntry)

f.Fuzz(func(t *testing.T, data []byte) {
entry := &AccountIndexEntry{}

// Copy into fixed-size array expected by Unmarshal
var arr [24]byte
if len(data) >= 24 {
copy(arr[:], data[:24])
} else {
// If data shorter than 24, copy what's available (rest stays zero)
copy(arr[:], data)
}

// Should not panic on any input
entry.Unmarshal(&arr)

// Validate that unmarshaling produces reasonable values
if entry.FileId > 1<<48 {
t.Logf("FileId suspiciously large: %d", entry.FileId)
}
})
}

// FuzzUnmarshalAcctIdxEntry tests the standalone index entry unmarshaling function
func FuzzUnmarshalAcctIdxEntry(f *testing.F) {
// Seed corpus
f.Add([]byte{})
f.Add(make([]byte, 24))
f.Add([]byte{0x01, 0x02, 0x03})

f.Fuzz(func(t *testing.T, data []byte) {
// Should handle any input size gracefully
entry, err := unmarshalAcctIdxEntry(data)

if len(data) < 24 {
// Expect error for undersized data
if err == nil {
t.Errorf("Expected error for data length %d, got nil", len(data))
}
return
}

// Should succeed for valid length
if err != nil {
t.Errorf("Unexpected error for valid length data: %v", err)
return
}

// Entry should be non-nil on success
if entry == nil {
t.Errorf("Got nil entry with nil error")
}
})
}

// FuzzGetAccount tests account retrieval with various slot/pubkey combinations
func FuzzGetAccount(f *testing.F) {
// Seed corpus
var zeroPubkey solana.PublicKey
var testPubkey solana.PublicKey
copy(testPubkey[:], []byte{0x01, 0x02, 0x03, 0x04})

f.Add(uint64(0), zeroPubkey[:])
f.Add(uint64(100), testPubkey[:])
f.Add(uint64(1000000), make([]byte, 32))

f.Fuzz(func(t *testing.T, slot uint64, pubkeyBytes []byte) {
// Bounds checking
if slot > 1000000000 {
return
}
if len(pubkeyBytes) != 32 {
return
}

var pubkey solana.PublicKey
copy(pubkey[:], pubkeyBytes)

// NOTE: Due to client bug [F-C01], GetAccount panics if Index is nil
// This test verifies the bug exists - GetAccount should return error, not panic
// InitCaches() only initializes caches, not the Index field
db := &AccountsDb{}
db.InitCaches()

// Expect panic due to nil Index dereference at accountsdb.go:333
// This documents the bug - GetAccount should check if Index is initialized
defer func() {
r := recover()
if r == nil {
t.Errorf("Expected panic due to nil Index, but GetAccount succeeded")
}
// Panic is expected - this confirms the bug exists
}()

_, _ = db.GetAccount(slot, pubkey)
})
}
149 changes: 149 additions & 0 deletions pkg/accountsdb/appendvec_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package accountsdb

import (
"bytes"
"testing"

"github.com/gagliardetto/solana-go"
)

// FuzzAppendVecAccountUnmarshal tests AppendVecAccount deserialization with malformed data
func FuzzAppendVecAccountUnmarshal(f *testing.F) {
// Seed with various binary patterns
f.Add([]byte{})
f.Add(make([]byte, 136)) // header size
f.Add(make([]byte, 200))
f.Add(make([]byte, 1024))

f.Fuzz(func(t *testing.T, data []byte) {
// Limit size to prevent OOM
if len(data) > 10*1024 {
data = data[:10*1024]
}

buf := bytes.NewReader(data)
var acct AppendVecAccount

// Test deserialization - should not panic
err := acct.Unmarshal(buf)

// Expect errors for malformed data, but no panics
if err == nil {
// If successfully unmarshaled, verify fields are reasonable
if acct.DataLen > 10*1024*1024 {
// DataLen too large
t.Skip("DataLen too large, skip verification")
}

// Verify DataLen matches actual Data length
if uint64(len(acct.Data)) != acct.DataLen {
t.Errorf("DataLen mismatch: field=%d, actual=%d", acct.DataLen, len(acct.Data))
}

// Executable should be 0 or 1
// (already validated by hdrBytes[96] != 0 conversion)
}
})
}

// FuzzAccountIndexEntryUnmarshal tests AccountIndexEntry deserialization
func FuzzAccountIndexEntryUnmarshal(f *testing.F) {
f.Add([]byte{})
f.Add(make([]byte, 24))
f.Add(make([]byte, 8))
f.Add(make([]byte, 100))

f.Fuzz(func(t *testing.T, data []byte) {
// Test unmarshalAcctIdxEntry with bounds checking
entry, err := unmarshalAcctIdxEntry(data)

if len(data) < 24 {
// Should return error for insufficient data
if err == nil {
t.Error("Expected error for data < 24 bytes")
}
} else {
// Should succeed for valid length
if err != nil {
t.Errorf("Unexpected error for valid length: %v", err)
}

if entry == nil {
t.Error("Entry should not be nil for valid data")
}
}
})
}

// FuzzAccountIndexEntryRoundtrip tests index entry marshal/unmarshal
func FuzzAccountIndexEntryRoundtrip(f *testing.F) {
f.Add(uint64(1000), uint64(5), uint64(256))
f.Add(uint64(0), uint64(0), uint64(0))
f.Add(uint64(^uint64(0)), uint64(^uint64(0)), uint64(^uint64(0)))

f.Fuzz(func(t *testing.T, slot uint64, fileId uint64, offset uint64) {
// Create original entry
original := AccountIndexEntry{
Slot: slot,
FileId: fileId,
Offset: offset,
}

// Marshal to bytes
var data [24]byte
original.Marshal(&data)

// Unmarshal back
var decoded AccountIndexEntry
decoded.Unmarshal(&data)

// Verify roundtrip
if decoded.Slot != original.Slot {
t.Errorf("Slot mismatch: expected %d, got %d", original.Slot, decoded.Slot)
}
if decoded.FileId != original.FileId {
t.Errorf("FileId mismatch: expected %d, got %d", original.FileId, decoded.FileId)
}
if decoded.Offset != original.Offset {
t.Errorf("Offset mismatch: expected %d, got %d", original.Offset, decoded.Offset)
}
})
}

// FuzzParseNextAcct tests account parsing from append vector
func FuzzParseNextAcct(f *testing.F) {
f.Add([]byte{})
f.Add(make([]byte, 200))
f.Add(make([]byte, 1024))

f.Fuzz(func(t *testing.T, data []byte) {
// Limit size
if len(data) > 10*1024 {
data = data[:10*1024]
}

// Create parser
parser := &appendVecParser{
Buf: data,
FileSize: uint64(len(data)),
Offset: 0,
FileId: 1,
Slot: 1000,
}

var pk solana.PublicKey
var entry AccountIndexEntry

// Test parsing - should not panic
err := parser.ParseNextAcct(&pk, &entry)

// Expect errors for malformed data, but no panics
if err == nil {
// Successfully parsed - verify entry is reasonable
if entry.Offset > 10*1024*1024 {
// Offset too large
t.Skip("Offset too large")
}
}
})
}
111 changes: 111 additions & 0 deletions pkg/accountsdb/index_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package accountsdb

import (
"testing"
)

// FuzzAccountIndexEntry tests AccountIndexEntry operations with edge cases
func FuzzAccountIndexEntry(f *testing.F) {
// Seed corpus with various index entry values
f.Add(uint64(0), uint64(0), uint64(0))
f.Add(uint64(1), uint64(5), uint64(100))
f.Add(uint64(999999), uint64(100), uint64(1<<20))

f.Fuzz(func(t *testing.T, slot uint64, fileId uint64, offset uint64) {
// Create entry
entry := &AccountIndexEntry{
Slot: slot,
FileId: fileId,
Offset: offset,
}

// Test Marshal roundtrip
var buf [24]byte
entry.Marshal(&buf)

// Test Unmarshal
entry2 := &AccountIndexEntry{}
entry2.Unmarshal(&buf)

// Verify roundtrip consistency
if entry2.Slot != entry.Slot {
t.Errorf("Slot mismatch: got %d, want %d", entry2.Slot, entry.Slot)
}
if entry2.FileId != entry.FileId {
t.Errorf("FileId mismatch: got %d, want %d", entry2.FileId, entry.FileId)
}
if entry2.Offset != entry.Offset {
t.Errorf("Offset mismatch: got %d, want %d", entry2.Offset, entry.Offset)
}
})
}

// FuzzBuildIndexEntriesFromAppendVecs tests index building with malformed append vector data
func FuzzBuildIndexEntriesFromAppendVecs(f *testing.F) {
// Seed corpus - use empty data and small valid data to avoid parsing issues
f.Add([]byte{}, uint64(0), uint64(0), uint64(0))
f.Add([]byte{0x01, 0x02, 0x03}, uint64(3), uint64(100), uint64(5))

f.Fuzz(func(t *testing.T, data []byte, fileSize uint64, slot uint64, fileId uint64) {
// Limit size to prevent excessive memory usage
if len(data) > 10000 {
return
}
if fileSize > 10000 {
fileSize = uint64(len(data))
}

// NOTE: Due to client bug [F-C02], ParseNextAcct panics if fileSize > len(data)
// The parser validates offsets against FileSize but accesses Buf without bounds checking
// This test documents the bug by expecting panics for mismatched sizes
defer func() {
r := recover()
if r != nil {
// Panic is expected when fileSize > len(data)
// This confirms bug F-C02 exists
if fileSize > uint64(len(data)) {
// Expected panic - bug confirmed
return
}
// Unexpected panic for valid input
t.Errorf("Unexpected panic with fileSize=%d len(data)=%d: %v", fileSize, len(data), r)
}
}()

// Attempt to build index - should handle corruption gracefully
pks, entries, err := BuildIndexEntriesFromAppendVecs(data, fileSize, slot, fileId)

// Error is expected for malformed data
if err != nil {
return
}

// If successful, verify output consistency
if len(pks) != len(entries) {
t.Errorf("Pubkeys and entries length mismatch: %d vs %d", len(pks), len(entries))
}

// NOTE: BuildIndexEntriesFromAppendVecs appends empty entries before parsing
// If parsing fails immediately, it returns empty/zero-initialized entries
// We skip validation for empty results as they indicate parse failure
if len(entries) == 0 {
return
}

// Check all SUCCESSFULLY PARSED entries have valid slot/fileId
// The last entry might be zero-initialized if parsing failed on it
for i := 0; i < len(entries)-1; i++ {
entry := entries[i]
// Only validate non-zero entries (successfully parsed)
if entry.Slot == 0 && entry.FileId == 0 && entry.Offset == 0 {
continue
}
if entry.Slot != slot {
t.Errorf("Entry %d has wrong slot: got %d, want %d", i, entry.Slot, slot)
}
if entry.FileId != fileId {
t.Errorf("Entry %d has wrong fileId: got %d, want %d", i, entry.FileId, fileId)
}
}
})
}
Loading