Hi,

Given the recent discussions regarding the calls to
non-async-signal-safe functions from signal handlers, let me add my
two pesos to the discussion, even though I don't think there is
anything security-related in this email.

A few months ago, I handled a customer report about a deadlock in Ceph
RADOS Gateway. The Ceph version is 17.2.7. In that case, I attached
gdb to the deadlocked process and applied the backtrace to all
threads. One of the threads had this backtrace:

```
Thread 594 (Thread 0x7f41ac610700 (LWP 870073) "radosgw"):
#0  0x00007f43ca050d0e in __lll_lock_wait_private () from
target:/lib64/libc.so.6
#1  0x00007f43ca0568fa in malloc () from target:/lib64/libc.so.6
#2  0x00007f43ca0df884 in backtrace_symbols () from target:/lib64/libc.so.6
#3  0x00007f43cb24485a in handle_oneshot_fatal_signal(int) () from
target:/usr/lib64/libradosgw.so.2
#4  <signal handler called>
#5  0x00007f43ca003d2b in raise () from target:/lib64/libc.so.6
#6  0x00007f43ca0053e5 in abort () from target:/lib64/libc.so.6
#7  0x00007f43ca049c87 in __libc_message () from target:/lib64/libc.so.6
#8  0x00007f43ca051d2a in malloc_printerr () from target:/lib64/libc.so.6
#9  0x00007f43ca0525f6 in unlink_chunk.isra () from target:/lib64/libc.so.6
#10 0x00007f43ca0555c0 in _int_malloc () from target:/lib64/libc.so.6
#11 0x00007f43ca0567d8 in malloc () from target:/lib64/libc.so.6
#12 0x00007f43ca28e3fc in operator new(unsigned long) () from
target:/usr/lib64/libstdc++.so.6
#13 0x00007f43cae3b42f in std::map<rgw_obj, RGWObjState,
std::less<rgw_obj>, std::allocator<std::pair<rgw_obj const,
RGWObjState> > >::operator[](rgw_obj const&) () from
target:/usr/lib64/libradosgw.so.2
#14 0x00007f43caddb5a5 in RGWObjectCtx::set_atomic(rgw_obj const&) ()
from target:/usr/lib64/libradosgw.so.2
#15 0x00007f43caf189c0 in
rgw::sal::RadosObject::set_atomic(RGWObjectCtx*) const () from
target:/usr/lib64/libradosgw.so.2
#16 0x00007f43cad8319b in
RGWDeleteMultiObj::handle_individual_object(rgw_obj_key const&,
optional_yield,
boost::asio::basic_deadline_timer<boost::posix_time::ptime,
boost::asio::time_traits<boost::posix_time::ptime>,
boost::asio::executor>*) () from target:/usr/lib64/libradosgw.so.2
#17 0x00007f43cad83b99 in void
boost::context::detail::context_entry<boost::context::detail::record<boost::context::continuation,
boost::context::basic_fixedsize_stack<boost::context::stack_traits>,
spawn::detail::spawn_helper<boost::asio::executor_binder<void (*)(),
boost::asio::strand<boost::asio::io_context::basic_executor_type<std::allocator<void>,
0u> > >, 
RGWDeleteMultiObj::execute(optional_yield)::{lambda(spawn::basic_yield_context<boost::asio::executor_binder<void
(*)(), 
boost::asio::strand<boost::asio::io_context::basic_executor_type<std::allocator<void>,
0u> > > >)#3}, 
boost::context::basic_fixedsize_stack<boost::context::stack_traits>
>::operator()()::{lambda(boost::context::continuation&&)#1}>
>(boost::context::detail::transfer_t) () from
target:/usr/lib64/libradosgw.so.2
#18 0x00007f43cb293c1f in make_fcontext () from
target:/usr/lib64/libradosgw.so.2
```

Here is my attempt to make sense of it from the bottom up.

In frame #13, something was written into a C++ std::map; this has led
to a memory allocation via malloc() in frame #11. However, the
internal logic of the glibc implementation of malloc() detected some
corruption, and there was, indeed this message in the log:

Jun 21 04:52:35 abc-osd08 radosgw[868945]: corrupted size vs. prev_size
Jun 21 04:52:36 abc-osd08 radosgw[868945]: *** Caught signal (Aborted) **
Jun 21 04:52:36 abc-osd08 radosgw[868945]:  in thread 7f41ac610700
thread_name:radosgw

After printing this message, glibc tried to safely abort the process
to prevent further damage/exploitation, see frame #6.

Ceph daemons, however, have a signal handler that catches SIGABRT and
SIGSEGV and tries to format and log a backtrace. Their systemd units
are also set to automatically restart the daemons. Without the
handler, this is what would have happened. However, within the
handler, radosgw calls backtrace_symbols(), which, in turn, calls
malloc() in frame #1, which is not async-signal-safe. So, here is a
usage bug by definition, and it converted a restartable crash into a
classical A-A deadlock because the outer malloc() call from frame #11
already holds a lock.

In Ceph public bug tracker, there is also a similar report with MDS
instead of the RADOS gateway: https://tracker.ceph.com/issues/65039

The question here is whether this is only a Ceph bug or whether
backtrace_symbols() is an unusable piece of API a-la gets() because it
is calling malloc() and is documented to do so.

The same manual page documents backtrace_symbols_fd(), but, as-is, it
is not good enough because Ceph also wants to demangle C++ symbols, to
log the result, and to post the crash information into the designated
directory.

I acknowledge that there is a todo comment in src/global/signal_handler.cc:

  // TODO: don't use an ostringstream here. It could call malloc(), which we
  // don't want inside a signal handler.
  // Also fix the backtrace code not to allocate memory.
  ClibBackTrace bt(1);
  ostringstream oss;
  bt.print(oss);
  dout_emergency(oss.str());

Apparently, the authors don't realize that the use of ostringstream is
not the only cause of possible malloc() calls.

What would be a good solution (as in: something that does not convert
crashes into deadlocks) here? I understand that, after memory
corruption, we are already in the UB territory, but is there anything
better possible than what is implemented?

-- 
Alexander Patrakov

Reply via email to