Fix null pointers passed to memcmp/memcpy in skip_list #65

Merged
andrew merged 1 commits from weaselbot/conflict-set:weaselbot/issue-64 into main 2026-07-13 21:55:54 +00:00
Member

Closes #64.

The skip list implementation passed nullptr to memcmp/memcpy on valid input, which is undefined behavior even when the count is 0, and is flagged by UBSan. Two root causes:

  1. addWrites sizing-constructor bug: std::vector<KeyInfo>(count * 2) value-initialized count*2 default KeyInfos (whose key span has data() == nullptr, size() == 0), then emplace_back appended the real points after them. std::is_sorted/sortPoints then compared the junk entries via operator<, calling memcmp with null pointers. The sizing constructor was meant to be reserve.

  2. Empty spans reaching memcmp/memcpy: setOldestVersion resolves an empty removal key (data() == nullptr, size() == 0), passing it to SkipList::less (memcmp) and copyToArena (memcpy).

Changes

  • addWrites: replace the sizing constructor with reserve(count * 2) so no junk entries are created (also avoids the wasted memory/sort work).
  • copyToArena: return early for an empty key instead of memcpy-ing with a null pointer.
  • SkipList::less: skip memcmp when there are no bytes to compare (n > 0 ? memcmp(...) : 0).
  • operator<: guard the memcmp with i > 0 ? ... : 0. When one key is empty and the other is not, c defaults to 0 and the existing downstream length comparison (lhs.key.size() < rhs.key.size()) places the empty key first, so ordering is unchanged.

Verification

Built with -fsanitize=undefined (and address) on aarch64/GCC 16.1.1:

  • The issue's two repros (a point write on "key"; setOldestVersion on a fresh conflict set) produce no UBSan reports.
  • The full test_conflict_set.py suite (bisimulating the skip list against the radix tree via DebugConflictSet) is clean under UBSan.
  • The same repros and suite against an unpatched build trigger the UBSan reports from the issue, confirming the fix targets the reported behavior.
Closes #64. The skip list implementation passed `nullptr` to `memcmp`/`memcpy` on valid input, which is undefined behavior even when the count is `0`, and is flagged by UBSan. Two root causes: 1. **`addWrites` sizing-constructor bug:** `std::vector<KeyInfo>(count * 2)` value-initialized `count*2` default `KeyInfo`s (whose `key` span has `data() == nullptr`, `size() == 0`), then `emplace_back` *appended* the real points after them. `std::is_sorted`/`sortPoints` then compared the junk entries via `operator<`, calling `memcmp` with null pointers. The sizing constructor was meant to be `reserve`. 2. **Empty spans reaching `memcmp`/`memcpy`:** `setOldestVersion` resolves an empty removal key (`data() == nullptr`, `size() == 0`), passing it to `SkipList::less` (`memcmp`) and `copyToArena` (`memcpy`). ### Changes - `addWrites`: replace the sizing constructor with `reserve(count * 2)` so no junk entries are created (also avoids the wasted memory/sort work). - `copyToArena`: return early for an empty key instead of `memcpy`-ing with a null pointer. - `SkipList::less`: skip `memcmp` when there are no bytes to compare (`n > 0 ? memcmp(...) : 0`). - `operator<`: guard the `memcmp` with `i > 0 ? ... : 0`. When one key is empty and the other is not, `c` defaults to `0` and the existing downstream length comparison (`lhs.key.size() < rhs.key.size()`) places the empty key first, so ordering is unchanged. ### Verification Built with `-fsanitize=undefined` (and address) on aarch64/GCC 16.1.1: - The issue's two repros (a point write on `"key"`; `setOldestVersion` on a fresh conflict set) produce no UBSan reports. - The full `test_conflict_set.py` suite (bisimulating the skip list against the radix tree via `DebugConflictSet`) is clean under UBSan. - The same repros and suite against an unpatched build trigger the UBSan reports from the issue, confirming the fix targets the reported behavior.
weaselbot added 1 commit 2026-07-13 16:04:30 +00:00
Fix null pointers passed to memcmp/memcpy in skip_list
CI / pre-commit (pull_request) Successful in 2m14s
CI / release (arm64, ubuntu-latest-arm64) (pull_request) Successful in 3m32s
CI / test (-DCMAKE_BUILD_TYPE=Debug, debug) (pull_request) Successful in 3m38s
CI / test (-DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 64-bit-versions) (pull_request) Successful in 3m29s
CI / test (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc) (pull_request) Successful in 3m39s
CI / test (-DUSE_SIMD_FALLBACK=ON, simd-fallback) (pull_request) Successful in 3m30s
CI / release (amd64, ubuntu-latest-amd64) (pull_request) Successful in 5m1s
CI / coverage (pull_request) Successful in 3m38s
2231c093df
In addWrites, the sizing constructor std::vector<KeyInfo>(count * 2)
was used instead of reserve(count * 2), leaving count*2 default-
constructed (null/empty key) entries that were then compared via
operator<, passing nullptr to memcmp. Switch to reserve so only real
entries exist.

Additionally guard the memcmp/memcpy calls in operator<, SkipList::less,
and copyToArena against empty spans (where data() may be nullptr), which
is reachable on the first setOldestVersion with an empty removal key.

Closes #64
andrew requested changes 2026-07-13 19:51:47 +00:00
Dismissed
SkipList.cpp Outdated
@@ -98,3 +101,3 @@
bool operator<(const KeyInfo &lhs, const KeyInfo &rhs) {
int i = std::min(lhs.key.size(), rhs.key.size());
int c = memcmp(lhs.key.data(), rhs.key.data(), i);
int c = i > 0 ? memcmp(lhs.key.data(), rhs.key.data(), i) : 0;
Owner

If one of them is non-empty, this could compare them incorrectly

If one of them is non-empty, this could compare them incorrectly
andrew marked this conversation as resolved
andrew requested changes 2026-07-13 20:43:19 +00:00
andrew left a comment
Owner

I missed that there was a downstream length comparison. First commit was fine. Please remove the last commit

I missed that there was a downstream length comparison. First commit was fine. Please remove the last commit
weaselbot force-pushed weaselbot/issue-64 from 494e836aae to 2231c093df 2026-07-13 21:30:30 +00:00 Compare
andrew scheduled this pull request to auto merge when all checks succeed 2026-07-13 21:32:39 +00:00
andrew merged commit d96d862cea into main 2026-07-13 21:55:54 +00:00
andrew deleted branch weaselbot/issue-64 2026-07-13 21:55:54 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/conflict-set#65