Skip to content

Data races in connection lifecycle code (found via TSAN while validating PR #459) #460

Description

@p-alik

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions