Skip to content

fix: kll_sketch::update() copies the item before a compaction can free it - #573

Open
stojkomilos wants to merge 1 commit into
apache:masterfrom
stojkomilos:stojkomilos/kll-update-aliased-item
Open

stojkomilos wants to merge 1 commit into
apache:masterfrom
stojkomilos:stojkomilos/kll-update-aliased-item

Conversation

@stojkomilos

@stojkomilos stojkomilos commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

kll_sketch::update(item) copies item into the sketch after internal_update(). If item refers to an item of the same sketch compaction is triggered, those items move. This then leads to item becoming invalid in the middle of the call kll_sketch::update(item).

Note: on master, this is triggered only by compaction, but after #569 merged, it will also be triggered by regrowth. This is not too important for understanding this PR, as the fix is identical for both situations.

Master

This is @proost 's test from #569 (see the next section), with k = 200 instead of 5000, so that the 201st update triggers the first compaction:

TEST_CASE("kll sketch: update with aliased item during compaction", "[kll_sketch]") {
  const uint16_t k = 200;
  kll_sketch<std::string> sketch(k);

  for (int i = 0; i < kll_constants::DEFAULT_K; ++i) {
    sketch.update(std::string(64, 'x') + std::to_string(i));
  }

  REQUIRE(sketch.get_n() == kll_constants::DEFAULT_K);

  const std::string& item = sketch.begin()->first;
  const std::string expected = item;

  // Triggers compaction
  sketch.update(item);

  REQUIRE(sketch.get_n() == kll_constants::DEFAULT_K + 1);

  uint32_t count = 0;
  for (const auto& entry : sketch) {
    if (entry.first == expected) {
      ++count;
    }
  }
  REQUIRE(count == 2);
}

On master (98c6b74), with GCC 10.5 and 10 runs per build:

  • -O3: fails 10/10 with std::bad_alloc.
  • -O1 -fsanitize=address: heap-use-after-free 10/10. The read is in update() at kll_sketch_impl.hpp:185. The memory was freed in add_empty_top_level_to_completely_full_sketch() (:742), called from compress_while_updating() (:662) and internal_update() (:202).

sketch.update(sketch.get_quantile(0.5)) on a kll_sketch<std::string> hits the same bug, because get_quantile() returns a reference into the sketch. On master it reports heap-use-after-free under ASan. With this PR, the added item equals the median.

Tests

Added 2 tests that would catch this bug. One is for regrowth and one is for compaction.

update_min_max(static_cast<const T&>(item)); // min and max are always copies
const uint32_t index = internal_update();
new (&items_[index]) T(std::forward<FwdT>(item));
if (levels_[0] == 0) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compaction and regrowth can only happen when level 0 is full. So we use that fact to handle this situation.

…e it

If the item refers to an item of the same sketch and level 0 is full,
internal_update() compacts first, which can destroy that item or free
items_. update() then read freed memory.

Adds a test for this compaction case, and proost's test from apache#569 for
the buffer growth case, which passes on master.

Co-authored-by: Isaac <no-reply@databricks.com>
@stojkomilos
stojkomilos force-pushed the stojkomilos/kll-update-aliased-item branch from 0c1dd75 to eabcc54 Compare November 12, 2026 10:13
@stojkomilos
stojkomilos marked this pull request as ready for review November 12, 2026 10:15
@leerho

leerho commented Nov 12, 2026

Copy link
Copy Markdown
Member

Merge order for the open KLL PRs: #573 first, then #558, then #569. #569's buffer growth depends on this fix, and #558 and #569 change the same storage code, so landing them in that order keeps each rebase small. This one goes first once CI passes and it's approved.

This branch has not been deployed

No deployments
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