From 3a82d9091422cfc5098f7e573b9ce13de4672448 Mon Sep 17 00:00:00 2001 From: Andrew Noyes Date: Fri, 12 Jun 2026 16:35:57 -0400 Subject: [PATCH] Work around arm64 clang codegen issues breaking CI Disable preserve_none under ASan on aarch64: every clang tested (20, 21, trunk 22) miscompiles the continuation chains with that combination, crashing ~97% of fuzz corpus tests. The chains still work with the default calling convention. See #38. Mark the masked Node3 scan results defined for valgrind on aarch64: clang 21+ lowers the in-bounds tests through flags+csel, which memcheck models imprecisely, tainting bits the mask provably clears. See #39. Skip valgrind tests in the arm64 release job, since -DNVALGRIND compiles out the client requests the workaround relies on. Both issues track filing upstream bugs. --- .gitea/workflows/ci.yml | 6 +++++- ConflictSet.cpp | 30 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 74f2911..135b8bb 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -149,7 +149,11 @@ jobs: - name: Test run: | cd build - ctest --no-compress-output --test-output-size-passed 100000 --test-output-size-failed 100000 -T Test -j "$(nproc)" --timeout 90 > /dev/null + # On arm64, valgrind needs the MAKE_MEM_DEFINED client requests for + # https://git.weaselab.dev/weaselab/conflict-set/issues/39, but this + # build has -DNVALGRIND, which compiles them out. Skip valgrind + # tests here; they run annotated in the test job. + ctest --no-compress-output --test-output-size-passed 100000 --test-output-size-failed 100000 ${{ matrix.arch == 'arm64' && '-E valgrind' || '' }} -T Test -j "$(nproc)" --timeout 90 > /dev/null - name: Package run: | diff --git a/ConflictSet.cpp b/ConflictSet.cpp index 6fcfcbf..26d173d 100644 --- a/ConflictSet.cpp +++ b/ConflictSet.cpp @@ -52,6 +52,14 @@ limitations under the License. #endif #endif +#ifndef __SANITIZE_ADDRESS__ +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#define __SANITIZE_ADDRESS__ +#endif +#endif +#endif + #include using namespace weaselab; @@ -2246,6 +2254,13 @@ bool checkMaxBetweenExclusiveImpl(Node3 *n, int begin, int end, mask |= inBounds(self->index[i]) << i; } mask &= (1 << self->numChildren) - 1; +#ifdef __aarch64__ + // The bits surviving the mask above don't derive from uninitialized slots, + // but clang 21+ on aarch64 lowers inBounds through flags+csel, which + // memcheck models imprecisely, tainting bits the mask provably clears. + // https://git.weaselab.dev/weaselab/conflict-set/issues/39 + VALGRIND_MAKE_MEM_DEFINED(&mask, sizeof(mask)); +#endif if (!mask) { return true; } @@ -2257,7 +2272,13 @@ bool checkMaxBetweenExclusiveImpl(Node3 *n, int begin, int end, compared |= (self->childMaxVersion[i] > readVersion) << i; } - return !(compared & mask) && firstRangeOk; + uint32_t compared_masked = compared & mask; +#ifdef __aarch64__ + // Same imprecise csel modeling as above. + // https://git.weaselab.dev/weaselab/conflict-set/issues/39 + VALGRIND_MAKE_MEM_DEFINED(&compared_masked, sizeof(compared_masked)); +#endif + return !compared_masked && firstRangeOk; } template @@ -3048,7 +3069,12 @@ Node *firstGeqPhysical(Node *n, const TrivialSpan key) { #define MUSTTAIL #endif -#if __has_attribute(preserve_none) +// ASan + preserve_none miscompiles the continuation chains on aarch64 with +// every clang tested (20, 21, trunk 22). The chains work with the default +// calling convention, so use that under ASan on aarch64. +// https://git.weaselab.dev/weaselab/conflict-set/issues/38 +#if __has_attribute(preserve_none) && \ + !(defined(__aarch64__) && defined(__SANITIZE_ADDRESS__)) #define PRESERVE_NONE __attribute__((preserve_none)) #else #define PRESERVE_NONE