Skip to content

Fix Xor modifying its argument and 64-bit self-XOR - #564

Open
gitRasheed wants to merge 2 commits into
RoaringBitmap:masterfrom
gitRasheed:fix/xor-argument-mutation
Open

Fix Xor modifying its argument and 64-bit self-XOR#564
gitRasheed wants to merge 2 commits into
RoaringBitmap:masterfrom
gitRasheed:fix/xor-argument-mutation

Conversation

@gitRasheed

@gitRasheed gitRasheed commented Sep 5, 2026

Copy link
Copy Markdown

Description

Three bugs I hit while making roaring64.Bitmap.Xor work in place (#565). Each fails on master with the tests added here.

  1. 32-bit a.Xor(b) modifies b when a container of a is an array or run and the matching container of b is a bitmap. Since v2.14.5 that pair runs the in-place XOR on b's container and adopts it, so b holds the result too and the two bitmaps share a container:

    a := roaring.BitmapOf(1, 3, 5)
    b := roaring.New()
    for i := uint32(0); i < 65536; i += 2 {
    	b.Add(i)
    }
    a.Xor(b)
    a.Add(777)
    b.Contains(777) // true
  2. 64-bit Xor inserted the containers of argument-only keys by pointer, so a.Xor(b); a.Remove(v) removed v from b. They are now cloned, as Or does since roaring64: honor copy-on-write in Bitmap.Or #525.

  3. 64-bit x.Xor(x) with more than one key panicked while iterating the structure it was deleting from. It now clears the receiver, as the 32-bit Xor does.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Changes Made

What was changed?

  • arraycontainer.go, runcontainer.go: ixorBitmap computes on the receiver's side.
  • roaring64/roaring64.go: clone on insertion, early return for self-XOR.
  • inplace_ownership_test.go in both packages: every in-place operation on every pair of nine container shapes, copy-on-write on and off, checked against the pure function; the argument must be unchanged after the call and after the result is edited; each operation applied to the same bitmap twice.

Why was it changed?

An in-place operation must not modify its argument.

How was it changed?

Two one-line call changes, a Clone() and a four-line guard.

Testing

go test ./... passes on arm64 and amd64. Or, And and AndNot pass the new test on master; only Xor fails.

Formatting

go fmt clean.

Performance Impact

Cloning costs where the old code shared. The two disjoint-key benchmarks, four interleaved single-value keys per input, slow down: two XORs that insert and then cancel go from 359 to 1,310 ns on c8g.xlarge with 4 to 40 allocations, and clone plus one XOR goes from 1,069 to 1,705 ns. Matching-key synthetic cases move by up to 8.6% between the two builds and the six real-data cases by under 0.5%.

Breaking Changes

None.

@gitRasheed gitRasheed mentioned this pull request Sep 5, 2026
1 task
An array or run receiver XORed with a bitmap container ran the in-place
operation on the argument's container and adopted it, so a.Xor(b)
changed b and left both bitmaps sharing storage. Compute the result on
the receiver's side instead. The test covers every in-place operation
and container pair.
Keys present only in the argument were inserted by pointer, so editing
the receiver afterwards changed the argument. Clone them, as Or does.
XORing a bitmap with itself removed entries from the structure being
iterated and could panic; clear the receiver instead, as the 32-bit Xor
does.
@gitRasheed
gitRasheed force-pushed the fix/xor-argument-mutation branch from 0c0d656 to 0765837 Compare September 5, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant