Summary
While validating PR #459 under ThreadSanitizer (building with -fsanitize=thread and stress-testing a --threads=4 gearmand instance), TSAN reported a cluster of data races in the connection lifecycle code. These are not related to PR #459's changes — they reproduce independent of the epoch-job feature, purely from concurrent client connections and normal job traffic across the proc/IO/main threads. Filing separately since they're pre-existing.
How this was found
- Built gearmand with
CXXFLAGS='-fsanitize=thread -g -O1' LDFLAGS='-fsanitize=thread', --threads=4.
- Drove load with a client hammering the server from 16 threads, ~5k submissions per run, over several runs (~50k total).
- No worker was needed to trigger these — they show up purely from connection setup/teardown and packet queuing across the proc thread, IO threads, and the main thread.
Representative races
1. con->is_dead read/written with no lock at all
Write of size 1 at ... by thread T4 (IO thread):
#0 gearman_server_con_attempt_free connection.cc:188
con->is_dead= true;
#1 gearmand_con_free gearmand_con.cc:721
Previous read of size 1 at ... by thread T3 (proc thread):
#0 _proc gearmand_con.cc:515
if (con->is_dead)
libgearman-server/connection.cc:188 sets con->is_dead from an IO thread with no lock; libgearman-server/gearmand_con.cc:515 reads it from the proc thread, also with no lock.
2. con->io_packet_list FIFO: writer locks, reader doesn't
Write of size 8 at ... by thread T3 (proc thread, mutexes: write M0 = con->thread->lock):
#0 gearman_server_io_packet_add packet.cc:193
GEARMAND_FIFO__ADD(con->io_packet, server_packet);
Previous read of size 8 at ... by thread T5 (IO thread, no mutex held):
#0 _thread_packet_flush thread.cc:350
while (con->io_packet_list)
gearman_server_io_packet_add() (libgearman-server/packet.cc:193) correctly takes con->thread->lock before touching the FIFO, but _thread_packet_flush() (libgearman-server/thread.cc:350) reads con->io_packet_list without taking that same lock.
3. thread->to_be_freed_list unlocked fast-path check before the lock
gearman_server_con_to_be_freed_next() (libgearman-server/connection.cc:430) does:
if (thread->to_be_freed_list == NULL) // <- read outside the lock
{
return NULL;
}
int lock_error;
if ((lock_error= pthread_mutex_lock(&thread->lock)) == 0) // lock only taken after the check
while gearman_server_con_to_be_freed_add() (libgearman-server/connection.cc:403) always mutates to_be_freed_list under thread->lock. The unlocked pre-check in _next() races against the locked writer.
Other race sites observed (same session, same pattern of missing/inconsistent locking)
libgearman-server/connection.cc:189 (gearman_server_con_attempt_free)
libgearman-server/connection.cc:477 (gearman_server_con_io_add)
libgearman-server/connection.cc:559 (gearman_server_con_proc_add)
libgearman-server/connection.cc:614, :628 (gearman_server_con_proc_next)
libgearman-server/gearmand_con.cc:504 (_proc)
libgearman-server/gearmand_con.cc:633, :634, :636 (gearmand_con_create)
libgearman-server/gearmand_con.cc:761 (gearmand_con_check_queue)
libgearman-server/io.cc:592, :643 (gearman_io_send)
libgearman-server/packet.cc:238 (gearman_server_proc_packet_add)
libgearman-server/plugins/protocol/gear/protocol.cc:324, :336 (Geartext::pack)
libgearman-server/thread.cc:203 (gearman_server_thread_run)
libgearman-server/thread.cc:356 (_thread_packet_flush)
- a
memcpy race under bits/string_fortified.h:29, called from the packet/protocol path
44 individual TSAN reports total across these sites in one test session.
Scope / what's untested
- Reproduced with
--threads=4. I did not test whether these also reproduce with the default --threads=0 — worth checking, since the proc thread runs as a separate OS thread from the IO/main thread even at --threads=0, so some of these (particularly the proc-thread-vs-IO-thread ones) may not be specific to --threads>0.
- No crash was observed during these runs; these are TSAN-detected memory-ordering violations, not confirmed-reproducible corruption. Severity/exploitability would need further investigation — flagging so a maintainer familiar with the connection lifecycle code can judge which of these are real risk vs. currently-benign due to scheduling luck.
Repro
CXXFLAGS='-fsanitize=thread -g -O1' CFLAGS='-fsanitize=thread -g -O1' LDFLAGS='-fsanitize=thread' ./configure ...
make
TSAN_OPTIONS='halt_on_error=0:log_path=/tmp/tsan' ./gearmand/gearmand --threads=4 --verbose=INFO &
# then drive concurrent client connections / job submissions against it
Summary
While validating PR #459 under ThreadSanitizer (building with
-fsanitize=threadand stress-testing a--threads=4gearmand instance), TSAN reported a cluster of data races in the connection lifecycle code. These are not related to PR #459's changes — they reproduce independent of the epoch-job feature, purely from concurrent client connections and normal job traffic across the proc/IO/main threads. Filing separately since they're pre-existing.How this was found
CXXFLAGS='-fsanitize=thread -g -O1' LDFLAGS='-fsanitize=thread',--threads=4.Representative races
1.
con->is_deadread/written with no lock at alllibgearman-server/connection.cc:188setscon->is_deadfrom an IO thread with no lock;libgearman-server/gearmand_con.cc:515reads it from the proc thread, also with no lock.2.
con->io_packet_listFIFO: writer locks, reader doesn'tgearman_server_io_packet_add()(libgearman-server/packet.cc:193) correctly takescon->thread->lockbefore touching the FIFO, but_thread_packet_flush()(libgearman-server/thread.cc:350) readscon->io_packet_listwithout taking that same lock.3.
thread->to_be_freed_listunlocked fast-path check before the lockgearman_server_con_to_be_freed_next()(libgearman-server/connection.cc:430) does:while
gearman_server_con_to_be_freed_add()(libgearman-server/connection.cc:403) always mutatesto_be_freed_listunderthread->lock. The unlocked pre-check in_next()races against the locked writer.Other race sites observed (same session, same pattern of missing/inconsistent locking)
libgearman-server/connection.cc:189(gearman_server_con_attempt_free)libgearman-server/connection.cc:477(gearman_server_con_io_add)libgearman-server/connection.cc:559(gearman_server_con_proc_add)libgearman-server/connection.cc:614,:628(gearman_server_con_proc_next)libgearman-server/gearmand_con.cc:504(_proc)libgearman-server/gearmand_con.cc:633,:634,:636(gearmand_con_create)libgearman-server/gearmand_con.cc:761(gearmand_con_check_queue)libgearman-server/io.cc:592,:643(gearman_io_send)libgearman-server/packet.cc:238(gearman_server_proc_packet_add)libgearman-server/plugins/protocol/gear/protocol.cc:324,:336(Geartext::pack)libgearman-server/thread.cc:203(gearman_server_thread_run)libgearman-server/thread.cc:356(_thread_packet_flush)memcpyrace underbits/string_fortified.h:29, called from the packet/protocol path44 individual TSAN reports total across these sites in one test session.
Scope / what's untested
--threads=4. I did not test whether these also reproduce with the default--threads=0— worth checking, since the proc thread runs as a separate OS thread from the IO/main thread even at--threads=0, so some of these (particularly the proc-thread-vs-IO-thread ones) may not be specific to--threads>0.Repro