Conversation
misuse.c checks what happens when a connection is used from two threads, which it can do from one thread because the progress watcher supplies the second. Nothing checked that the arrangement the header tells a host to use actually works, which needs real threads. Four cases. Eight threads with a connection each off one shared database, sixty four statements down each, all answering the same number. A connection moved from thread to thread with a join between every handoff, which is what a pool does. One connection in two threads at once, racing for real rather than through the watcher, where the only two allowed answers are ok and ZU_MISUSE_CONCURRENT and the connection has to be correct afterwards. And an interrupt from another thread, which the header calls the one exception to the sharing rule, landing while the connection is busy and leaving it fit to use. Its own file rather than more cases in misuse.c because the thread sanitizer and the address sanitizer cannot be linked into one binary.
The README promised ASan, UBSan, TSan and valgrind over the ABI. The first two were there and the last two were not, so this adds them. Valgrind is the straightforward half. Both C files run under memcheck, definitely and indirectly lost count as errors, and both are clean: zero errors, zero definitely lost, zero indirectly lost. Possibly lost is one 180 byte block belonging to a thread the library starts, which is what a thread-local arena looks like to a leak checker. TSan is not straightforward, and the honest version of it took longer than the job. libzu takes no pthread lock at all: nm shows no pthread_mutex, no pthread_rwlock, no pthread_cond, and one syscall, which is the futex Rust's std locks take directly. TSan learns about an uninstrumented library through the pthread calls it intercepts, so it has no edge to learn from and reports the engine as racing with itself. Eighteen reports came out of the eight-thread case. None of them means anything, and the way to know is to take the sharing away: eight threads on eight separate databases, sharing nothing whatever, still report one, a malloc on one thread against a free of the same address on another. So reports from inside libzu are suppressed, with the measurement written down in test/tsan.supp and filed against the engine as tamnd/zu#622, which asks for the instrumented build in which those reports could be read instead. What the job checks is the half this repository owns, and that it checks anything is itself checked: a race planted in the suite's own memory is still reported with the suppressions on, exactly as the leak probe was checked before believing LSan. Also adds ZU_TEST_ROWS, because the two fixtures that exist in order to be slow are slow enough already once memcheck is slowing the machine by forty. The valgrind job passes 300 and gets the same behaviour for the same reason, in a fifth of the wall clock.
tamnd
added a commit
to tamnd/zu
that referenced
this pull request
Aug 22, 2026
Sanitizer and leak jobs in CI, which is what the item asks for. ASan and UBSan were already there over the whole tree and misuse.c already ran again with leak detection on. tamnd/zu-c#8 adds the other two the README promised: a threading suite under TSan, and both C suites under valgrind with definitely and indirectly lost counted as errors. Both clean, and both gates probed against a planted defect before being believed. The TSan half is mostly suppressed and the file says why at length: libzu takes no pthread lock, so TSan has no happens-before edge to learn and reports the engine racing with itself even when eight threads share nothing at all. That is #622. What the job checks is the client's half, and that it checks anything is checked. 39 to 50 of the 90 zu-c owes. Left: idiom at 15, then reference, install and stability at 10 each.
22 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
leaksitem on the scorecard is sanitizer and leak jobs in CI, and the README has been promising four of them for a while: ASan, UBSan, TSan and valgrind, over the full ABI surface including the deliberate misuse cases. Two of the four were real. This is the other two, plus the threading suite they needed to have something to run.The threading suite
test/misuse.cchecks what happens when the sharing rule is broken, which it can do from one thread because the progress watcher supplies the second. Nothing checked that the arrangement the header actually tells a host to use works. That needs real threads, sotest/threads.cis the only file in the suite that starts any.Four cases, each one a sentence from the top of
zu.hrun rather than read.ZU_OKandZU_MISUSE_CONCURRENT, nothing else came back, and the connection is correct afterwards.ZU_INTERRUPTED, and leaving the connection fit to run the next one.All four pass. The three promises hold.
Valgrind
Both C files under memcheck, with definitely and indirectly lost counted as errors. Both clean: zero errors, zero definitely lost, zero indirectly lost. The one possibly-lost block is 180 bytes belonging to a thread the library starts, which is how a thread-local arena looks to a leak checker rather than how a leak looks, and it is excluded deliberately and said so in the job.
TSan, and why most of it is suppressed
This is the part that took the time and it is worth reading before the diff.
libzu takes no pthread lock. Not one:
TSan learns what happened before what in an uninstrumented library by intercepting the pthread calls it makes, and Rust's own locks go straight to futex without touching any of them. So there is no edge for it to learn, and any two threads that touch the same byte look like a race even when one of them plainly waited.
Eighteen reports came out of the eight-thread case on the first run. None of them is evidence, and the way to know that is to take the sharing away. Eight threads, eight separate in-memory databases, nothing shared between them at all, still reports one:
which is the allocator handing one thread a block another gave back. If two threads that share nothing can be reported as racing, two threads that share a database being reported as racing says nothing either.
So
test/tsan.suppdrops reports whose frames are inside libzu, with that measurement written into the file rather than a bare pair of lines, and the engine side is filed as tamnd/zu#622 asking for the instrumented build in which those reports could be read instead of suppressed.What is left is this repository's half: that the suite's own threading is sound and that a connection handed between threads is handed with a real edge. That it still checks anything is itself checked, the same way the leak probe was checked before LSan was believed. A race planted in the suite's own memory is still reported with the suppressions on:
exit 66. A suppression file nobody probed is worse than no suppression file.
ZU_TEST_ROWS
Two fixtures exist in order to be slow: the watcher has to fire while a statement runs and the interrupt has to land before it ends, and both get there by counting pairs over three thousand people, which is about a third of a second. Under memcheck that is not a third of a second.
zt_rowslets a run ask for fewer, clamped so it can only shrink, and the valgrind job passes 300. The behaviour is the same for the same reason: slowing the machine down by forty is another way of arriving at a statement that lasts long enough to be interrupted.Checked
On the build host, gcc 13:
ctestwith-DZU_CPP_WERROR=ON, 39 of 39 passed, zero warnings.threadsunder TSan with the suppressions, 4 of 4, exit 0, no reports.misuseandthreadsunder memcheck, 15 of 15 and 4 of 4, zero errors, exit 0.No clang on this fleet, so the clang jobs are CI's to run, same as last time.