master
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c414a27e44 |
Fix CI's actual root cause: a set -e scripting bug, not a code bug
CI / build-and-test (push) Successful in 41s
The reported failure -- "Build and run under ThreadSanitizer" exiting
with code 66 -- traced back to a real bug in .gitea/workflows/ci.yml
itself, confirmed by direct reproduction, not just theorized:
1. Root cause: this step's default shell runs under `set -e`. The
previous version assigned OUT via a bare, unwrapped
`OUT=$("./binary" 2>&1)` -- under `-e`, that command's own nonzero
exit status aborts the whole script immediately, *before* the very
next line (even `RC=$?`) ever runs. Confirmed with a two-line
reproduction: `OUT=$(false); echo "after"` under `set -e` never
prints "after". This meant none of the step's careful classification
logic (distinguishing the known TSan-can't-start-here limitation from
a real finding) ever executed -- the first binary to hit the FATAL/
exit-66 case (all of them, on this runner) killed the step outright
with that raw exit code, exactly the outcome the classification logic
exists to prevent. Fixed by wrapping every such invocation in
`if cmd; then RC=0; else RC=$?; fi`, which bash's `-e` rules exempt
from triggering an abort -- verified directly under `bash -eo
pipefail` (matching Gitea Actions' actual shell), not assumed correct.
2. Fixing (1) surfaced a second bug: capturing potentially unbounded
output into a bash variable via `OUT=$(cmd)` is not just slow, it
reproduced as a several-hundred-megabyte shell string when the
DEADLYSIGNAL flake's worse, unbounded-repeating-loop form hit during
this fix's own testing -- and caused the classification logic to
misbehave at that scale (a real failure was misreported with "exit
0"). Fixed in both the ASan/UBSan and TSan steps by redirecting
output to a file and reading back only a bounded 64 KiB prefix for
classification and logging, never loading the whole thing into a
shell variable.
3. While repeatedly reproducing the TSan flake locally to verify (1) and
(2), found a second, previously undocumented variant of the same
underlying "TSan can't start on this runner" issue: instead of
printing FATAL: ThreadSanitizer: unexpected memory mapping and
exiting 66, TSan's broken startup occasionally segfaults outright.
Confirmed this is the same environmental cause, not a bug in any
specific test file, by hitting three different, unrelated binaries
(test_journal_failure in one run, test_crash_consistency and test_dir
together in another) across repeated full-suite runs -- a real bug
in one file's code would not migrate to different files at random.
The TSan step's classification now recognizes this variant too
(a log containing only timeout's own "dumped core" notice and nothing
else -- no program output, no real WARNING/SUMMARY ThreadSanitizer
race report).
4. The ASan/UBSan step's retry budget was bumped from 3 to 5 after
observing a real 3-in-a-row flake exhaustion in practice during this
same verification work -- this sandbox's actual flake rate is
meaningfully higher than the "roughly 1 in 5-10" CONTRIBUTING.md
documents, and 3 retries turned out not to be a big enough margin.
5. Separately, the -Wunused-result warning on tests/test_crash_consistency.c's
write() call: the (void) cast that silenced it locally did not silence
it on the Gitea runner's gcc -- reproduced clean locally with the
exact same compiler flags, confirming this is a real toolchain
version/config difference, not a local misconfiguration. void-cast
suppression of warn_unused_result is documented as unreliable across
gcc configurations; fixed with an actual conditional branch on the
return value instead, which every gcc/clang version used so far
honors.
Every fix here was verified by direct reproduction under `bash -eo
pipefail` locally (matching Gitea Actions' actual shell invocation), not
reasoned about and assumed correct: the original -e bug was reproduced
and fixed, the TSan step was re-run 10 full-suite times (80 individual
binary executions) with both flake variants recurring and both correctly
classified as warnings rather than errors, and the ASan/UBSan step was
re-run 10 full-suite times with the 5-retry budget with zero false
failures. Also verified with a clean make all + make test locally
(zero warnings, all 8 binaries pass).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
|
||
|
|
4aad7d6f64 |
Fix real memory leak Gitea CI caught, that local testing had been masking
CI / build-and-test (push) Failing after 38s
Gitea CI flagged two things on the last push: 1. A -Wunused-result warning on an intentionally-ignored write() return value in test_crash_consistency.c's progress side-channel. Fixed with an explicit (void) cast and a comment explaining why ignoring it is safe (a short write there only makes the progress count more conservative, per that function's own existing documented tolerance). 2. A real LeakSanitizer failure -- 256 bytes across 4 allocations from vfs_new/vfs_unmount. Root cause: tests/test_journal_failure.c's two "reopen after the failure, verify recovery" blocks called vfs_unmount/backend_free/backend_free inside their `if (ov2)` branch (the normal, expected path) but vfs_free(v2) only on the `else` branch, which is never actually reached in practice. Fixed by moving vfs_free(v2) to run unconditionally after the if, in both blocks. This bug was invisible locally across many runs because local sanitizer verification had been using ASAN_OPTIONS=detect_leaks=0 -- adopted originally for a real reason (a SIGKILLed forked child in test_crash_consistency.c never runs its own exit-time leak check, so its allocations were never the actual concern) but applied to the whole test run, which also suppressed detection of this real bug in the *parent* process's own code. CI doesn't set that option, so it caught what local runs couldn't. Documented in CONTRIBUTING.md as a real process gap, not just a code bug: a local verification habit that diverges from what CI actually runs can let a real finding through until it reaches CI. Verified: confirmed the leak directly first (reproduced locally by dropping the detect_leaks=0 override, matching CI exactly, before touching any code), then confirmed the fix by re-running the same no-override sweep across all 8 test binaries with zero leaks found, plus a clean make all + make test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB |
||
|
|
e41bd2334a |
Add real fault-injection crash-safety testing; investigate TSan for real
CI / build-and-test (push) Failing after 38s
Prompted by a direct question about data-integrity trustworthiness: the crash-safety design (self-checking journal records, atomic-rename compaction, Section 4.3/4.4) had never actually been tested against a real crash -- only reasoned about statically and tested against an already-corrupted file (test_pack_overlay.c, a different scenario). Added tests/test_crash_consistency.c: real fork()+SIGKILL fault injection against an actual child process, not a hand-truncated file standing in for a crash, with empirically-calibrated kill-delay sampling (measured via throwaway scripts, not guessed) so trials land genuine mid-operation interruptions rather than always completing first: - Journal-write crash injection (25 trials): kills a child mid-burst of individually-journaled, individually-fsynced writes; verifies a strict clean-prefix recovery (every completed write present and correct, every write after the kill cleanly absent, no gaps). 20-22/25 trials per run land a genuine interruption; zero corruption found. - Compaction crash injection (40 trials): kills a child mid-vfs_sync; verifies every write durably journaled *before* vfs_sync was called survives regardless of whether compaction itself completed. 40/40 trials per run land a genuine interruption; zero corruption found. A real finding from building this test, not from the code under test: the first draft reported 128 failures, every one a bug in the test itself -- it couldn't distinguish "the child was killed before ever attempting this write" from "the write completed and was then lost," since SIGKILL can't be caught to report progress. Fixed with an independent progress side-channel (plain write()+fsync() on a separate file, entirely outside packfs) as ground truth. Documented in CLAUDE.md's new "Known reliability characteristics" section, including this finding, because a claim is only as trustworthy as what's measuring it. Also investigated whether this sandbox's TSan block is actually unfixable, rather than re-asserting the known limitation: tried personality(ADDR_NO_RANDOMIZE) directly (EPERM), setarch -R (fails identically), searched TSAN_OPTIONS for a bypass (none exists), and checked capabilities/seccomp/unshare --user (zero effective caps, active seccomp filter, namespace escape also blocked). Confirmed categorical and documented as such in CLAUDE.md/CONTRIBUTING.md, rather than left as an unexamined "it doesn't work here." Also closed a real documentation gap SECURITY.md had: the pack integrity checksum's non-cryptographic nature (FNV-1a64, already documented in the context of the compaction-dedup bug) had never been explicitly connected to its *other* use, load-time pack integrity validation -- added, since it's a real, relevant caveat for anyone relying on that checksum against a deliberate adversary rather than accidental corruption. Verified: clean make all + make test (all 7 binaries), full ASan/UBSan sweep of all 7 binaries with zero real findings, and the new test run standalone 4+ times (this session) with stable, consistent results. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB |