RoaringBitmap / RoaringBitmap/roaring
`Bitmap.Clone()` data race on copy-on-write bitmaps (no writers)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 2.9k
- Forks
- 262
- Avg merge
- 2h 34m
- Merged PRs (30d)
- 8
Description
Package: github.com/RoaringBitmap/roaring/v2
Version: v2.18.0
Go: 1.26.2
Summary
Bitmap.Clone() is not safe for concurrent use on a copy-on-write bitmap, even when there are no writers. Two goroutines that each only call src.Clone() on the same shared bitmap race with each other.
When copyOnWrite is enabled, (*roaringArray).clone() calls ra.markAllAsNeedingCopyOnWrite(), which writes the source bitmap's needCopyOnWrite slice (sets every element to true) on every clone. So concurrent clones of one source perform concurrent writes to the same slice → data race.
This is surprising because:
Clone()reads like a read-only operation on the source.- A COW bitmap is a natural candidate for a shared, immutable snapshot that many goroutines clone concurrently (e.g. a decoded index block held in a cache).
Relevant source
roaringarray.go:
func (ra *roaringArray) clone() *roaringArray {
sa := roaringArray{}
sa.copyOnWrite = ra.copyOnWrite
// this is where copyOnWrite is used.
if ra.copyOnWrite {
sa.keys = make([]uint16, len(ra.keys))
copy(sa.keys, ra.keys)
sa.containers = make([]container, len(ra.containers))
copy(sa.containers, ra.containers)
sa.needCopyOnWrite = make([]bool, len(ra.needCopyOnWrite))
ra.markAllAsNeedingCopyOnWrite() // <-- WRITES THE SOURCE
sa.markAllAsNeedingCopyOnWrite()
// sa.needCopyOnWrite is shared
} else {
// make a full copy
...
}
return &sa
}
func (ra *roaringArray) markAllAsNeedingCopyOnWrite() {
for i := range ra.needCopyOnWrite {
ra.needCopyOnWrite[i] = true // <-- racy write under concurrent Clone()
}
}
Reproducer
Standalone module that depends only on roaring/v2.
go.mod:
module roaringracerepro
go 1.22
require github.com/RoaringBitmap/roaring/v2 v2.18.0
race_test.go:
package roaringracerepro
import (
"sync"
"testing"
"github.com/RoaringBitmap/roaring/v2"
)
// TestConcurrentCloneOfCOWBitmapRaces shows that Clone() of a single
// copy-on-write bitmap is NOT safe to call concurrently, even though every
// goroutine only "reads" the shared source by cloning it.
//
// Root cause: with copy-on-write enabled, (*Bitmap).Clone() ->
// roaringArray.clone() calls ra.markAllAsNeedingCopyOnWrite(), which WRITES
// the *source* bitmap's needCopyOnWrite slice (sets every element to true).
// Two goroutines cloning the same source therefore write the same slice
// concurrently -> data race.
//
// Run: go test -race -run TestConcurrentCloneOfCOWBitmapRaces
func TestConcurrentCloneOfCOWBitmapRaces(t *testing.T) {
src := roaring.New()
// Add enough values across many high-16-bit groups so the bitmap holds
// multiple containers (i.e. needCopyOnWrite has several entries to scribble on).
for i := uint32(0); i < 1<<20; i += 977 {
src.Add(i)
}
src.SetCopyOnWrite(true)
const (
goroutines = 8
iterations = 2000
)
start := make(chan struct{})
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start // line everyone up so the clones overlap
for i := 0; i < iterations; i++ {
_ = src.Clone()
}
}()
}
close(start)
wg.Wait()
}
Run:
go test -race -run TestConcurrentCloneOfCOWBitmapRaces
Race detector output
==================
WARNING: DATA RACE
Write at 0x00c000012490 by goroutine 10:
github.com/RoaringBitmap/roaring/v2.(*roaringArray).markAllAsNeedingCopyOnWrite()
.../roaring/v2@v2.18.0/roaringarray.go:776 +0x52c
github.com/RoaringBitmap/roaring/v2.(*roaringArray).clone()
.../roaring/v2@v2.18.0/roaringarray.go:270 +0x284
github.com/RoaringBitmap/roaring/v2.(*Bitmap).Clone()
.../roaring/v2@v2.18.0/roaring.go:1038 +0xc0
roaringracerepro.TestConcurrentCloneOfCOWBitmapRaces.func1()
race_test.go:42 +0x9c
Previous write at 0x00c000012490 by goroutine 12:
github.com/RoaringBitmap/roaring/v2.(*roaringArray).markAllAsNeedingCopyOnWrite()
.../roaring/v2@v2.18.0/roaringarray.go:776 +0x52c
github.com/RoaringBitmap/roaring/v2.(*roaringArray).clone()
.../roaring/v2@v2.18.0/roaringarray.go:270 +0x284
github.com/RoaringBitmap/roaring/v2.(*Bitmap).Clone()
.../roaring/v2@v2.18.0/roaring.go:1038 +0xc0
roaringracerepro.TestConcurrentCloneOfCOWBitmapRaces.func1()
race_test.go:42 +0x9c
==================
--- FAIL: TestConcurrentCloneOfCOWBitmapRaces (0.04s)
testing.go:1712: race detected during execution of test
FAIL
Both goroutines write the same address (0x00c000012490), i.e. an element of the source bitmap's needCopyOnWrite slice.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with roaringarray.go, especially roaringArray.clone and markAllAsNeedingCopyOnWrite, then trace the call from Bitmap.Clone in roaring.go. Run the provided race_test.go reproducer with go test -race -run TestConcurrentCloneOfCOWBitmapRaces. Done means concurrent clones no longer report a race while preserving copy-on-write behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100