Skip to content

[BUG FIX] In-place operations and overwrite with the same bitmap as both operands - #963

Merged
lemire merged 1 commit into
masterfrom
fix-inplace-ops-self-aliasing
Nov 6, 2026
Merged

lemire merged 1 commit into
masterfrom
fix-inplace-ops-self-aliasing

Conversation

@SalvatorePreviti

@SalvatorePreviti SalvatorePreviti commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

In-place operations and overwrite with the same bitmap as both operands

scope net lines lines touched hunks files changed
source +27 49 (+49 -22) 24 7 edited
tests +94 113 (+111 -17) 16 4 edited

Passing one bitmap as both arguments of an in-place operation could read freed memory, give a wrong result or trip
an assert. Each such call is now decided at entry, as roaring_bitmap_and_inplace and
roaring64_bitmap_or_inplace already did: x | x leaves x as it is, x ^ x and x \ x empty it.

These are set identities, true for every set A whatever it holds: union and intersection are idempotent
(A ∪ A = A ∩ A = A); every element of A is also in A, so none is in exactly one of the two (A △ A = ∅) and none
is in A but not in A (A \ A = ∅); copying A onto itself leaves A. So the answer is known from the pointers alone, without reading the containers.

Leaving the check to callers did not work: the headers disagreed (and_inplace allowed it, or_inplace had a TODO,
lazy_or_inplace and overwrite were silent, others forbade it), the only check was an assert that release
builds drop, and aliasing is often invisible at the call site (a fold over a list holding the accumulator, C++
r = other through references). The result is always defined, and one pointer comparison here replaces a guard at every call site.

Note: I do have a follow up PR after this isolated change that improves all the in place operations performance making them all linear

(bug fix) roaring_bitmap_lazy_or_inplace(a, a, true) no longer reads freed memory

The header did not forbid r1 == r2 here; it now says it is allowed. With the bitset conversion on, each container
was converted and freed, then read again as the second operand: a heap use-after-free on array and run containers,
and wrong values when it did not crash. Bitset containers were not converted, so they were not affected.

(bug fix) roaring_bitmap_lazy_xor_inplace(a, a) no longer reads freed memory

Each emptied container was removed from the array that both operands share, while the walk kept the old length: on
a bitmap of several containers, release builds read and copied containers already freed (a heap use-after-free).
Debug builds stopped at its assert(x1 != x2). It now empties the bitmap, its capacity kept.

(bug fix) roaring_bitmap_overwrite(a, a) and C++ Roaring self-assignment no longer read freed memory

ra_overwrite freed the destination's containers, then cloned the source's, which were the same ones. r = r on a
Roaring goes through it. It now leaves the bitmap unchanged (as roaring64_bitmap_overwrite already did) and
returns true, and the header says so.

(bug fix) roaring64_bitmap_xor_inplace(a, a) and roaring64_bitmap_andnot_inplace(a, a) empty the bitmap

The walk erased from the tree it was also reading as the second operand, so values were left behind: about half of
them (67959 of the 142858 in roaring64_bitmap_from_range(0, 1000000, 7)), in release builds for xor and in every
build for andnot, which had no assert.
The C++ Roaring64 operators ^= and -= reach them; their "undefined if &r == this" notes are dropped.

(bug fix) 32-bit xor_inplace and andnot_inplace accept r1 == r2

They asserted r1 != r2 in debug builds (release builds gave the right, empty, result); they now empty the bitmap
(its capacity kept) in every build, so C++ Roaring r ^= r and r -= r work in debug builds too.
or_inplace(a, a) returns at once, and the TODO in its header ("decide whether r1 == r2 ok") becomes "r1 == r2 is
allowed"; the 64-bit or_inplace and overwrite headers now state the same. The C++ -= and ^= docs
(Roaring, Roaring64, Roaring64Map) now say r -= r and r ^= r empty r.

🤖 Written and verified with the aid of Claude Code; I have reviewed every line.

@lemire
lemire merged commit e0620db into master Nov 6, 2026
43 checks passed
@SalvatorePreviti
SalvatorePreviti deleted the fix-inplace-ops-self-aliasing branch November 6, 2026 20:09
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.

2 participants