Skip to content

Guard the in-place operations against self-application - #31

Merged
lemire merged 1 commit into
masterfrom
fix-inplace-self-aliasing
Aug 22, 2026
Merged

Guard the in-place operations against self-application#31
lemire merged 1 commit into
masterfrom
fix-inplace-self-aliasing

Conversation

@lemire

@lemire lemire commented Aug 22, 2026

Copy link
Copy Markdown
Member

Follow-up to #30. This was ready as a second commit on that branch but did not make it in before the merge, so master currently aborts the process on rb.Xor(rb).

The crash

roaring_bitmap_xor_inplace, roaring_bitmap_andnot_inplace, roaring_bitmap_lazy_xor_inplace and roaring64_bitmap_xor_inplace assert that their two operands are distinct:

rb := gocroaring.New(1, 2, 3)
rb.Xor(rb)
Assertion failed: (x1 != x2), function roaring_bitmap_xor_inplace, file roaring.c, line 16340.
SIGABRT: abort
signal arrived during cgo execution

Under -DNDEBUG the assertion is compiled out and the same call corrupts memory instead, which is the worse outcome.

The C library is not consistent about this, so there is no single rule a caller could learn:

routine 32-bit 64-bit
and_inplace guards guards
or_inplace no guard guards
overwrite no guard guards
xor_inplace asserts asserts
andnot_inplace asserts no guard
lazy_xor_inplace asserts n/a

Rather than document a precondition per method, the wrapper now answers the aliased case itself, which costs one pointer comparison: And, Or and Assign become no-ops, Xor and AndNot empty the bitmap. That is what the set algebra says the answer is.

Empty ranges

roaring_bitmap_from_range returns a null pointer both for a zero step and for max <= min. Only the first is a programming error, but I had guarded only the first, so FromRange(5, 5, 1) panicked with "C code returned a null pointer." instead of returning the empty bitmap its documentation promises. Fixed on both widths.

Documentation

ReadFrozenView64 needs the buffer length to be exactly the length written (the 32-bit doc said so, the 64-bit one did not), and MoveFrom32 takes the containers without regard for copy-on-write sharing, so it should not be used on a bitmap that has been cloned with copy-on-write enabled.

Testing

New tests cover every aliased combination on both widths and the empty-range cases. Full suite passes, including under -race.

https://claude.ai/code/session_01NzKGBVi8pbqkwocqktcjLU

roaring_bitmap_xor_inplace, roaring_bitmap_andnot_inplace,
roaring_bitmap_lazy_xor_inplace and roaring64_bitmap_xor_inplace assert that
their two operands are distinct, so rb.Xor(rb) aborted the whole process
rather than emptying the bitmap:

    Assertion failed: (x1 != x2), function roaring_bitmap_xor_inplace

Under -DNDEBUG the assertion is compiled out and the same call corrupts
memory instead. The remaining in-place routines are inconsistent about it:
and_inplace guards on both widths, or_inplace and overwrite guard only on
the 64-bit side, and the 64-bit andnot_inplace guards nowhere.

Rather than document a precondition per method, answer the aliased case in
the wrapper: And and Or and Assign become no-ops, Xor and AndNot empty the
bitmap. That is what the set algebra calls for, and it costs one pointer
comparison.

Also stop turning an empty range into a panic. roaring_bitmap_from_range
returns a null pointer both for a zero step and for max <= min, and only
the former is a programming error, so FromRange and FromRange64 now return
an empty bitmap for an empty range as their documentation promised.

Two documentation gaps: ReadFrozenView64 needs the buffer length to be
exactly the length written, and MoveFrom32 takes the containers without
regard for copy-on-write sharing.

Claude-Session: https://claude.ai/code/session_01NzKGBVi8pbqkwocqktcjLU
@lemire
lemire merged commit f445943 into master Aug 22, 2026
10 checks passed
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