accounts: copy bbolt values before closing read transaction

bucket.Get returns a byte slice that points directly into bbolt's
mmap'd file. This slice is only valid for the lifetime of the
transaction. In Account() and LastIndexes(), the slices were escaping
the View callback and being read after the transaction closed. Under
concurrent write transactions the file gets remapped, invalidating
the pointer and causing a segmentation fault.

Copy the byte slices inside the transaction so deserialization
operates on stable, heap-allocated memory.
This commit is contained in:
bitromortac 2026-04-09 10:39:48 +02:00
parent 50376dd429
commit 21933e8c8e
No known key found for this signature in database
GPG key ID: 1965063FC13BEBE2

View file

@ -457,11 +457,17 @@ func (s *BoltStore) Account(_ context.Context, id AccountID) (
return ErrAccountBucketNotFound
}
accountBinary = bucket.Get(id[:])
if len(accountBinary) == 0 {
v := bucket.Get(id[:])
if len(v) == 0 {
return ErrAccNotFound
}
// Copy the value since bbolt's Get returns a slice into
// the mmap'd file that is only valid for the lifetime of
// the transaction.
accountBinary = make([]byte, len(v))
copy(accountBinary, v)
return nil
}, func() {
accountBinary = nil
@ -560,16 +566,24 @@ func (s *BoltStore) LastIndexes(_ context.Context) (uint64, uint64, error) {
return ErrAccountBucketNotFound
}
addValue = bucket.Get(lastAddIndexKey)
if len(addValue) == 0 {
av := bucket.Get(lastAddIndexKey)
if len(av) == 0 {
return ErrNoInvoiceIndexKnown
}
settleValue = bucket.Get(lastSettleIndexKey)
if len(settleValue) == 0 {
sv := bucket.Get(lastSettleIndexKey)
if len(sv) == 0 {
return ErrNoInvoiceIndexKnown
}
// Copy values since bbolt's Get returns slices into the
// mmap'd file, only valid within the transaction.
addValue = make([]byte, len(av))
copy(addValue, av)
settleValue = make([]byte, len(sv))
copy(settleValue, sv)
return nil
}, func() {
addValue, settleValue = nil, nil