Skip to content

Commit

Permalink
add test check page
Browse files Browse the repository at this point in the history
Signed-off-by: Mustafa Elbehery <[email protected]>
  • Loading branch information
Elbehery committed Jan 24, 2024
1 parent 248e6f8 commit ac4b7d3
Showing 1 changed file with 220 additions and 0 deletions.
220 changes: 220 additions & 0 deletions db_whitebox_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package bbolt

import (
"bytes"
crand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand"
"path/filepath"
"testing"
"unsafe"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.etcd.io/bbolt/errors"
"go.etcd.io/bbolt/internal/common"
"go.etcd.io/bbolt/internal/guts_cli"
)

func TestOpenWithPreLoadFreelist(t *testing.T) {
Expand Down Expand Up @@ -112,6 +120,218 @@ func TestMethodPage(t *testing.T) {
}
}

func TestTx_Check_CorruptPage_ViolateBtreeInvariant(t *testing.T) {
bucketKey := "testBucket"

t.Logf("Creating db file.")
db, err := Open(filepath.Join(t.TempDir(), "db"), 0666, &Options{PageSize: 4096})
require.NoError(t, err)
defer func() {
require.NoError(t, db.Close())
}()

uErr := db.Update(func(tx *Tx) error {
t.Logf("Creating bucket '%v'.", bucketKey)
b, bErr := tx.CreateBucketIfNotExists([]byte(bucketKey))
require.NoError(t, bErr)
t.Logf("Generating random data in bucket '%v'.", bucketKey)
generateSampleDataInBucket(t, b, 3)
return nil
})
require.NoError(t, uErr)

t.Logf("Corrupting random leaf page in bucket '%v'.", bucketKey)
victimPageId, validPageIds := corruptLeafPage(t, db, false)

t.Log("Running consistency check.")
vErr := db.View(func(tx *Tx) error {
chkConfig := checkConfig{
kvStringer: HexKVStringer(),
}

t.Log("Check corrupted page.")
ch := make(chan error)
chkConfig.pageId = uint(victimPageId)
go func() {
defer close(ch)
tx.check(chkConfig, ch)
}()

var cErrs []error
for cErr := range ch {
cErrs = append(cErrs, cErr)
}
require.Equal(t, 1, len(cErrs))
require.ErrorContains(t, cErrs[0], fmt.Sprintf("leaf page(%d)", victimPageId))

t.Log("Check valid pages.")
cErrs = cErrs[:0]
for _, pgId := range validPageIds {
ch = make(chan error)
chkConfig.pageId = uint(pgId)
go func() {
defer close(ch)
tx.check(chkConfig, ch)
}()

for cErr := range ch {
cErrs = append(cErrs, cErr)
}
require.Equal(t, 0, len(cErrs))
}
return nil
})
require.NoError(t, vErr)
}

func TestTx_Check_CorruptPage_DumpRandomBytes(t *testing.T) {
bucketKey := "testBucket"

t.Logf("Creating db file.")
db, err := Open(filepath.Join(t.TempDir(), "db"), 0666, &Options{PageSize: 4096})
require.NoError(t, err)
defer func() {
require.NoError(t, db.Close())
}()

uErr := db.Update(func(tx *Tx) error {
t.Logf("Creating bucket '%v'.", bucketKey)
b, bErr := tx.CreateBucketIfNotExists([]byte(bucketKey))
require.NoError(t, bErr)
t.Logf("Generating random data in bucket '%v'.", bucketKey)
generateSampleDataInBucket(t, b, 3)
return nil
})
require.NoError(t, uErr)

t.Logf("Corrupting random leaf page in bucket '%v'.", bucketKey)
victimPageId, _ := corruptLeafPage(t, db, true)

t.Log("Running consistency check.")
vErr := db.View(func(tx *Tx) error {
chkConfig := checkConfig{
kvStringer: HexKVStringer(),
pageId: uint(victimPageId),
}

ch := make(chan error)
defer close(ch)

defer func() {
r := recover()
require.NotNil(t, r)
}()

tx.check(chkConfig, ch)

return nil
})
require.NoError(t, vErr)
}

