When ConflictSet.cpp is compiled with -DUSE_64_BIT=1 on aarch64, the ARM NEON fast paths in checkMaxBetweenExclusiveImpl and scan16 treat InternalVersionT as a 32-bit value. They copy only the low 32 bits of each version into uint32x4_t lanes and compare them against a 32-bit readVersion. This yields wrong conflict/Commit decisions once any version exceeds 2^32 - 1.
This is a correctness bug in the aarch64 SIMD code, distinct from #68 (which is about reading uninitialized child slots).
1/2 Test #5730: script_test_internal_version_zero ............***Failed
AssertionError in test_conflict_set.py:28 (DebugConflictSet compares radix_tree vs skip_list)
Expected commit, got conflict for read of [x8e...x00x01, x8e...x01) at version 6993204482
Root cause
ConflictSet.cpp has three ARM NEON blocks that load only 4 bytes per version:
scan16 with indices (used by Node48): lines 2109–2131
uint32x4_tw4[4];memcpy(w4,vs,sizeof(w4));// only 64 bytes; wrong when InternalVersionT is 8 bytes
uint32_trv;memcpy(&rv,&readVersion,sizeof(rv));// only low 32 bits
scan16 without indices (used by Node256): lines 2176–2200
same truncation when comparing childMaxVersion against readVersion.
With USE_64_BIT=1, InternalVersionT stores an int64_t, so 8 bytes per element. The helpers copy at most 4 bytes per element and compare only the low 32 bits, which is incorrect for versions above 2^32.
Verification
Building the same configuration with the SIMD fallback makes the failures disappear:
Any aarch64 build using 64-bit versions (USE_64_BIT=1) can return spurious Conflict results (or miss real ones, depending on the high bits) once the version counter exceeds 2^32 - 1. This violates the documented result semantics and breaks the existing script_test_internal_version_zero regression test as well as many fuzz-corpus blackbox tests.
## Summary
When `ConflictSet.cpp` is compiled with `-DUSE_64_BIT=1` on aarch64, the ARM NEON fast paths in `checkMaxBetweenExclusiveImpl` and `scan16` treat `InternalVersionT` as a 32-bit value. They copy only the low 32 bits of each version into `uint32x4_t` lanes and compare them against a 32-bit `readVersion`. This yields wrong conflict/Commit decisions once any version exceeds `2^32 - 1`.
This is a correctness bug in the aarch64 SIMD code, distinct from #68 (which is about reading uninitialized child slots).
## Reproduction
On an aarch64 host with clang:
```sh
cmake -B build-64 -S . -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1
cmake --build build-64 -j4
ctest --test-dir build-64 -R script_test_internal_version_zero --output-on-failure
```
Result:
```
1/2 Test #5730: script_test_internal_version_zero ............***Failed
AssertionError in test_conflict_set.py:28 (DebugConflictSet compares radix_tree vs skip_list)
```
Many corpus blackbox tests also abort, e.g.:
```sh
./build-64/driver corpus/eace36b916b0cde74b86ce8b89aaf9c0271e04b0
```
Output:
```
Expected commit, got conflict for read of [x8e...x00x01, x8e...x01) at version 6993204482
```
## Root cause
`ConflictSet.cpp` has three ARM NEON blocks that load only 4 bytes per version:
- `scan16` with indices (used by `Node48`): lines 2109–2131
```cpp
uint32x4_t w4[4];
memcpy(w4, vs, sizeof(w4)); // only 64 bytes; wrong when InternalVersionT is 8 bytes
uint32_t rv;
memcpy(&rv, &readVersion, sizeof(rv)); // only low 32 bits
```
- `scan16` without indices (used by `Node256`): lines 2176–2200
- same `uint32_t rv` truncation.
- `checkMaxBetweenExclusiveImpl(Node16)`: lines 2324–2345
- same truncation when comparing `childMaxVersion` against `readVersion`.
With `USE_64_BIT=1`, `InternalVersionT` stores an `int64_t`, so 8 bytes per element. The helpers copy at most 4 bytes per element and compare only the low 32 bits, which is incorrect for versions above `2^32`.
## Verification
Building the same configuration with the SIMD fallback makes the failures disappear:
```sh
cmake -B build-64-fallback -S . -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-DUSE_64_BIT=1 -DUSE_SIMD_FALLBACK=ON"
cmake --build build-64-fallback -j4
ctest --test-dir build-64-fallback --output-on-failure -E valgrind
```
All non-valgrind tests pass.
## Impact
Any aarch64 build using 64-bit versions (`USE_64_BIT=1`) can return spurious `Conflict` results (or miss real ones, depending on the high bits) once the version counter exceeds `2^32 - 1`. This violates the documented result semantics and breaks the existing `script_test_internal_version_zero` regression test as well as many fuzz-corpus blackbox tests.
weaselbot
was assigned by andrew2026-08-03 02:27:09 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
When
ConflictSet.cppis compiled with-DUSE_64_BIT=1on aarch64, the ARM NEON fast paths incheckMaxBetweenExclusiveImplandscan16treatInternalVersionTas a 32-bit value. They copy only the low 32 bits of each version intouint32x4_tlanes and compare them against a 32-bitreadVersion. This yields wrong conflict/Commit decisions once any version exceeds2^32 - 1.This is a correctness bug in the aarch64 SIMD code, distinct from #68 (which is about reading uninitialized child slots).
Reproduction
On an aarch64 host with clang:
Result:
Many corpus blackbox tests also abort, e.g.:
Output:
Root cause
ConflictSet.cpphas three ARM NEON blocks that load only 4 bytes per version:scan16with indices (used byNode48): lines 2109–2131scan16without indices (used byNode256): lines 2176–2200uint32_t rvtruncation.checkMaxBetweenExclusiveImpl(Node16): lines 2324–2345childMaxVersionagainstreadVersion.With
USE_64_BIT=1,InternalVersionTstores anint64_t, so 8 bytes per element. The helpers copy at most 4 bytes per element and compare only the low 32 bits, which is incorrect for versions above2^32.Verification
Building the same configuration with the SIMD fallback makes the failures disappear:
All non-valgrind tests pass.
Impact
Any aarch64 build using 64-bit versions (
USE_64_BIT=1) can return spuriousConflictresults (or miss real ones, depending on the high bits) once the version counter exceeds2^32 - 1. This violates the documented result semantics and breaks the existingscript_test_internal_version_zeroregression test as well as many fuzz-corpus blackbox tests.