Skip to content

Race condition in ctx_t::send_command() causes UAF during concurrent socket destruction #4926

Description

@surprized

This is a race condition in ctx_t::send_command() where it dereferences _slots[tid_] while destroy_socket() concurrently nulls and frees that slot under _slot_sync which results in a UAF (caught by ASan), or a SIGSEGV in signaler_t::send() on non-instrumented builds of libzmq users.

Steps to trigger:
While I cannot provide a reproducible PoC (the issue was caught using an internal build on proprietary software), this can be triggered by rapidly creating and destroying sockets across threads while IO threads route term-ack commands under heavy load.

Root Cause & Technical Details

The race seems observable from any code path that calls ctx_t::send_command() without already holding _slot_sync, most critically the internal termination acknowledgement protocol running entirely on IO threads:

IO thread:

check_term_acks()
  send_term_ack(_owner)
    object_t::send_command()
      ctx_t::send_command(_slots[tid_]->send())  <-- no lock held

Reaper thread (concurrently):

socket_base_t::check_destroy()
  destroy_socket()                <-- _slots[tid] = NULL, mailbox freed
  own_t::process_destroy()        <-- delete this

ASan Report

<process name>.1594
=================================================================
==1594==ERROR: AddressSanitizer: heap-use-after-free on address 0xffff9720f828 at pc 0xffff9e89ff34 bp 0xffff78ce9e00 sp 0xffff78ce9e18
READ of size 4 at 0xffff9720f828 thread T15 (ZMQbg/IO/0)
    #0 0xffff9e89ff30 in zmq::signaler_t::send() src/signaler.cpp:195
    #1 0xffff9e7d5860 in zmq::mailbox_t::send(zmq::command_t const&) src/mailbox.cpp:73
    #2 0xffff9e7443d0 in zmq::ctx_t::send_command(unsigned int, zmq::command_t const&) src/ctx.cpp:491
    #3 0xffff9e80bf28 in zmq::object_t::send_command(zmq::command_t&) src/object.cpp:551
    #4 0xffff9e80d860 in zmq::object_t::send_term_ack(zmq::own_t*) src/object.cpp:396
    #5 0xffff9e81c630 in zmq::own_t::check_term_acks() src/own.cpp:202
    #6 0xffff9e81d694 in zmq::own_t::process_term(int) src/own.cpp:170
    #7 0xffff9e894e20 in zmq::session_base_t::pipe_terminated(zmq::pipe_t*) src/session_base.cpp:283
    #8 0xffff9e8281f0 in zmq::pipe_t::process_pipe_term_ack() src/pipe.cpp:372
    #9 0xffff9e80a0d4 in zmq::object_t::process_command(zmq::command_t&) src/object.cpp:140
    #10 0xffff9e7c1eb8 in zmq::io_thread_t::in_event() src/io_thread.cpp:101
    #11 0xffff9e7b5424 in zmq::epoll_t::loop() src/epoll.cpp:253
    #12 0xffff9e846420 in zmq::worker_poller_base_t::worker_routine(void*) src/poller_base.cpp:139
    #13 0xffff9e9214f8 in thread_routine src/thread.cpp:225
    #14 0xffffa090984c  (/lib/libpthread.so.0+0x884c)
    #15 0xffff9fa9ef68  (/lib/libc.so.6+0xcdf68)

0xffff9720f828 is located 104 bytes inside of 176-byte region [0xffff9720f7c0,0xffff9720f870)
freed by thread T14 (ZMQbg/Reaper) here:
    #0 0xffffa31e67b8 in operator delete(void*, unsigned long) (/usr/lib/libasan.so.5+0xe77b8)
    #1 0xffff9e7d5518 in zmq::mailbox_t::~mailbox_t() src/mailbox.cpp:56
    #2 0xffff9e8b3168 in zmq::socket_base_t::~socket_base_t() src/socket_base.cpp:282
    #3 0xffff9e79c3ec in zmq::dealer_t::~dealer_t() src/dealer.cpp:43
    #4 0xffff9e86e30c in zmq::req_t::~req_t() src/req.cpp:51
    #5 0xffff9e86e454 in zmq::req_t::~req_t() src/req.cpp:53
    #6 0xffff9e81ad54 in zmq::own_t::process_destroy() src/own.cpp:211
    #7 0xffff9e8bfcd0 in zmq::socket_base_t::check_destroy() src/socket_base.cpp:1591
    #8 0xffff9e8c16bc in zmq::socket_base_t::in_event() src/socket_base.cpp:1564
    #9 0xffff9e7b5424 in zmq::epoll_t::loop() src/epoll.cpp:253
    #10 0xffff9e846420 in zmq::worker_poller_base_t::worker_routine(void*) src/poller_base.cpp:139
    #11 0xffff9e9214f8 in thread_routine src/thread.cpp:225
    #12 0xffffa090984c  (/lib/libpthread.so.0+0x884c)
    #13 0xffff9fa9ef68  (/lib/libc.so.6+0xcdf68)