// corruptLeafPage write an invalid leafPageElement into the victim page.
func corruptLeafPage(t testing.TB, db *DB, expectPanic bool) (victimPageId common.Pgid, validPageIds []common.Pgid) {
t.Helper()

victimPageId, validPageIds = findVictimPageId(t, db)

victimPage, victimBuf, err := guts_cli.ReadPage(db.Path(), uint64(victimPageId))
require.NoError(t, err)
require.True(t, victimPage.IsLeafPage())
require.True(t, victimPage.Count() > 0)

// Dumping random bytes in victim page for corruption.
copy(victimBuf[32:], generateCorruptionBytes(t, expectPanic))
// Write the corrupt page to db file.
err = guts_cli.WritePage(db.Path(), victimBuf)
require.NoError(t, err)

return victimPageId, validPageIds
}

// findVictimPageId finds all the leaf pages of a bucket and picks a random leaf page as a victim to be corrupted.
func findVictimPageId(t testing.TB, db *DB) (victimPageId common.Pgid, validPageIds []common.Pgid) {
t.Helper()
// Read DB's RootPage.
rootPageId, _, err := guts_cli.GetRootPage(db.Path())
require.NoError(t, err)
rootPage, _, err := guts_cli.ReadPage(db.Path(), uint64(rootPageId))
require.NoError(t, err)
require.True(t, rootPage.IsLeafPage())
require.Equal(t, 1, len(rootPage.LeafPageElements()))
// Find Bucket's RootPage.
lpe := rootPage.LeafPageElement(uint16(0))
require.Equal(t, uint32(common.BranchPageFlag), lpe.Flags())
k := lpe.Key()
require.Equal(t, "testBucket", string(k))
bucketRootPageId := lpe.Bucket().RootPage()
// Read Bucket's RootPage.
bucketRootPage, _, err := guts_cli.ReadPage(db.Path(), uint64(bucketRootPageId))
require.NoError(t, err)
require.Equal(t, uint16(common.BranchPageFlag), bucketRootPage.Flags())
// Retrieve Bucket's PageIds
var bucketPageIds []common.Pgid
for _, bpe := range bucketRootPage.BranchPageElements() {
bucketPageIds = append(bucketPageIds, bpe.Pgid())
}

randomIdx := rand.Intn(len(bucketPageIds))
victimPageId = bucketPageIds[randomIdx]
validPageIds = append(bucketPageIds[:randomIdx], bucketPageIds[randomIdx+1:]...)
return victimPageId, validPageIds
}

// generateSampleDataInBucket fill in sample data into given bucket to create the given
// number of leafPages. To control the number of leafPages, sample data are generated in order.
func generateSampleDataInBucket(t testing.TB, bk *Bucket, lPages int) {
t.Helper()

maxBytesInPage := 2200

currentKey := 1
currentVal := 100
for i := 0; i < lPages; i++ {
currentSize := common.PageHeaderSize
for {
err := bk.Put(convertIntIntoBytes(t, currentKey), convertIntIntoBytes(t, currentVal))
require.NoError(t, err)
currentSize += common.LeafPageElementSize + unsafe.Sizeof(currentKey) + unsafe.Sizeof(currentVal)
if int(currentSize) >= maxBytesInPage {
break
}
currentKey++
currentVal++
}
}
}

// generateCorruptionBytes returns random bytes to corrupt a page.
// It inserts a page element which violates the btree key order if no panic is expected.
// Otherwise, it dumps random bytes into a page which causes a panic.
func generateCorruptionBytes(t testing.TB, expectPanic bool) []byte {
if expectPanic {
// Generated data size is between pageHeader and pageSize.
corruptDataLength := rand.Intn(4096-16) + 16
corruptData := make([]byte, corruptDataLength)
_, err := crand.Read(corruptData)
require.NoError(t, err)
return corruptData
}
// Insert LeafPageElement which violates the BTree range.
invalidLPE := common.NewLeafPageElement(0, 0, 0, 0)
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, invalidLPE)
require.NoError(t, err)
return buf.Bytes()
}

func convertIntIntoBytes(t testing.TB, i int) []byte {
t.Helper()
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, uint32(i))
return buf
}

func prepareData(t *testing.T) (string, error) {
fileName := filepath.Join(t.TempDir(), "db")
db, err := Open(fileName, 0666, nil)
Expand Down

0 comments on commit ac4b7d3

Please sign in to comment.