public inbox for gdb-patches@sourceware.org
 help / color / mirror / Atom feed
From: Andrew Burgess <aburgess@redhat.com>
To: gdb-patches@sourceware.org
Cc: Aaron Merey <amerey@redhat.com>,
	Andrew Burgess <aburgess@redhat.com>,
	Mark Wielaard <mark@klomp.org>
Subject: [PATCH] gdb/debuginfod: cleanup debuginfod earlier
Date: Tue, 23 May 2023 12:23:25 +0100	[thread overview]
Message-ID: <46c030dcd5fc7a1a2ef4e078a92798f18c947ace.1684840908.git.aburgess@redhat.com> (raw)

A GDB crash was discovered on Fedora GDB that was tracked back to an
issue with the way that debuginfod is cleaned up.

The bug was reported on Fedora 37, 38, and 39.  Here are the steps to
reproduce:

1. The file /etc/ssl/openssl.cnf contains the following lines:

   [provider_sect]
   default = default_sect
   ##legacy = legacy_sect
   ##
   [default_sect]
   activate = 1

   ##[legacy_sect]
   ##activate = 1

   The bug will occur when the '##' characters are removed so that the
   lines in question look like this:

   [provider_sect]
   default = default_sect
   legacy = legacy_sect

   [default_sect]
   activate = 1

   [legacy_sect]
   activate = 1

2. Clean up any existing debuginfod cache data:

   > rm -rf $HOME/.cache/debuginfod_client

3. Run GDB:

   > gdb -nx -q -iex 'set trace-commands on' \
                -iex 'set debuginfod enabled on' \
                -iex 'set confirm off' \
		-ex 'start' -ex 'quit' /bin/ls
   +set debuginfod enabled on
   +set confirm off
   Reading symbols from /bin/ls...
   Downloading separate debug info for /usr/bin/ls
   ... snip ...
   Temporary breakpoint 1, main (argc=1, argv=0x7fffffffde38) at ../src/ls.c:1646
   1646    {
   +quit

   Fatal signal: Segmentation fault
   ----- Backtrace -----
   ... snip ...

So GDB ends up crashing during exit.

What's happening is that when debuginfod is initialised
debuginfod_begin is called (this is in the debuginfod library), this
in turn sets up libcurl, which makes use of openssl.  Somewhere during
this setup process an at_exit function is registered to cleanup some
state.

Back in GDB the debuginfod_client object is managed using this code:

  /* Deleter for a debuginfod_client.  */

  struct debuginfod_client_deleter
  {
    void operator() (debuginfod_client *c)
    {
      debuginfod_end (c);
    }
  };

  using debuginfod_client_up
    = std::unique_ptr<debuginfod_client, debuginfod_client_deleter>;

And then a global debuginfod_client_up is created to hold a pointer to
the debuginfod_client object.  As a global this will be cleaned up
using the standard C++ global object destructor mechanism, which is
run after the at_exit handlers.

However, it is expected that when debuginfod_end is called the
debuginfod_client object will still be in a usable state, that is, we
don't expect the at_exit handlers to have run and started cleaning up
the library state.

To fix this issue we need to ensure that debuginfod_end is called
before the at_exit handlers have a chance to run.

I propose that we should make use of GDB's make_final_cleanup
mechanism to call debuginfod_end.  Or rather, I intend to continue
using the debuginfod_client_up class, but ensure that the global
instance of this class is set back to nullptr before GDB calls exit.
Setting debuginfod_client_up to nullptr will invoke the deleter, which
will call debuginfod_end.

For the implementation I've created a new class which holds the
debuginfod_client_up we can then pass a pointer to this class to
make_final_cleanup.  The alternative, passing a pointer to a
unique_ptr made me feel really uncomfortable -- we'd then have
multiple references to the unique_ptr, which seems like a bad idea.

There's no test associated with this patch.  I have no idea how I
might trigger this bug from within the testsuite.  If anyone has any
ideas then I'm happy to have a go at writing something.

None of the existing debuginfod tests fail after this change.

Co-Authored-By: Mark Wielaard <mark@klomp.org>
---
 gdb/debuginfod-support.c | 104 +++++++++++++++++++++++++++++++++++----
 1 file changed, 94 insertions(+), 10 deletions(-)

diff --git a/gdb/debuginfod-support.c b/gdb/debuginfod-support.c
index 5853f420a18..5dfb313f0af 100644
--- a/gdb/debuginfod-support.c
+++ b/gdb/debuginfod-support.c
@@ -180,20 +180,104 @@ progressfn (debuginfod_client *c, long cur, long total)
   return 0;
 }
 
