Skip to content

fix(build): give sanitized-build detection one spelling and a backstop - #1615

Merged
DeusData merged 1 commit into
DeusData:mainfrom
moffermann:fix/sanitized-build-detection
Aug 14, 2026
Merged

fix(build): give sanitized-build detection one spelling and a backstop#1615
DeusData merged 1 commit into
DeusData:mainfrom
moffermann:fix/sanitized-build-detection

Conversation

@moffermann

Copy link
Copy Markdown

What does this PR do?

Follow-up to #1590/#1595. Those widened the spawn-retry budget for sanitized builds and then had to
chase the define onto the TSan leg. This removes the class of failure rather than the instance.

The four spellings. Four places ask "is this binary instrumented?", each differently:

site condition
src/foundation/subprocess.c CBM_SANITIZED_BUILD + three __SANITIZE_*__
tests/test_daemon_frontend.c same four
src/foundation/compat_thread.c defined(X) && X
tests/test_cs_lsp_bench.c ASan only

The last one means TSan and MSan were measuring an instrumented parse against the native 200 ms budget.

Why the probes could not cover for the missing define. 0a163d4f concluded that compiler probes
would not have helped, because clang spells thread instrumentation __has_feature(thread_sanitizer)
rather than __SANITIZE_THREAD__. That is right about the probes we had — and it is exactly the
probe that was missing. No site consulted __has_feature.

What replaces them. src/foundation/sanitized.h answers once, as CBM_SANITIZED, from two
sources with distinct jobs:

  • CBM_SANITIZED_BUILD from the build system stays the source of truth, and is the only thing
    that can answer for UBSan and trap-UBSan — UB instrumentation leaves no macro and no
    __has_feature bit behind.
  • The clang and GCC probes are the backstop for the three sanitizers that do announce themselves, so
    a lane that forgets the define still gets correct budgets instead of native ones.

No #error when a probe fires without the define: promoting leaves the binary correct while the lane
gets fixed, and it does not break an out-of-tree make CFLAGS_EXTRA=-fsanitize=address.

Build lanes. The define now also goes on the instrumented flag sets of our own code that lacked
it: CXXFLAGS_TSAN (preprocessor.cpp is ours; CXXFLAGS_TEST already had it),
GRAMMAR_CFLAGS_TEST, GRAMMAR_CFLAGS_TSAN. Neither tree can include the header today (no -Isrc),
so this is the build system keeping its own promise, not a behaviour change. Vendored flag sets are
untouched.

Behaviour change worth naming: test_cs_lsp_bench now allows 2000 ms on the TSan and MSan lanes
instead of 200 ms. It loosens a bound that was being applied to an instrumented binary by accident.

Verification

Resolution matrix, measured rather than assumed (clang 22, -dM -E):

flags CBM_SANITIZED
native 0
-DCBM_SANITIZED_BUILD=1 1
-fsanitize=address 1
-fsanitize=thread (linux target) 1
-fsanitize=memory (linux target) 1
-fsanitize=undefined 0 — define-only, as designed

The thread row is the point: the failure this header is named after would have self-healed.

Not verified locally, needs CI: this machine has no POSIX-target compiler, so the POSIX half of
subprocess.c was compiled only on the Windows leg, and the sanitized lanes (test-tsan, ASan,
MSan, trap-UBSan) were not run. make -f Makefile.cbm test-foundation does not link here for an
unrelated pre-existing reason — tests/test_main.c references the store/cypher/... suites
unconditionally, so that target cannot link against TEST_FOUNDATION_SRCS alone.

Checklist

  • Every commit is signed off (git commit -s)
  • Tests pass locally (make -f Makefile.cbm test) — see above, no POSIX toolchain on this box
  • Lint passes — clang-format clean on every touched file
  • New behavior is covered — the macro matrix above; the change is a compile-time contract with no
    runtime surface of its own

@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

Four places ask "is this binary instrumented?" and each asked it differently.
One of them, the C# LSP bench, only recognised ASan, so TSan and MSan measured
an instrumented parse against the NATIVE 200ms budget. The other three carried
a hand-copied list of `__SANITIZE_*__` macros that nobody kept in sync.