previously allocated by thread T0 here:
    #0 0xffffa31e58a0 in operator new(unsigned long, std::nothrow_t const&) (/usr/lib/libasan.so.5+0xe68a0)
    #1 0xffff9e8b2354 in zmq::socket_base_t::socket_base_t(zmq::ctx_t*, unsigned int, int, bool) src/socket_base.cpp:256
    #2 0xffff9e79d018 in zmq::dealer_t::dealer_t(zmq::ctx_t*, unsigned int, int) src/dealer.cpp:38
    #3 0xffff9e86e7cc in zmq::req_t::req_t(zmq::ctx_t*, unsigned int, int) src/req.cpp:46
    #4 0xffff9e8a263c in zmq::socket_base_t::create(int, zmq::ctx_t*, unsigned int, int) src/socket_base.cpp:164
    #5 0xffff9e748e50 in zmq::ctx_t::create_socket(int) src/ctx.cpp:384
    #6 0xffff9e96f144 in zmq_socket src/zmq.cpp:242
    <redacted trace>

Thread T15 (ZMQbg/IO/0) created by T13 here:
    #0 0xffffa313af28 in __interceptor_pthread_create (/usr/lib/libasan.so.5+0x3bf28)
    #1 0xffff9e920454 in zmq::thread_t::start(void (*)(void*), void*, char const*) src/thread.cpp:236
    #2 0xffff9e742238 in zmq::thread_ctx_t::start_thread(zmq::thread_t&, void (*)(void*), void*, char const*) const src/ctx.cpp:437
    #3 0xffff9e8468e4 in zmq::worker_poller_base_t::start(char const*) src/poller_base.cpp:127
    #4 0xffff9e7bfefc in zmq::io_thread_t::start() src/io_thread.cpp:67
    #5 0xffff9e747e10 in zmq::ctx_t::start() src/ctx.cpp:333
    #6 0xffff9e748efc in zmq::ctx_t::create_socket(int) src/ctx.cpp:360
    #7 0xffff9e96f144 in zmq_socket src/zmq.cpp:242
    <redacted trace>

Thread T13 created by T6 here:
    <redacted trace>

Thread T6 created by T0 here:
    <redacted trace>

Thread T14 (ZMQbg/Reaper) created by T13 here:
    #0 0xffffa313af28 in __interceptor_pthread_create (/usr/lib/libasan.so.5+0x3bf28)
    #1 0xffff9e920454 in zmq::thread_t::start(void (*)(void*), void*, char const*) src/thread.cpp:236
    #2 0xffff9e742238 in zmq::thread_ctx_t::start_thread(zmq::thread_t&, void (*)(void*), void*, char const*) const src/ctx.cpp:437
    #3 0xffff9e8468e4 in zmq::worker_poller_base_t::start(char const*) src/poller_base.cpp:127
    #4 0xffff9e86a6dc in zmq::reaper_t::start() src/reaper.cpp:74
    #5 0xffff9e747980 in zmq::ctx_t::start() src/ctx.cpp:315
    #6 0xffff9e748efc in zmq::ctx_t::create_socket(int) src/ctx.cpp:360
    #7 0xffff9e96f144 in zmq_socket src/zmq.cpp:242
    <redacted trace>

SUMMARY: AddressSanitizer: heap-use-after-free src/signaler.cpp:195 in zmq::signaler_t::send()
Shadow bytes around the buggy address:
  0x200ff2e41eb0: fd fd fd fd fa fa fa fa fa fa fa fa fd fd fd fd
  0x200ff2e41ec0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x200ff2e41ed0: fd fd fa fa fa fa fa fa fa fa 00 00 00 00 00 00
  0x200ff2e41ee0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x200ff2e41ef0: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x200ff2e41f00: fd fd fd fd fd[fd]fd fd fd fd fd fd fd fd fa fa
  0x200ff2e41f10: fa fa fa fa fa fa fd fd fd fd fd fd fd fd fd fd
  0x200ff2e41f20: fd fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa
  0x200ff2e41f30: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
  0x200ff2e41f40: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa
  0x200ff2e41f50: fa fa fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc 

A security advisory was issued but the developers state this is a not a security concern, therefore, posting here to keep track of the core issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions