skip_list: newestVersion - 2e9 is computed in double precision, so the TooOld window boundary is wrong for versions >= 2^53 (Commit instead of TooOld); the test oracle in Internal.h has the same bug
#99
Reference in new issue
Block a user
Summary
SkipList.cpp:744decidesTooOldfor reads that fell out of the two-billion-version window with2e9is adoubleliteral, sonewestVersion - 2e9is evaluated indouble(theint64_toperand is converted, andreadVersionis converted for the comparison). Adoublehas a 53-bit mantissa, so for anynewestVersion >= 2^53the boundary is off by up to ~512 versions, in both directions. The result is thatcheck()returnsCommitfor reads that the API says must beTooOld, andTooOldfor reads that are exactly at the boundary and must not be.The radix tree gets this right because it keeps the window as an integer (
ConflictSet.cpp:99,constexpr int64_t kNominalVersionWindow = 2e9;, used atConflictSet.cpp:5178-5192), so the two shipped implementations disagree on the same valid input.Internal.h:516(ReferenceImpl::check, the test oracle used byTestDriver/the corpus driver) contains the identical expression, so the differential tests share the defect and cannot catch it (and would flag the correct radix tree if the corpus ever produced such versions).Reproduction
Same 3-operation program, linked against each shipped implementation:
Observed:
The repo's own bisimulation harness (
DebugConflictSetintest_conflict_set.py, which asserts that the skip list and the radix tree agree) fails on the same input:It is wrong in both directions
Measured with the same program (
skip_listonly; the radix tree is correct in every case):newestVersionreadVersionreadVersion < newest - 2e9)newest-2e9-1)newest-2e9-1)newest-2e9-1)newest-2e9)newest-2e9)For
newestVersion = INT64_MAX, everyreadVersionin[newest-2e9-510, newest-2e9-1]is misreported as not-TooOld(510 consecutive versions), becausedouble(INT64_MAX)rounds up to2^63. In general the error is bounded by half an ULP ofnewestVersion, i.e. up to 512 versions near 2^63 and 256 near 2^62.Expected vs actual
include/ConflictSet.hstates (both the C++ and the C text):TooOldforreadVersion < newestVersion - 2·10^9(exact, allint64_t).skip_list): the comparison is done indouble, so the answer flips wheneverreadVersionis within ~ULP(newestVersion)/2of the boundary.Why this is valid input
>= 0;int64_tversions nearINT64_MAXare explicitly treated as valid by this repo --conflict_set_cxx_api_test.cppexercisesINT64_MAX - 1e9with the comment "Versions are only required to be >= 0, so this is valid input" (the regression test for #84).count == 0inaddWrites("newestVersion(and possiblyoldestVersion) still advance") is a documented postcondition, so advancingnewestVersionwithout adding a write is valid usage.Impact
2e9window exists so that reads whose versions may have been garbage collected are toldTooOldinstead of being given an answer (see the reasoning quoted in #98 and commita68ad5d).skip_listsilently returnsCommitfor such reads, which is exactly the case the window is meant to prevent: a transaction reading a version whose writes were reclaimed is allowed to commit.Conflictis equally possible if stale entries remain.Internal.h:516has the same2e9expression, theENABLE_FUZZcorpus/TestDriveroracle agrees with the buggy skip list, so this class of bug is invisible to the differential tests (and the oracle would blame the radix tree if the corpus ever generated versions >= 2^53).commits_total/too_olds_totalare also wrong for those reads.Not a duplicate
hash_tablenever implements the window at all (nonewestVersionmember). Different file, different cause; here the rule is implemented but evaluated with the wrong precision.addWritesnearINT64_MAX. Different file and expression;ConflictSet.cppalready uses the integer constantkNominalVersionWindow.SkipList.cpp::setOldestVersionmonotonicity. Different function.Suggested fix
Use integer arithmetic in both places:
and add a regression case to
test_conflict_set.py(theDebugConflictSetbisimulation makes it fail without the fix):