[PATCHv3 5/5] gdb: disable internal b/p when a solib is unloaded

Andrew Burgess aburgess@redhat.com
Tue Dec 3 10:35:22 GMT 2024


Bug PR gdb/32079 highlights an issue where GDB will try to remove a
breakpoint for a shared library that has been unloaded.  This will
trigger an error from GDB like:

  (gdb) next
  61            dlclose (handle[dl]);
  (gdb) next
  warning: error removing breakpoint 0 at 0x7ffff78169b9
  warning: error removing breakpoint 0 at 0x7ffff7730b57
  warning: error removing breakpoint 0 at 0x7ffff7730ad3
  54        for (dl = 0; dl < 4; ++dl)
  (gdb)

What happens is that as the inferior steps over the dlclose() call GDB
notices that the library has been unloaded and calls
disable_breakpoints_in_unloaded_shlib.  However, this function only
operates on user breakpoints and tracepoints.

In the example above what is happening is that the test loads multiple
copies of libc into different linker namespsaces.  When we 'next' over
the dlclose call one of the copies of libc is unloaded.  As GDB placed
longjmp master breakpoints within the copy of libc that was just
unloaded, the warnings we see are GDB trying (and failing) to remove
these breakpoints.

I think the solution is for disable_breakpoints_in_unloaded_shlib to
handle all breakpoints, even internal ones like the longjmp master
breakpoints.

If we do this then the breakpoint will be marked as shlib_disabled and
also will be marked as not inserted.  Later when we call
breakpoint_re_set() and the longjmp breakpoints are deleted we will no
longer try to remove them.

This solution is inspired by a patch suggested in the bug report:

  https://sourceware.org/bugzilla/show_bug.cgi?id=32079#c3

There are some differences with my approach compared to the patch
suggested in the bug.  First I have no need to delete the breakpoint
inside disable_breakpoints_in_unloaded_shlib as an earlier patch in
this series arranged for breakpoint_re_set to be called when shared
libraries are removed.  Calling breakpoint_re_set will take care of
deleting the breakpoint for us.  For details see the earlier commit
titled:

  gdb: fixes for code_breakpoint::disabled_by_cond logic

Next, rather than only handling bp_longjmp and bp_longjmp_master, I
allow all breakpoints to be handled.  I also only give the warning
about disabling breakpoints for user breakpoints, I don't see the
point of warning the user about internal b/p changes.

The biggest change though is that I've added some code to detect
shared libraries that appear to be mapped multiple times.  Here's the
'info sharedlibrary' output for the binary for gdb.base/dlmopen.exp
after all the shared libraries have been loaded:

  61	      dlclose (handle[dl]);
  (gdb) info sharedlibrary
  From                To                  Syms Read   Shared Object Library
  0x00007ffff7fca000  0x00007ffff7ff03f5  Yes         /lib64/ld-linux-x86-64.so.2
  0x00007ffff7edb3d0  0x00007ffff7f4f898  Yes (*)     /lib64/libm.so.6
  0x00007ffff7d0f800  0x00007ffff7e6eacd  Yes (*)     /lib64/libc.so.6
  0x00007ffff7fb8040  0x00007ffff7fb80f9  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib-dep.so
  0x00007ffff7c003d0  0x00007ffff7c74898  Yes (*)     /lib64/libm.so.6
  0x00007ffff7a34800  0x00007ffff7b93acd  Yes (*)     /lib64/libc.so.6
  0x00007ffff7fca000  0x00007ffff7ff03f5  Yes         /lib64/ld-linux-x86-64.so.2
  0x00007ffff7ce2040  0x00007ffff7ce2116  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib.1.so
  0x00007ffff7cdd040  0x00007ffff7cdd0f9  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib-dep.so
  0x00007ffff79283d0  0x00007ffff799c898  Yes (*)     /lib64/libm.so.6
  0x00007ffff775c800  0x00007ffff78bbacd  Yes (*)     /lib64/libc.so.6
  0x00007ffff7fca000  0x00007ffff7ff03f5  Yes         /lib64/ld-linux-x86-64.so.2
  0x00007ffff7cd8040  0x00007ffff7cd8116  Yes         /tmp/build/gdb/testsuite/outputs/gdb.base/dlmopen/dlmopen-lib.2.so
  (*): Shared library is missing debugging information.
  (gdb)

