SIGN IN SIGN UP

ci: clang-cl sanitizer/static-analysis job + Windows-ASan + cffi-ASan + blocking clang-tidy (LEO-327) (#40)

* fix(c): size sw_decode_field for the local UTF-8 worst case (LEO-327)

Surfaced by the new blocking clang-tidy gate (clang-analyzer
security.ArrayBound): the 3-bytes-per-input-byte allocation bound was only
safe because sw_cp1252_codepoint can never return above U+FFFD -- a
cross-function invariant the analyzer (rightly) refuses to assume. Size for
the 4-byte UTF-8 worst case instead so the bound is provable from the
function alone. Output bytes are unchanged (allocation size only); golden
byte-compat suites pass untouched. Vendored mirror synced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* build: teach CMake the clang-cl flag surface (LEO-327)

Detect clang-cl once (compiler-id Clang + simulate-id MSVC) as SW_CLANG_CL
and branch the flag blocks on it: warnings become /W4 /WX /guard:cf plus the
clang-cl set the coding standard asks for (-Wconversion -Wshadow -Wformat=2;
/sdl and /analyze are cl.exe-only and would trip -Wunknown-argument under
/WX), and SW_ENABLE_ASAN becomes ASan via embedded /DEFAULTLIB directives
plus UBSan in trap mode (no runtime to link; UB aborts and fails ctest).
SW_BUILD_FUZZ now rejects clang-cl explicitly -- libFuzzer is not wired for
the windows-msvc target here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: clang-cl, MSVC-ASan, cffi-ASan, and blocking clang-tidy legs (LEO-327)

Four new native-CI legs:
- native (windows, clang-cl): the coding standard's secondary compiler, VS
  bundled ClangCL toolset under Ninja in the existing Dev Shell pattern;
  plain /WX pass then ASan + UBSan-trap pass.
- native (windows, msvc): second pass under MSVC AddressSanitizer -- the
  first CI leg to run the Windows-only test_session.c under a sanitizer.
- cffi extension under ASan (ubuntu): extension built with
  -fsanitize=address, tests/test_native.py run under the preloaded runtime;
  catches FFI marshalling/lifetime bugs the pure-C runs cannot see.
- clang-tidy (blocking) in native-linux via the exact-pinned PyPI wheel
  (22.1.8) and the new .clang-tidy check set: the repo's first blocking C
  static analysis. MSVC /analyze deliberately stays non-fatal
  (analyzer-version skew, see CMakeLists.txt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* build: clang-cl passthrough for frame pointers + full UBSan runtime attempt (LEO-327)

The plain clang-cl pass came back fully green (all /W4 /WX targets, all 5
tests incl. test_session), but the sanitizer pass tripped on
-fno-omit-frame-pointer: not in clang-cl's accepted -f set, needs the
/clang: passthrough spelling. Also switch UBSan from trap mode to the full
diagnostic runtime (-fno-sanitize-recover=all) as the pre-agreed ladder
attempt -- if the ubsan runtime does not link under link.exe/lld-link this
commit gets reverted to trap mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: pin RelWithDebInfo on the Windows sanitizer configures (LEO-327)

Under clang-cl the configure resolved to a Debug configuration
(/Od /RTC1 -MDd), and clang-cl refuses -fsanitize=address with the debug
CRT. The CMakeLists RelWithDebInfo default only fires when the build type
is truly unset, and CMake's Windows platform init supplies one for
clang-cl. Pass -DCMAKE_BUILD_TYPE=RelWithDebInfo explicitly on the new
Windows sanitizer configures, matching what native-linux's ASan pass
already does for the same RTC/ASan reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: static-lib-only clang-cl sanitizer pass (LEO-327)

The RelWithDebInfo pin got the clang-cl ASan pass compiling; the link then
failed on sensorwatch.dll only -- undefined __asan_* symbols. clang-cl's
embedded ASan runtime /DEFAULTLIB directives cover executables but not a
DLL linked directly by lld-link. The sanitizer pass exists to run the tests
under ASan, and every test links sensorwatch_static, so build the pass with
SW_BUILD_SHARED=OFF: identical TU coverage, no DLL link. The shipped DLL
still gets a sanitizer build in the MSVC job's ASan pass (green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* build: explicitly link the clang-cl ASan runtime; UBSan lands as trap mode (LEO-327)

With SW_BUILD_SHARED=OFF the test executables failed the same way the DLL
did -- undefined __asan_* at every image link. clang-cl's objects carry no
usable ASan-runtime /DEFAULTLIB directives when CMake+Ninja invokes
lld-link directly, so nothing links without naming the runtime. Resolve
the /MD dynamic-runtime pair (clang_rt.asan_dynamic +
_runtime_thunk) from the compiler's resource dir, covering both the legacy
lib/windows and per-target lib layouts, and fail the configure loudly if
absent. That also lets the DLL build again, so the SHARED=OFF stopgap is
reverted, and the job now locates clang_rt.asan_dynamic-x86_64.dll for
PATH before ctest. UBSan settles on trap mode per the pre-agreed ladder:
the full ubsan_standalone runtime never linked cleanly in this
direct-lld-link setup, and trap mode needs no runtime while still failing
ctest on any UB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: describe the CI that actually runs (LEO-327)

Drops the 'clang-cl ... still aspirational' caveat from
C_CODING_STANDARDS and replaces the 'run all three in CI' static-analysis
claim with per-tool reality (clang-tidy blocking via pinned PyPI wheel;
MSVC /analyze in CI non-fatal; cppcheck optional local). Documents the
clang-cl flag set and trap-mode UBSan as wired, updates the README CI
scope + build-option notes, the ROADMAP CI row, removes C_ABI's stale
'no fuzzing harness yet' line (LEO-414 shipped it), retargets the fuzz.yml
header note now that LEO-327 landed per-PR, gitignores the new CI build
dirs, and adds the CHANGELOG entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: review round 1 -- real cache isolation + instrumentation assert for the ASan leg (LEO-327)

Addresses the round-1 findings from the claude and codex reviewers:

- python-native-asan now sets enable-cache: false explicitly. setup-uv's
  default is auto (= on for hosted runners) under the same key as the plain
  py3.13 test job, and uv's cache does not key on CFLAGS/LDFLAGS -- the
  codex reviewer reproduced a shared cache handing the leg an
  uninstrumented build. The comment that claimed no-cache is now true.
- New assert step between build and pytest: the import of the extension
  WITHOUT the preloaded runtime must fail with the ASan came-first abort.
  A green run can no longer mean silently-uninstrumented. uv run --no-sync
  on both steps so nothing can rebuild the project without the ASan flags.
  Verified locally in both directions (instrumented -> correct abort;
  plain build -> import succeeds, assert would fail the job).
- clang-cl leg throws when the ASan runtime DLL search misses, instead of
  letting ctest die with a bare 0xc0000135.
- CMakeLists intro comment no longer claims /DEFAULTLIB directives resolve
  the sanitizer runtimes (contradicted the explicit-link workaround below).
- Standards doc: the cffi-under-ASan run is a dedicated Linux job, not
  part of the Python test job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: review round 2 -- compile-time ASan symbol check + layout-validated runtime pair (LEO-327)

Addresses the codex should-fix and both Copilot comments:

- The instrumentation assert now checks the extension's dynamic symbols
  for compiler-emitted __asan_init/__asan_report_* references before the
  import-abort check. Codex reproduced the hole: -fsanitize=address in
  LDFLAGS alone links libasan and produces the identical came-first abort
  while every object stays uninstrumented, so the runtime-linkage check
  cannot catch a lost-CFLAGS build. Verified locally in all three
  directions: fully instrumented passes; the codex counterexample
  (LDFLAGS-only) and an ordinary build both fail at the symbol check.
- The clang-cl ASan runtime discovery selects exactly one layout
  (per-target lib/<triple> first, then legacy lib/windows) and requires
  the complete asan_dynamic + runtime_thunk pair, instead of a glob where
  any single match configured successfully; a half-pair or mixed-layout
  result now FATAL_ERRORs at configure. The runtime-DLL search widens to
  both layout namings and excludes the thunk.
- Standards doc grammar: 'the flag logic branches on SW_CLANG_CL'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
L
lcj-claude-coder committed
9521730bf9878567e7a4ac9e48843c8aaecc05e2
Parent: 41dcef0
Committed by GitHub <noreply@github.com> on 7/15/2026, 9:27:07 PM