The `-static-pie` binary relocates itself at startup: about 1.4M
`R_X86_64_RELATIVE` entries, 34 MB of `.rela.dyn`. `-z pack-relative-relocs`
describes the same relocations as DT_RELR in 254 KB, which musl's
`ldso/dlstart.c` already applies. The binary keeps its ASLR and every
pointer is still patched, so a start with a warm page cache costs the same,
but the binary is 33.7 MB smaller and a cold start reads that much less.
Same objects linked both ways (x86_64, `clickhouse local -q "SELECT 1"`):
binary 897.6 MB -> 863.9 MB; warm start 26.8 ms -> 26.8 ms (50 runs);
cold start after evicting the binary from the page cache 222 ms -> 204 ms
(8 runs, repeated with the same result); page faults 7,800 -> 7,480.
Related: https://github.com/ClickHouse/ClickHouse/pull/109239
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a static binary there is no `dlsym` for the sanitizer runtime to find
the real libc functions behind its interceptors. The compiler-rt
runtimes are built with `SANITIZER_STATIC_LIBC_INTERCEPTION` (see the
`static-musl-interception` branch of `contrib/llvm-project`): `REAL(f)`
binds to `__real_f` at link time, and the runtime defines the bare
intercepted names itself. `contrib/compiler-rt-cmake` produces a copy of
the musl archive with those names renamed to `__real_*`, and
`cmake/linux/default_libs.cmake` links that copy (`musl_intercepted`)
for every sanitizer except standalone UBSan, which has no interceptors.
The plain allocation functions are left unrenamed so that memory musl
hands out internally (`getdelim`, `strdup`, ...) is freed through the
same interposed allocator.
The `llvm-project` bump also carries the TSan `setjmp`/`longjmp` fixes
for static musl, the raw-syscall `MonotonicNanoTime`, the `posix_close`
interceptor, and `GetTls` covering musl's `struct pthread` on
`TLS_ABOVE_TP` targets; the `musl` bump exports
`__pthread_current_thread_range` for the latter.
`ThreadFuzzer` uses the internal `__pthread_mutex_*` symbols under musl
even with ASan (they are plain strong symbols there, and `dlsym` cannot
look anything up in a static binary); the `harmful` traps that collide
with musl's own definitions once the interceptors pull them in move
under `#ifndef USE_MUSL`; Rust drops `crt-static` under a sanitizer,
which rustc otherwise refuses to combine with one.
Split out of https://github.com/ClickHouse/ClickHouse/pull/109239.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Everything we build is linked into the position-independent executable, so
-fPIE is the right code model: -fPIC only adds symbol interposition (GOT
loads, TLS descriptor sequences) that an executable never uses. Instead of
putting -fPIE back on the three C++ runtime targets, the global flag is now
-fPIE, set where #125094 put it, before the default_libs include. The
objects stay relocatable, so builds with compilers that do not default to
PIE keep working.
Also drop the per-target PIC that would override it: POSITION_INDEPENDENT_CODE
on _cld2, the cuDF fragments and the GPU island targets, -Xcompiler -fPIC in
CMAKE_CUDA_FLAGS (now -Xcompiler -fPIE), -fPIC on xgboost and dmlc-core, and
the MSan "-fPIC -fpie" (now -fPIE).
-fPIC stays where it is needed or mirrors upstream: compiler-rt (builtins,
and the sanitizer and XRay runtimes through SANITIZER_COMMON_FLAGS),
glibc-compatibility with ENABLE_OPENSSL_DYNAMIC, and the shared crypto/ssl
libraries of that option, which CMake compiles as PIC by itself. musl's
rcrt1.c keeps -fPIC on master.
Measured locally (compile_commands.json, effective last PIC/PIE flag per TU):
- amd64 debug, release; aarch64 binary, release; asan, msan, tsan: every
C/C++ TU is -fPIE except clang_rt_builtins and the compiler-rt sanitizer /
XRay runtimes; with ENABLE_OPENSSL_DYNAMIC also crypto, ssl and
glibc-compatibility.
- amd64 debug clickhouse: ELF type DYN, FLAGS_1 NOW PIE; clickhouse local
queries with threads and exceptions work.
- aarch64 flatc, msan and asan llvm-min-tblgen: DYN + PIE, the sanitizer
builds run.
- With a clang wrapper defaulting to -fno-pic, llvm-min-tblgen still links
as PIE.
The GPU island (ENABLE_GPU) is not built by CI and I have no CUDA toolkit,
so the cmake/cuda.cmake and contrib/cudf-cmake changes are not built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts in the generated `.github/workflows/master.yml` and
`.github/workflows/pull_request.yml` by regenerating them with
`python3 -m ci.praktika yaml`. `contrib/llvm-project` fast-forwards to the
PR's pointer, which already contains master's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GNU `objcopy` regenerates `.symtab` and `.strtab` when they are removed with
`--remove-section` while symbols are still present, so with GNU binutils the
packaged binary kept the full symbol table next to `.gnu_debugdata`. Add
`--strip-unneeded`, which drops the symbols with both GNU and LLVM `objcopy`.
`--strip-all` is not used because `llvm-objcopy` also removes non-allocated
sections such as `.clickhouse.hash` and `.gnu_debuglink` with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The glibc binary has been a PIE since the release binary was made position
independent, but musl builds kept linking with `-static` because
`contrib/musl-cmake` only built `crt1.o`. Build musl's `rcrt1.o` instead, whose
`ldso/dlstart.c` applies the executable's own relocations at startup, and link
with `-static-pie`, so the musl binary gets ASLR too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use `CACHE INTERNAL` like the Emscripten block, so the disables also apply
when reconfiguring an existing build directory or when the options are
passed explicitly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS libc guards its time zone state with a private pthread_rwlock
(`lcl_rwlock` in Libc stdtime/FreeBSD/localtime.c), taken by strftime(_l),
wcsftime(_l), strptime(_l), localtime(_r), ctime(_r), mktime, timelocal,
time2posix, posix2time, tzset and tzsetwall. It is the psynch rwlock that
loses wakeups when its waiters are interrupted by signals (FB24027930), and
the macOS query profiler signals every registered thread. The pthread_rwlock
shim from #112267 does not reach it, because libsystem_c binds to libpthread
through the two-level namespace.
Seen in Fast test (arm_darwin) on #117253: S3 request signing
(Aws::Utils::DateTime::ToGmtString), EnabledQuota::Impl::throwQuotaExceed and
ExternalLoader::LoadingDispatcher::calculateNextUpdateTime (holding the
ExternalLoader mutex) all wait in strftime_l -> _st_tzset_basic with no holder.
Define these 16 functions in the force-loaded darwin-compatibility archive.
Each resolves the libc function with dlsym(RTLD_NEXT) and calls it under one
recursive mutex, so at most one thread of the binary is inside libc's time
zone code and none waits on its rwlock. errno is preserved across the mutex,
because a contended Darwin mutex retries an interrupted __psynch_mutexwait /
__psynch_mutexdrop and can leave EINTR behind on success.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add toolchain and ci job to build for illumos, modeled after freebsd
cross-compilation.
Note: we disable a number of features that don't currently build on
illumos. These can be reenabled in the future if desired.
Resolves#23777.
`Build (wasm64)` / `Run clickhouse local under Node.js` fails rarely with
`RuntimeError: memory access out of bounds` in a pool worker before any
output (PRs #113726 and #116898, both unrelated diffs).
V8 checks some wasm accesses explicitly against the instance's copy of the
memory size, which another thread's `memory.grow` updates on this thread only
at its next interrupt (https://issues.chromium.org/issues/529880019, not
fixed in any Node release). On arm64 Linux that covers every plain store; on
x64 it covers `memory.fill` and atomics. `clickhouse local` started at
256 MiB and grew 7-8 times to ~930 MiB during startup (every pthread mallocs
an 8 MiB stack), while new pool workers made their first allocations, so a
worker writing a just-grown chunk trapped. The CI frames are dlmalloc
`malloc` (`wasm-function[575]`) under `__cxa_thread_atexit` in the first
statement of `ThreadFromThreadPool::worker()`.
Start at 2 GiB so that startup and small workloads never grow; untouched
pages cost no RSS and growth stays enabled. The binary-builder prewarm line
mirrors the link flags, so it changes too.
Measured on x64 with Node v24.19.0 from the builder image, 2000 runs of the CI
smoke query per arm on the master CI artifact: 8 traps (CI's exact frames)
with `--wasm-enforce-bounds-checks` and 12 with default flags at 256 MiB;
0 and 0 at 2 GiB, with no grow at all. A build of this tree: 0 traps in 2000
runs per flag set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `aor` definitions are strong. Under sanitizers the interceptors stay in
effect only because every routine in the archive happens to be intercepted
and the sanitizer runtimes are `--whole-archive`d ahead of the lazy `-laor`,
so no member is ever extracted. Adding a routine or reordering the link would
silently bypass the interceptors. Sanitized builds gain nothing from AOR, so
leave these functions with the libc there.
The target is also not defined when it is not linked (sanitizers, or
`ENABLE_LLVM_LIBC_MATH` off), so the Rust symbol localization in
`contrib/corrosion-cmake` does not use it as a reference archive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`strip_rust_symbols.sh` runs `ld -r` and `llvm-objcopy`, which leave the
address-significance table with `sh_link=0`. Since the link switched to
`--icf=safe`, lld ignores such tables and warns for every link:
ld.lld: warning: rust/workspace/lib_ch_rust_prql.a(stripped.o): --icf=safe
conservatively ignores SHT_LLVM_ADDRSIG [index 6] with sh_link=0
Removing the section keeps the same conservative folding without the warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Build (arm_tidy)` was failing on a duplicate `#include <IO/VarInt.h>` in
`src/Core/tests/gtest_settings.cpp`, a file this branch does not touch. Master
carried that duplicate for a few hours - every PR built in that window failed
the same way, and so did master itself - and has since reverted it. Re-running
the job replays the same merge commit, so it needs a fresh one.
No conflicts. `cmake/ccache.cmake` drops out of the diff: master has that change
already.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splits the build-system half of #119502 out of it, so that the pull request
there is the aggregation code and its tests alone.
What is here:
- `cmake/cuda.cmake`: the include half of `ch_gpu::cudart`, which every consumer
of `ch_contrib::rmm` needs; `gpu_island_executable`, for the build-time tools
on the island; and the shared `libstdc++` the island's host code is compiled
against.
- `cmake/linux/default_libs.cmake`: where that `libstdc++` is appended, and why
the order matters for the Itanium ABI runtime.
- `cmake/ccache.cmake`: a compiler launcher for `.cu` as well.
- `contrib/cudf-cmake`, `contrib/nvcomp-cmake`, `contrib/CMakeLists.txt`: cuDF
and nvcomp as `ch_contrib::` targets.
- `contrib/cucascade`, and the `cuCollections` bump it needs.
- `base/glibc-compatibility`: the `__isoc23_strto*` aliases and
`__libc_single_threaded`, which the island needs because it compiles against
the host's headers and links against the sysroot's glibc.
- `programs/main.cpp`, `programs/keeper/keeper_main.cpp`: the `dlopen` override
lifted in a GPU build, because the CUDA runtime reaches a device no other way.
- `.gitignore`: nvcomp is unpacked into `contrib/nvcomp` rather than vendored.
`contrib/cucascade` was a gitlink with no `.gitmodules` entry, so
`git submodule update --init` could not fetch it. Added it here.
`ENABLE_GPU` stays `OFF`, so nothing in a default build changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two settings of the CUDA toolchain that only ever cost time.
`cmake/ccache.cmake` never set `CMAKE_CUDA_COMPILER_LAUNCHER` - only the C and
C++ launchers - so nothing nvcc compiles was ever cached, and every new build
directory recompiled all of it. Measured on one translation unit of cuDF,
which is what the GPU island exists to compile: 129 s cold against 0.08 s on
a hit. Over that library it is the difference between paying 11 CPU-hours once
and paying it per build directory.
Not in depend mode, unlike C and C++. That mode reads the compiler's
dependency file rather than parsing includes, CMake drives nvcc with no
dependency flags at all, and without such a file ccache does not merely lose
its edge - it stops caching. On the same unit it reports an error, takes no
hit, and the second compilation costs the full 132 s.
`CMAKE_CUDA_ARCHITECTURES` becomes `75-real`, so nvcc emits the cubin for
`sm_75` and not also the PTX for `compute_75`. PTX is what the driver JITs when
it meets a card newer than anything the binary was built for, which a build
pinned to a single architecture of an experimental engine is not asking for,
and plain `75` made CMake pass `code=[compute_75,sm_75]` - the whole
translation unit compiled twice over.
`-lineinfo` moves behind `ENABLE_GPU_KERNEL_LINEINFO`, off by default. It maps
device instructions back to source lines, which Nsight Compute needs to
attribute a kernel's time and nothing else does - it says nothing about the
host code a debugger walks, which gets its `-g` elsewhere. On cuDF it was the
entire difference between a `RelWithDebInfo` island and a `Release` one, 60393
CPU-seconds against 39658, so a build that is never opened in a profiler paid
half again for nothing.
The measurements come from a tree with `contrib/cudf-cmake/CMakeLists.txt` and
`src/GPU` wired up. Neither is in master yet, so master compiles no CUDA at
all today and none of this changes a build here until they land - which is why
it is worth having in place before they do, rather than after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The AI review and a reviewer question on the deleted block pointed out that
removing the best-effort `/proc/cpuinfo` check for the modern AArch64 profile
does not remove the host dependency of a native build: intermediate tools such
as `protoc` and `llvm-tblgen` are still compiled with the target flags and
executed on the build host, so the failure only moved from an actionable
configure-time error to a later `SIGILL`. The check does not select the target
(that is done by `NO_ARMV81_OR_HIGHER` alone) and mirrors the preflight that
master has for `X86_ARCH_LEVEL`, so it is restored verbatim from master.
`cmake/cpu_features.cmake` now differs from master only in the compatibility
profile: `-march=armv8-a+crc` and Rust `target_feature=+crc` without `-neon`,
which is the actual fix for the `ring` build failure.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sysroot changes (https://github.com/ClickHouse/sysroot, branch update-sysroots-2026-09):
- FreeBSD sysroots refreshed from 14.5-RELEASE (13.x is end-of-life); static
archives and the target OS C++ headers are no longer checked in.
- linux-riscv64 rebuilt from Debian 13 packages (glibc 2.41, kernel headers 6.12,
gcc 14) and linux-loongarch64 from Debian sid (glibc 2.43, kernel headers 7.1,
gcc 16) with a reproducible refresh script.
- linux-s390x: OpenSSL blobs, duplicate gcc tree and libgcc_eh/libgcc_s removed.
- linux-aarch64: debug info stripped from the shared objects.
- README with provenance and update instructions for every sysroot.
- linux-x86_64, linux-aarch64, linux-s390x and linux-powerpc64le glibc versions
are unchanged; they define the glibc floor of the release binaries.
Main repository:
- cmake/freebsd/toolchain-*.cmake, ci/jobs/build_toolchain.py: target triples
*-freebsd14 to match the sysroot.
- cmake/linux/toolchain-riscv64.cmake: the sysroot has the Debian layout, GCC
startup objects live under usr/, so --gcc-toolchain points there.
- contrib/liburing-cmake: glibc >= 2.43 includes <linux/openat2.h> from
<fcntl.h>, which made the fallback definition of struct open_how in compat.h
a redefinition on loongarch64. LIBURING_CONFIG_HAS_OPEN_HOW is now derived
from the presence of linux/openat2.h in the sysroot.
Verified by building programs/clickhouse for s390x, riscv64, loongarch64,
FreeBSD x86_64 and aarch64.
The AOR scalar math (exp, exp2, log, log10, atan, atan2) added in
e687278fb9 is 0.5-2.5 ULP instead of correctly rounded. x86_64 keeps
llvm-libc's correctly-rounded implementations, so the same query started
printing different last digits per architecture, which the stateless
tests caught on arm: 02224_s2_test_const_columns, 01849_geoToS2 and
01851_s2_to_geo (s2 calls atan2 internally), 04092_kql_conformance_baby_kusto
(log10(0.1) returned -0.9999999999999999 instead of -1) and
03233_dynamic_in_functions:
https://s3.amazonaws.com/clickhouse-test-reports/praktika.html?PR=109239&sha=e687278fb9fc24b2a96e90201410f3c5b7758f29&name_0=PR
Correct rounding is what makes results architecture-independent, so the
math goes back to llvm-libc everywhere; the h3 trig cost on aarch64
(~10-16% on three h3 perf queries) is the accepted price. The AOR
AdvSIMD/MTE string routines stay - byte semantics have no rounding, and
they are the larger win (memcpy +26-37%, memcmp +50-80%, strlen 2.9x).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit bf8c209ff4)
New contrib optimized-routines (upstream ARM-software/optimized-routines,
no fork needed - built as-is): on aarch64, scalar exp, exp2, log, log10,
atan, atan2 replace the llvm-libc generic implementations (excluded from
libllvmlibc there), and the AdvSIMD/MTE string routines - memcpy,
memmove, memcmp, memchr, strlen, strcmp, strchr, strnlen - replace the
mix of musl generic C, musl's vendored 2020 AOR asm, and llvm-libc weak
definitions. Both archives are linked with --whole-archive, so the
definitions win the link structurally.
Measured on Graviton4 (Neoverse-V2) against the previous static-binary
state: exp 2.1x, exp2 2.2x, atan 1.7x, atan2 1.6x, memcpy +26-37%,
memmove ~2x on small sizes, memcmp +50-80%, strlen 2.9x, memchr 3x.
This closes the gap to glibc, whose aarch64 libm and ifunc-selected
string routines derive from the same upstream. The SVE string variants
are deliberately not used: on 128-bit-SVE cores they measure up to 3x
slower than the NEON/MTE ones. AOR math scalars are 0.5-2.5 ULP instead
of llvm-libc's correct rounding, matching glibc's accuracy class.
Motivated by the h3ToGeoBoundary regression in the perf comparison:
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109239
Verified under qemu-aarch64 with a static test binary: exp/exp2/log/
log10/atan/atan2 exact on reference points; strlen/memcmp/memchr/strcmp
correct incl. the MTE variants. x86_64 builds are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e687278fb9)
Archive symbol resolution is first-definition-wins in command-line
order, and the Rust static libraries precede liblibllvmlibc.a on the
link line. Whenever symbol localization is bypassed (as happened under
ThinLTO due to the llvm-nm bitcode output format), the libm functions
Rust's compiler_builtins exports (cbrt, fmod, fma - built for baseline
x86-64) silently win over the llvm-libc ones. Loading every llvm-libc
member ahead of all objects and archives makes the resolution
structural instead of order-dependent: a later archive can never supply
an already-defined symbol. compiler_builtins keeps each function in its
own archive member, so no duplicate definitions arise.
This also covers the mem functions, previously forced per-symbol with
-u; those flags are kept as a backstop.
Confirmed by the perf report for the static-binary PR (1.5x cbrt
regression on amd):
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109239
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 2e3f2f1df6)
(cherry picked from commit 5fc022dc2e)
Under ThinLTO (enabled in CI release builds via ENABLE_THINLTO=1) the
reference archives liblibllvmlibc.a and libmusl.a contain LLVM bitcode
members, for which llvm-nm prints dashes instead of a hex address. The
defined-globals regex required a hex address, so the reference symbol
set came out empty, nothing was localized in the Rust archives, and the
cbrt from Rust compiler_builtins (baseline x86-64, musl-ported soft
implementation) shadowed the llvm-libc one in the linked binary. This
showed up as a 1.5x cbrt regression in the perf comparison of the
static-binary PR:
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109239
Local non-LTO builds were unaffected, which is why the problem only
reproduced in CI artifacts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 0b7bbbf8e0)
(cherry picked from commit 96780624ca)
Archive symbol resolution is first-definition-wins in command-line
order, and the Rust static libraries precede liblibllvmlibc.a on the
link line. Whenever symbol localization is bypassed (as happened under
ThinLTO due to the llvm-nm bitcode output format), the libm functions
Rust's compiler_builtins exports (cbrt, fmod, fma - built for baseline
x86-64) silently win over the llvm-libc ones. Loading every llvm-libc
member ahead of all objects and archives makes the resolution
structural instead of order-dependent: a later archive can never supply
an already-defined symbol. compiler_builtins keeps each function in its
own archive member, so no duplicate definitions arise.
This also covers the mem functions, previously forced per-symbol with
-u; those flags are kept as a backstop.
Confirmed by the perf report for the static-binary PR (1.5x cbrt
regression on amd):
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109239
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 2e3f2f1df6)
Under ThinLTO (enabled in CI release builds via ENABLE_THINLTO=1) the
reference archives liblibllvmlibc.a and libmusl.a contain LLVM bitcode
members, for which llvm-nm prints dashes instead of a hex address. The
defined-globals regex required a hex address, so the reference symbol
set came out empty, nothing was localized in the Rust archives, and the
cbrt from Rust compiler_builtins (baseline x86-64, musl-ported soft
implementation) shadowed the llvm-libc one in the linked binary. This
showed up as a 1.5x cbrt regression in the perf comparison of the
static-binary PR:
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109239
Local non-LTO builds were unaffected, which is why the problem only
reproduced in CI artifacts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 0b7bbbf8e0)
0599d405a0 replaced the -finstrument-functions shadow stack with an entry
call count read from the LLVM profile counters, but WITH_COVERAGE_DEPTH kept
injecting -finstrument-functions-after-inlining into the global C and C++ flags.
Every function entry and exit in the whole binary, contrib included, therefore
still calls two empty stubs in base/base/coverage.cpp that nothing consumes.
base/base/CMakeLists.txt compiles base/base with ${COVERAGE_FLAGS}, so the stubs
carry their own profile counters: no_instrument_function stops them recursing, it
does not stop them being profiled. Their entry counters are incremented once per
function entry and once per function exit program-wide, which makes them the two
hottest function counters in the process: on a SELECT count() FROM
numbers(10000000) they reach 6213755 and 6212763, 5.3x the next hottest function
counter and together 23.5% of all counter increments.
With -mllvm -runtime-counter-relocation and
the %c pattern both effective, those counters live in a MAP_SHARED mapping of the
profile pool file, so processes that run concurrently store into the same
file-backed words at function-call frequency.
The removal cannot change what per-test coverage measures: the hooks are empty,
min_depth comes from the entry counter (or from XRay where
CLICKHOUSE_XRAY_INSTRUMENT_COVERAGE is defined), and the WITH_COVERAGE_DEPTH
macro that compiles in CoverageCollection.cpp, LLVMCoverageMapping.cpp and
SYSTEM SET COVERAGE TEST comes from the CMake option, not from this flag.
Also correct the option description, the coverage.h min_depth doc and the two
build-flag lines in .claude/docs/relevant_tests.md, which all still described the
deleted shadow stack.