Skip to content

Run the threading rule under TSan, and both C suites under valgrind - #8

Merged
tamnd merged 2 commits into
mainfrom
leaks
Aug 22, 2026
Merged

tamnd merged 2 commits into
mainfrom
leaks

Conversation

@tamnd

@tamnd tamnd commented Aug 22, 2026

Copy link
Copy Markdown
Owner

The leaks item 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.c checks 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, so test/threads.c is the only file in the suite that starts any.

Four cases, each one a sentence from the top of zu.h run rather than read.

  • Eight threads with a connection each off one shared database, a frame each over one shared array, sixty four statements down every connection, all of them agreeing on the answer.
  • A connection moved from thread to thread with a join between every handoff, which is what a pool does and what "a connection may move between threads" is for.
  • One connection used from two threads at once, racing for real rather than through the watcher. The only two allowed answers are ZU_OK and ZU_MISUSE_CONCURRENT, nothing else came back, and the connection is correct afterwards.
  • An interrupt from another thread, which the header calls the one exception to the rule, landing while the connection is busy, stopping the statement with 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:

$ nm -D libzu.so | grep -E 'U (pthread_mutex|pthread_rwlock|pthread_cond)'
$ nm -D libzu.so | grep -E 'U (syscall|futex)'
                 U syscall@GLIBC_2.2.5

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:

WARNING: ThreadSanitizer: data race
  Write of size 8 by thread T4:   #0 free    #1 libzu.so+0x1f3e00
  Previous write of size 8 by T1: #0 malloc  #1 libzu.so+0x1f7167

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.supp drops 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:

WARNING: ThreadSanitizer: data race
  Read of size 4 by thread T2:  #0 run /tmp/probe.c:18

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_rows lets 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:

  • ctest with -DZU_CPP_WERROR=ON, 39 of 39 passed, zero warnings.
  • threads under TSan with the suppressions, 4 of 4, exit 0, no reports.
  • misuse and threads under memcheck, 15 of 15 and 4 of 4, zero errors, exit 0.
  • Both gates probed against a planted defect and both fail as they should.

No clang on this fleet, so the clang jobs are CI's to run, same as last time.

tamnd added 2 commits August 22, 2026 22:49
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
tamnd merged commit add1669 into main Aug 22, 2026
6 of 7 checks passed
@tamnd
tamnd deleted the leaks branch August 22, 2026 16:30
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant