Run the test suite under ASan+UBSan, and fix the clean that hid it - #801
Run the test suite under ASan+UBSan, and fix the clean that hid it#801ZacharyZcR wants to merge 1 commit into
Conversation
|
Holding this one, and it needs a decision from you rather than a rebase from me. Why it is held: your description opens with Why it now conflicts: #165 (DeepSeek V4) landed on # this PR
TEST_GLOBS = ["tests/test_*", "tests/bench_*", "tests/fuzz_*"]
KEEP_EXT = (".c", ".h", ".cc", ".cpp", ".cu", ".mm", ".py", ...) # allowlist of what to KEEP
# dev, after #165
ARTIFACT_GLOBS = ["tests/test_*", "COLI_V4_UNIT_*.o"]
BINARY_EXTENSIONS = {"", ".exe", ".o"} # allowlist of what to REMOVEPicking either side loses something real: yours drops the For what it is worth, the synthesis looks clean: keep your structure and add Separately, on the finding itself: the sanitizer story here is the part I would not want lost. That your first ASan run came back clean because One more thing you should know: #804 and #806 also opened with |
`make clean` removed nothing on Linux and macOS. tools/clean.py globbed
"tests/test_*.exe" -- its own comment says it means "no extension on Unix",
but only the Windows half was implemented, and the engine binaries
(colibri, inkling, kimi_k3) were never in the list at all. `make clean` on
this machine reported "removed 0 files/dirs" with 43 artifacts sitting
there.
That is not untidiness, it silently invalidates verification. Change a
compile flag, run `make clean && make test-c`, and the STALE binaries built
with the old flags are re-run and reported as passing. CONTRIBUTING's
`make check` opens with exactly that sequence.
It is also how this commit nearly shipped as a lie: the first sanitizer run
came back clean, and it was clean because clean.py had left the previous,
unsanitized binaries in place and make had nothing to rebuild. `nm | grep
asan` on the test binary was empty.
So, in order:
tools/clean.py removes Unix test binaries and the four engines. KEEP_EXT
is the safety rail -- a file with a source extension can never match, and
directories (tests/fixtures/) are skipped -- because the failure mode of
getting this wrong is deleting tracked sources.
EXTRA_CFLAGS/EXTRA_LDFLAGS are appended to every platform's flags. The
obvious way to build with sanitizers, `make CFLAGS=...`, REPLACES the
platform's flags and quietly drops its OpenMP and -march settings, which
is a second way to end up with a run that is not what it claims.
`make -C c test-asan` cleans, rebuilds the suite with
-fsanitize=address,undefined -O1 -g, and runs it. Leak detection is off:
the engines allocate the tensor index, parsed config and weight slabs once
and use them until exit -- st.h says so itself ("intentionally leaked ...
one-time startup parsing"). Reporting those 9 sites would bury a
heap-buffer-overflow under noise nobody intends to fix. UBSan halts on
first error rather than scrolling past it.
CI gets a `sanitizers` job running that plus `make fuzz-rans`. The fuzzer
has always been built with ASan+UBSan and asserted byte identity across
every compiled decode arm -- it simply had no job to run in, so nothing
ever ran it.
Result: the suite is clean under ASan+UBSan today, so this lands green and
guards what comes next.
Verified both directions, since a sanitizer job that cannot fail is worse
than none. With a deliberate 4-int buffer written at index 9 in
tests/test_topp.c:
make test-asan -> FAILED: tests/test_topp
make test-c -> passes, silently
Normal builds are unaffected: four engines warning-free, make test-c green.
0aa29ee to
ca13cce
Compare
|
Answering the revert question first, because you should not have to hold that open. #804 and #806 are finished. Please do not revert either.Both were complete when I opened them, and I stand behind them as merged. The convention is yours and I will follow it: GitHub's draft flag is the only signal, and I will stop writing clean.py β taking your synthesis, and it collapses further than you suggestedYou were right that the two sides are different designs, and right about which way it resolves. Pushed, rebased on current ARTIFACT_GLOBS = ["tests/test_*", "tests/bench_*", "tests/fuzz_*",
"COLI_V4_UNIT_*.o"]
KEEP_EXT = (".c", ".h", ".cc", ".cpp", ".cu", ".mm", ".py", ".txt", ".json",
".md", ".bin", ".sh", ".toml", ".yml", ".yaml")Your read of the rail was exactly right: Verified rather than asserted: 83 source files under Verifying it turned up something, and it is mineRunning the fixed
This is a blocker for this PR and I have filed it separately as #835 (untrack the binary, close the It is worth noting what happened here, since it is the same argument as the PR: the reason nobody caught the committed binary is that On the finding itselfThank you for saying the sanitizer argument is the part worth keeping. It is the part I care about: the first ASan run came back clean because |
c/tests/bench_omp_grain is a 16,880-byte ELF executable, mode 100755, added by 00129d2 (#808). It is the only tracked build artifact under c/tests/ and it is mine. .gitignore already guards c/tests/test_* with source-file exceptions, but the pattern was never extended to bench_* or fuzz_*, which build to extensionless binaries in the same directory. That gap is how it got in, so the fix is the gap rather than the one file: same shape, same exceptions, for both prefixes. This also matters for #801. That PR fixes tools/clean.py, which today globs only tests/test_*.exe and therefore deletes nothing on Unix; once it correctly removes extensionless test binaries, `make clean` would delete a tracked file and leave every contributor with a dirty tree. Removing the binary is what keeps that fix harmless.
|
The blocker on this one is gone β you merged it yourself. #835 landed in That was the only reason this PR was unsafe to merge. With the binary untracked, the fixed Current state: rebased on Verified rather than asserted: 83 source files under No rush from my side β just did not want it sitting on a condition that no longer holds. |
make cleanremoves nothing on Linux and macOStools/clean.pyglobs"tests/test_*.exe". Its own comment says it means "(.exe on Windows, no extension on Unix)" β only the Windows half was ever implemented. The engine binaries (colibri,inkling,kimi_k3) are not in the removal list at all either; it still listsglm, the pre-rename name.This silently invalidates verification. Change a compile flag, run
make clean && make test-c, and the stale binaries built with the old flags are re-run and reported as passing. CONTRIBUTING'smake checkopens with exactly that sequence.It is also how this PR nearly shipped as a lie. My first sanitizer run came back clean β because
clean.pyhad left the previous, unsanitized binaries in place andmakehad nothing to rebuild.nm tests/test_topp | grep asanwas empty. The "zero findings" was the old binaries passing again.Nothing in this repo has ever been sanitized in CI
fuzz-ransbuilds with-fsanitize=address,undefinedand has since it was written β but it is not wired to any job, and it coversrans.halone. The engine's 9k lines of pointer arithmetic, itsmmap/preadpaths and its worker threads have never been run under ASan here.What this adds
tools/clean.pyremoves Unix test binaries and the four engines.KEEP_EXTis the safety rail β a file with a source extension can never match, directories (tests/fixtures/) are skipped β because the failure mode of getting this wrong is deleting tracked sources. Rebased ondevafter Add DeepSeek V4 target-only CPU inferenceΒ #165:ARTIFACT_GLOBSnow carriesCOLI_V4_UNIT_*.oalongside the test globs, andBINARY_EXTENSIONSis gone β.ois absent fromKEEP_EXT, so the existing rail already permits removing it. One allowlist, not two facing opposite directions.EXTRA_CFLAGS/EXTRA_LDFLAGS, appended to every platform's flags. The obvious way to build with sanitizers,make CFLAGS=..., replaces the platform's flags and quietly drops its OpenMP and-marchsettings β a second way to end up with a run that is not what it claims to be. This is also the supported way to pass any one-off flag:make colibri EXTRA_CFLAGS=-DFOO.make -C c test-asanβ clean, rebuild the suite with-fsanitize=address,undefined -O1 -g, run it.sanitizersjob β that, plusmake fuzz-rans.On leak detection being off
It is off deliberately, and this is the one judgement call worth challenging.
The engines allocate the tensor index, the parsed config and the weight slabs once and use them until exit.
st.hstates the intent directly: "intentionally leaked ... one-time startup parsing". LeakSanitizer reports 9 such sites (json_parse,st_init,load_cfg,qalloc). Reporting them would bury the findings that matter β a heap-buffer-overflow, a use-after-free β under noise nobody intends to fix, and a noisy gate gets ignored.ASan's memory-error checks and all of UBSan stay on. Those are what catch defects. Flip
ASAN_OPTIONSin thetest-asantarget if you want the leak reports.Verification, both directions
A sanitizer job that cannot fail is worse than no job, so I checked it can. With a deliberate 4-int buffer written at index 9 in
tests/test_topp.c:make test-asanFAILED: tests/test_toppmake test-cAnd with the tree as-is:
make test-asanβ suite clean under ASan + UBSan (binaries confirmed linked against libasan, not assumed)make fuzz-ransβparsed_ok=1334 refused=3573 decoded_ok=2662 decode_refused=1331make cleanβ 43 artifacts removed, 0 tracked sources touched,tests/fixtures/intactmake test-cgreenCost
The sanitizer job is a full rebuild at
-O1plus a sanitized run of the suite β a few minutes, on its own job so it does not hold up the fast feedback.Not covered
GPU paths (CUDA/Metal/Vulkan) are compile-checked only; sanitizing them needs real hardware, which CI does not have.
test-pythonis not run under sanitizers here β it drives the engine binaries, so it would work, but it is a much longer run and belongs in a separate decision about CI budget.