That drift is what the TSan gap was made of. CFLAGS_TSAN never passed
SANITIZED_DEFINE, and the per-site conditions could not cover for it: they test
`__SANITIZE_THREAD__`, which is GCC's spelling. Clang — the compiler that leg
uses — announces thread instrumentation through `__has_feature(thread_sanitizer)`
only, and no site consulted it. The claim in 0a163d4 that compiler probes could
not have helped is true of the probes we had, not of the one clang actually
offers.

src/foundation/sanitized.h now answers the question once, as CBM_SANITIZED,
from two sources with distinct jobs:

  - CBM_SANITIZED_BUILD from the build system stays the source of truth, and is
    the ONLY thing that can answer for UBSan and trap-UBSan: undefined-behaviour
    instrumentation leaves no macro and no __has_feature bit to probe.
  - The clang and GCC probes are the backstop for the three sanitizers that do
    announce themselves, so a lane that forgets the define still gets correct
    budgets instead of native ones on an instrumented binary.

Deliberately no #error when a probe fires without the define: promoting leaves
the binary correct while the lane gets fixed, and it does not break an
out-of-tree `make CFLAGS_EXTRA=-fsanitize=address` that never went near
Makefile.cbm.

__has_feature is defined away where it does not exist rather than guarded with
`#elif defined(__has_feature)`. The guarded form compiles everywhere but fails
cppcheck, which walks every configuration and rejects the file with "failed to
evaluate #if condition, undefined function-like macro invocation". The
define-away idiom is what clang documents and what tests/test_mem.c already
uses; `defined(...) && __has_feature(...)` is not an option at all, since && does
not spare a preprocessor without the builtin from parsing `0 (0)`.

Verified the resolution rather than assuming it (clang 22, -dM -E):

  native                                  CBM_SANITIZED 0
  -DCBM_SANITIZED_BUILD=1                 CBM_SANITIZED 1
  -fsanitize=address                      CBM_SANITIZED 1
  -fsanitize=thread   (linux target)      CBM_SANITIZED 1
  -fsanitize=memory   (linux target)      CBM_SANITIZED 1
  -fsanitize=undefined                    CBM_SANITIZED 0   <- define-only, as designed

The third row is the one that matters: the TSan failure this header is named
after would have self-healed.

Also wired the define into the instrumented flag sets of our own code that
still lacked it — CXXFLAGS_TSAN (preprocessor.cpp is ours, and CXXFLAGS_TEST
already had it), GRAMMAR_CFLAGS_TEST and GRAMMAR_CFLAGS_TSAN. Neither tree can
include the header today (no -Isrc), so this is the build system keeping its
own promise rather than a behaviour change. Vendored flag sets are untouched:
mimalloc, sqlite3, tre, zstd, lz4 and tree-sitter read no macro of ours.

Behaviour change worth naming: test_cs_lsp_bench now allows 2000ms on the TSan
and MSan lanes instead of 200ms. It loosens a bound that was being applied to
an instrumented binary by accident; it never tightens one.

Not verified locally: this machine has no POSIX-target compiler, so the POSIX
half of subprocess.c was not compiled here. The full Windows test-runner builds
clean with -Werror and the subprocess suite is green (14 passed, 17 skipped);
clang-format clean; macro matrix as above.

Signed-off-by: Mauricio Offermann <mauricio.offermann@gocode.cl>
@moffermann
moffermann force-pushed the fix/sanitized-build-detection branch from b01e98f to c059396 Compare August 14, 2026 10:46
@moffermann

moffermann commented Aug 14, 2026

Copy link
Copy Markdown
Author

Force-pushed one amend: the first push failed the lint / lint job on cppcheck, not on a compiler.

src/foundation/sanitized.h:48:2: error: failed to evaluate #if condition,
undefined function-like macro invocation: __has_feature( ... ) [syntaxError]

The arm was nested under #elif defined(__has_feature), which every compiler handles but cppcheck does not — it walks each configuration and rejects the file rather than skipping the group. Switched to the define-away idiom (#ifndef __has_feature / #define __has_feature(x) 0), which is what clang documents and what tests/test_mem.c already does. The resolution matrix in the description is unchanged; re-measured after the amend.

@DeusData
DeusData merged commit 1beb6d8 into DeusData:main Aug 14, 2026
34 checks passed
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.

3 participants