From c45ea8dfac104c55e38e15dd68667217e9b14297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 6 Feb 2026 19:35:32 +0800 Subject: [PATCH] Recover from bbolt panics on corrupted database When bbolt encounters corrupted page data at runtime, it panics instead of returning an error. Wrap all DB transactions with recover to catch these panics, delete the corrupted database file, and reopen a fresh one. --- experimental/cachefile/cache.go | 59 +++++++++++++++++++++++++++----- experimental/cachefile/fakeip.go | 12 +++---- experimental/cachefile/rdrc.go | 6 ++-- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 88cffdbe..ac2d7002 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -45,6 +45,7 @@ type CacheFile struct { storeRDRC bool rdrcTimeout time.Duration DB *bbolt.DB + resetAccess sync.Mutex saveMetadataTimer *time.Timer saveFakeIPAccess sync.RWMutex saveDomain map[netip.Addr]string @@ -169,13 +170,55 @@ func (c *CacheFile) Close() error { return c.DB.Close() } +func (c *CacheFile) view(fn func(tx *bbolt.Tx) error) (err error) { + defer func() { + if r := recover(); r != nil { + c.resetDB() + err = E.New("database corrupted: ", r) + } + }() + return c.DB.View(fn) +} + +func (c *CacheFile) batch(fn func(tx *bbolt.Tx) error) (err error) { + defer func() { + if r := recover(); r != nil { + c.resetDB() + err = E.New("database corrupted: ", r) + } + }() + return c.DB.Batch(fn) +} + +func (c *CacheFile) update(fn func(tx *bbolt.Tx) error) (err error) { + defer func() { + if r := recover(); r != nil { + c.resetDB() + err = E.New("database corrupted: ", r) + } + }() + return c.DB.Update(fn) +} + +func (c *CacheFile) resetDB() { + c.resetAccess.Lock() + defer c.resetAccess.Unlock() + c.DB.Close() + os.Remove(c.path) + db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second}) + if err == nil { + _ = filemanager.Chown(c.ctx, c.path) + c.DB = db + } +} + func (c *CacheFile) StoreFakeIP() bool { return c.storeFakeIP } func (c *CacheFile) LoadMode() string { var mode string - c.DB.View(func(t *bbolt.Tx) error { + c.view(func(t *bbolt.Tx) error { bucket := t.Bucket(bucketMode) if bucket == nil { return nil @@ -193,7 +236,7 @@ func (c *CacheFile) LoadMode() string { } func (c *CacheFile) StoreMode(mode string) error { - return c.DB.Batch(func(t *bbolt.Tx) error { + return c.batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(bucketMode) if err != nil { return err @@ -230,7 +273,7 @@ func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error) func (c *CacheFile) LoadSelected(group string) string { var selected string - c.DB.View(func(t *bbolt.Tx) error { + c.view(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketSelected) if bucket == nil { return nil @@ -245,7 +288,7 @@ func (c *CacheFile) LoadSelected(group string) string { } func (c *CacheFile) StoreSelected(group, selected string) error { - return c.DB.Batch(func(t *bbolt.Tx) error { + return c.batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketSelected) if err != nil { return err @@ -255,7 +298,7 @@ func (c *CacheFile) StoreSelected(group, selected string) error { } func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) { - c.DB.View(func(t *bbolt.Tx) error { + c.view(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketExpand) if bucket == nil { return nil @@ -271,7 +314,7 @@ func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) { } func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { - return c.DB.Batch(func(t *bbolt.Tx) error { + return c.batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketExpand) if err != nil { return err @@ -286,7 +329,7 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary { var savedSet adapter.SavedBinary - err := c.DB.View(func(t *bbolt.Tx) error { + err := c.view(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketRuleSet) if bucket == nil { return os.ErrNotExist @@ -304,7 +347,7 @@ func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary { } func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error { - return c.DB.Batch(func(t *bbolt.Tx) error { + return c.batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketRuleSet) if err != nil { return err diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 8fe0f113..7a4bd384 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -23,7 +23,7 @@ var ( func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { var metadata adapter.FakeIPMetadata - err := c.DB.Batch(func(tx *bbolt.Tx) error { + err := c.batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { return os.ErrNotExist @@ -45,7 +45,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { } func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { - return c.DB.Batch(func(tx *bbolt.Tx) error { + return c.batch(func(tx *bbolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) if err != nil { return err @@ -69,7 +69,7 @@ func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) { } func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error { - return c.DB.Batch(func(tx *bbolt.Tx) error { + return c.batch(func(tx *bbolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) if err != nil { return err @@ -136,7 +136,7 @@ func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) { return cachedDomain, true } var domain string - _ = c.DB.View(func(tx *bbolt.Tx) error { + _ = c.view(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { return nil @@ -163,7 +163,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo return cachedAddress, true } var address netip.Addr - _ = c.DB.View(func(tx *bbolt.Tx) error { + _ = c.view(func(tx *bbolt.Tx) error { var bucket *bbolt.Bucket if isIPv6 { bucket = tx.Bucket(bucketFakeIPDomain6) @@ -180,7 +180,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo } func (c *CacheFile) FakeIPReset() error { - return c.DB.Batch(func(tx *bbolt.Tx) error { + return c.batch(func(tx *bbolt.Tx) error { err := tx.DeleteBucket(bucketFakeIP) if err != nil { return err diff --git a/experimental/cachefile/rdrc.go b/experimental/cachefile/rdrc.go index c4800951..d27ea8b2 100644 --- a/experimental/cachefile/rdrc.go +++ b/experimental/cachefile/rdrc.go @@ -31,7 +31,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) ( copy(key[2:], qName) defer buf.Put(key) var deleteCache bool - err := c.DB.View(func(tx *bbolt.Tx) error { + err := c.view(func(tx *bbolt.Tx) error { bucket := c.bucket(tx, bucketRDRC) if bucket == nil { return nil @@ -56,7 +56,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) ( return } if deleteCache { - c.DB.Update(func(tx *bbolt.Tx) error { + c.update(func(tx *bbolt.Tx) error { bucket := c.bucket(tx, bucketRDRC) if bucket == nil { return nil @@ -72,7 +72,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) ( } func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) error { - return c.DB.Batch(func(tx *bbolt.Tx) error { + return c.batch(func(tx *bbolt.Tx) error { bucket, err := c.createBucket(tx, bucketRDRC) if err != nil { return err