-static debuginfod_client *
-get_debuginfod_client ()
+/* A class that manages a debuginfod_client object.  There should only be a
+   single debuginfod_client created within GDB, so we should only ever have
+   a single instance of this class.
+
+   The critical part of this class is that we register a final cleanup with
+   GDB that is responsible for ensuring the debuginfod_client object is
+   correctly cleaned up before GDB calls exit.  If we rely on the C++
+   global object destructors to clean up the debuginfod_client object then
+   we run into problems with the order in which object destructors are
+   called relative to at_exit callbacks.  See the init method below for
+   more details.  */
+
+struct debuginfod_client_manager
 {
-  static debuginfod_client_up global_client;
+  /* Return a pointer to the managed debuginfod_client object, initialising
+     it first if needed.  */
 
-  if (global_client == nullptr)
-    {
-      global_client.reset (debuginfod_begin ());
+  debuginfod_client *get ()
+  {
+    if (m_global_client == nullptr)
+      this->init ();
+    return m_global_client.get ();
+  }
 
-      if (global_client != nullptr)
-	debuginfod_set_progressfn (global_client.get (), progressfn);
-    }
+  /* There should only be a single, global, instance of this class.  The
+     managed debuginfod_client will be reset back to nullptr by GDB's
+     make_final_cleanup mechanism before the global object is destroyed,
+     which is why the assertion here makes sense.  */
+
+  ~debuginfod_client_manager ()
+  {
+    gdb_assert (m_global_client == nullptr);
+  }
 
-  return global_client.get ();
+private:
+
+  /* Callback used by GDB's final cleanup mechanism.  Cleans up the
+     debuginfod_client object being managed by ARG, which should be a
+     debuginfod_client_manager pointer.  */
+
+  static void cleanup_debuginfod_client (void *arg)
+  {
+    debuginfod_client_manager *m = (debuginfod_client_manager *) arg;
+    m->cleanup ();
+  }
+
+  /* Set the managed debuginfod_client back to nullptr.  This will ensure
+     that the debuginfod cleanup function is called.  */
+
+  void cleanup ()
+  {
+    m_global_client = nullptr;
+  }
+
+  /* Initialise the managed debuginfod_client object and register with
+     GDB's global cleanup mechanism to ensure that the object is cleaned up
+     correctly before GDB calls exit.  */
+
+  void init ()
+  {
+    gdb_assert (m_global_client == nullptr);
+
+    debuginfod_client *client = debuginfod_begin ();
+
+    if (client != nullptr)
+      {
+	/* Debuginfod (or the libraries it uses) use at_exit to cleanup
+	   some of their state.  However, it is requires that the
+	   debuginfod_client object be cleaned up before the libraries that
+	   debuginfod uses are cleaned up.
+
+	   If we rely on the C++ global object destructors to cleanup the
+	   debuginfod object then these are run after the at_exit
+	   callbacks, which is too late.
+
+	   So instead, we register with GDB's final cleanup mechanism, this
+	   is run before GDB calls exit, which is before the at_exit
+	   handlers are run, this ensures things are cleaned up in the
+	   correct order.  */
+	make_final_cleanup (cleanup_debuginfod_client, this);
+	debuginfod_set_progressfn (client, progressfn);
+	m_global_client.reset (client);
+      }
+  }
+
+  /* The managed debuginfod_client object.  */
+
+  debuginfod_client_up m_global_client;
+};
+
+/* Return a pointer to the single global debuginfod_client, initialising it
+   first if needed.  */
+
+static debuginfod_client *
+get_debuginfod_client ()
+{
+  static debuginfod_client_manager global_client_manager;
+  return global_client_manager.get ();
 }
 
 /* Check if debuginfod is enabled.  If configured to do so, ask the user

base-commit: e84060b489746d031ed1ec9e7b6b39fdf4b6cfe3
-- 
2.25.4


             reply	other threads:[~2023-05-23 11:23 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-05-23 11:23 Andrew Burgess [this message]
2023-05-23 12:31 ` Simon Marchi
2023-05-23 14:23 ` Aaron Merey
2023-05-24 14:33   ` Andrew Burgess
2023-06-08 13:33 ` [PATCHv2] " Andrew Burgess
2023-06-08 15:09   ` Mark Wielaard
2023-06-08 21:08   ` Aaron Merey
2023-06-09 14:24   ` Tom Tromey
2023-06-09 14:38   ` Andrew Burgess

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=46c030dcd5fc7a1a2ef4e078a92798f18c947ace.1684840908.git.aburgess@redhat.com \
    --to=aburgess@redhat.com \
    --cc=amerey@redhat.com \
    --cc=gdb-patches@sourceware.org \
    --cc=mark@klomp.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).