The important thing to spot here is /lib64/ld-linux-x86-64.so.2.  This
library appears 3 times in the list but is at the same address in each
case.  This indicates three copies of this library mapped into three
different linker namespaces, but the linker has spotted a single
instance of the library can be shared.

When the inferior calls dlclose() GDB will still see an event for this
library being unloaded.  If we disable all the b/p within this library
on the first event then we end up leaving software b/p in the code as
the library isn't really unmapped at this point.

What we need to do is spot that the library is mapped multiple times
and only perform the b/p disable logic if this is the last instance of
the library.

With this done the issues in PR gdb/32079 are resolved.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32079

Tested-By: Hannes Domani <ssbssa@yahoo.de>
---
 gdb/breakpoint.c                   | 39 ++++++++++++---
 gdb/testsuite/gdb.base/dlmopen.exp | 76 ++++++++++++++++++++++++------
 2 files changed, 95 insertions(+), 20 deletions(-)

diff --git a/gdb/breakpoint.c b/gdb/breakpoint.c
index d0f4cbe18a9..e61da730e00 100644
--- a/gdb/breakpoint.c
+++ b/gdb/breakpoint.c
@@ -8076,17 +8076,44 @@ disable_breakpoints_in_shlibs (program_space *pspace)
 static void
 disable_breakpoints_in_unloaded_shlib (program_space *pspace, const solib &solib)
 {
+  /* A shared library can appear twice on the runtime-linkers shared
+     library list, but the shared library is actually mapped in only once.
+     This can happen when linker namespaces are used.  When one of those
+     mappings is removed this function is called.  If we then disable and
+     mark as removed all the b/p within the library then we'll end up
+     leaving software breakpoints in the code (as the library was never
+     actually unmapped).
+
+     To prevent this, we check the shared library list.  If we find some
+     other shared library with the same objfile then we can leave the
+     breakpoints alone.
+
+     If a shared library is mapped a second time, but at a different
+     address, then the shared library will be given a new objfile.  */
+  for (const struct solib &so : pspace->solibs ())
+    {
+      if (&solib == &so)
+	continue;
+
+      /* SOLIB and SO are two different solib objects, but, if they share
+	 the same objfile then there is really only a single mapped
+	 instance of the library.
+
+	 If SOLIB has no objfile then it will have no target sections (as
+	 target sections are derived from the objfile) and so the
+	 solib_contains_address_p call below will always return false, thus
+	 we don't need to worry about the case where neither SOLIB and SO
+	 have no objfile.  */
+      if (solib.objfile != nullptr && solib.objfile == so.objfile)
+	return;
+    }
+
   bool disabled_shlib_breaks = false;
 
   for (breakpoint &b : all_breakpoints ())
     {
       bool bp_modified = false;
 
-      if (b.type != bp_jit_event
-	  && !is_breakpoint (&b)
-	  && !is_tracepoint (&b))
-	continue;
-
       for (bp_location &loc : b.locations ())
 	{
 	  if (pspace != loc.pspace || loc.shlib_disabled)
@@ -8109,7 +8136,7 @@ disable_breakpoints_in_unloaded_shlib (program_space *pspace, const solib &solib
 
 	  bp_modified = true;
 
-	  if (!disabled_shlib_breaks)
+	  if (!disabled_shlib_breaks && user_breakpoint_p (&b))
 	    {
 	      target_terminal::ours_for_output ();
 	      warning (_("Temporarily disabling breakpoints "
diff --git a/gdb/testsuite/gdb.base/dlmopen.exp b/gdb/testsuite/gdb.base/dlmopen.exp
index 264c5180f3f..f0a29280d8b 100644
--- a/gdb/testsuite/gdb.base/dlmopen.exp
+++ b/gdb/testsuite/gdb.base/dlmopen.exp
@@ -144,27 +144,75 @@ gdb_breakpoint $srcfile:$bp_main
 test_dlmopen
 
 # Try the same again when attaching after dlmopen().
-require can_spawn_for_attach
+if { [can_spawn_for_attach] } {
 
-clean_restart $binfile
+    clean_restart $binfile
 
-# Start the test program.
-set test_spawn_id [spawn_wait_for_attach $binfile]
-set testpid [spawn_id_get_pid $test_spawn_id]
+    # Start the test program.
+    set test_spawn_id [spawn_wait_for_attach $binfile]
+    set testpid [spawn_id_get_pid $test_spawn_id]
 
-# Attach.
-if { ![gdb_attach $testpid] } {
-    return
+    # Attach.
+    if { ![gdb_attach $testpid] } {
+	return
+    }
+
+    with_test_prefix "attach" {
+	# Remove the pause.  We no longer need it.
+	gdb_test "print wait_for_gdb = 0" "\\\$1 = 0"
+
+	# Set the same breakpoints again.  This time, however, we do not allow the
+	# breakpoint to be pending since the library has already been loaded.
+	gdb_breakpoint $srcfile_lib:$bp_inc
+	gdb_breakpoint $srcfile:$bp_main
+
+	test_dlmopen
+    }
 }
 
-with_test_prefix "attach" {
+# Check that we can 'next' over the dlclose calls without GDB giving any
+# warnings or errors.
+with_test_prefix "next over dlclose" {
+    clean_restart $binfile
+
+    if { ![runto_main] } {
+	return
+    }
+
+    set dlclose_lineno [gdb_get_line_number "dlclose" $srcfile]
+    gdb_breakpoint $srcfile:$dlclose_lineno
+    gdb_breakpoint $srcfile:$bp_main
+
     # Remove the pause.  We no longer need it.
     gdb_test "print wait_for_gdb = 0" "\\\$1 = 0"
 
-    # Set the same breakpoints again.  This time, however, we do not allow the
-    # breakpoint to be pending since the library has already been loaded.
-    gdb_breakpoint $srcfile_lib:$bp_inc
-    gdb_breakpoint $srcfile:$bp_main
+    set loc_re [multi_line \
+		    "\[^\r\n\]+/[string_to_regexp $srcfile]:$dlclose_lineno" \
+		    "$dlclose_lineno\\s+dlclose \[^\r\n\]+"]
+
+    # This loop mirrors the loop in dlmopen.c where the inferior performs
+    # four calls to dlclose.  Here we continue to the dlclose, and then use
+    # 'next' to step over the call.  As part of the 'next' GDB will insert
+    # breakpoints to catch longjmps into the multiple copies of libc which
+    # have been loaded.  Each dlclose call will cause a copy of libc to be
+    # unloaded, which should disable the longjmp breakpoint that GDB
+    # previously inserted.
+    #
+    # At one point a bug in GDB meant that we failed to correctly disable
+    # the longjmp breakpoints and GDB would try to remove the breakpoint
+    # from a library after it had been unloaded, which would trigger a
+    # warning.
+    for { set i 0 } { $i < 4 } { incr i } {
+	gdb_continue_to_breakpoint "continue to dlclose $i" $loc_re
+	gdb_test "next" "^$decimal\\s+for \\(dl = 0;\[^\r\n\]+\\)" \
+	    "next over dlclose $i"
+    }
 
-    test_dlmopen
+    # Just to confirm that we are where we think we are, continue to the
+    # final 'return' line in main.  If this isn't hit then we likely went
+    # wrong earlier.
+    gdb_continue_to_breakpoint "continue to final return" \
+	[multi_line \
+	     "\[^\r\n\]+/[string_to_regexp $srcfile]:$bp_main" \
+	     "$bp_main\\s+return 0;\[^\r\n\]+"]
 }
-- 
2.25.4



More information about the Gdb-patches mailing list