* [RFC 0/3] Deferred, or on-demand, debuginfod downloading
@ 2026-08-14 15:06 Andrew Burgess
2026-08-14 15:06 ` [RFC 1/3] gdb: limited support for frame cache flushing while building stack Andrew Burgess
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Andrew Burgess @ 2026-08-14 15:06 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
Hi!
A while ago some patches were posted that added deferred debug info
downloading to GDB. The latest iteration of these patches can be
found here:
https://inbox.sourceware.org/gdb-patches/20250124152858.2085187-1-amerey@redhat.com
There was some great work done in the first cut of this work, however,
the series never really got much review attention, and the work ended
up on hold.
I've spent some time recently trying to move these patches forward to
a place where they might be ready to merge.
Progress seemed to be going OK, except for one issue which, right now,
seems like a truely nasty problem, my current fix for which does not
feel good. So I wanted to share the current work with the community
to see if anyone has any ideas for a better solution.
But before we get into the problems, a quick summary of what changed
since the previous version.
The biggest change is testing. The original series has a single test
which checked the standard basic use case. That test was fine, but
there were a lot of changes in the patch which were not covered by
that test. So I worked through the patch trying to ensure that every
single change was covered by a test. I cannot guarantee 100%
coverage, but I do think that most stuff is now covered by a test
case.
Doing this meant I found some small pieces which were not needed,
probably left overs from earlier iterations.
Having written all the tests it was much easier to do some minor
refactoring to move checks into better locations, which allowed some
redundancy to be removed.
But these were all pretty minor clean ups.
The core idea behind the patch remains unchanged. When we find a
objfile without debug information we ask debuginfod for the .gdb_index
section from the debug info file. If the section arrives then we
attach this to the original objfile and treat this as a debug index.
Later when we get a "hit" in this .gdb_index we download the full
debug info file, attach this new objfile to GDB, and then repeat the
lookup within the new objfile, this can then find the full debug info
entry (if possible). Finally, we remove the .gdb_index quick function
lookup from the original objfile, any future lookups will naturally be
serviced by the debug info objfile that was just added.
The original series also included a third patch that made is possible
for GDB to do an early download of .debug_line too, this could be used
for creating file/line breakpoints without the need for the full debug
info. I have not yet incorporated those changes into this series.
So, what's the problem?
The issue is when the debug info objfile is added to GDB. Adding a
new objfile triggers the Python new_objfile event listener, which can
then run user code. User code can easily trigger a frame cache flush.
This is a problem because the deferred debug info downloading almost
always happens while GDB is building the backtrace.
That is where patch #1 of this series comes from. It's an initial
attempt at allowing GDB to handle frame cache flushes while building
the backtrace. This works by wrapping a bunch of functions with a
loop. The functions all take a frame_info_ptr object. If while
executing the body of the function the frame cache is flushed then GDB
will spot this and go back around the loop. The hope is that any new
debug info will be added in the first iteration, triggering cache
flushes, then on the second iteration there will be no new debug info
added, so GDB will manage to exit the loop.
Clearly we don't want GDB looping forever, so if the frame cache does
end up being flushed a second time, triggering a third loop iteration,
then GDB will throw an error.
This approach works well enough, but something about it doesn't "feel"
right with me. For one thing, it feels like there are likely other
places in GDB where this sort of bug might reside, but we'll likely
not find them until some random point in the future where we encounter
the right combination of deferred debug download and new objfile observer.
What feedback do I want?
I'd love people to give their thoughts on the general appraoch taken,
both in patch #1, and in patch #3.
Also, do people have any ideas for better approaches?
Basically, all feedback is welcome.
Thanks,
Andrew
---
Aaron Merey (2):
gdb: use basic_safe_range to iterate over objfile::qf
gdb/debuginfod: support on-demand debuginfo downloading
Andrew Burgess (1):
gdb: limited support for frame cache flushing while building stack
gdb/ctfread.c | 4 +-
gdb/dwarf2/index-cache.c | 22 +-
gdb/dwarf2/index-cache.h | 11 +
gdb/dwarf2/public.h | 12 +
gdb/dwarf2/read-gdb-index.c | 185 +++++++++++++
gdb/dwarf2/read-gdb-index.h | 9 +
gdb/dwarf2/read.c | 185 ++++++++++++-
gdb/dwarf2/read.h | 9 +
gdb/elfread.c | 3 +-
gdb/frame.c | 176 ++++++++++---
gdb/jit.c | 4 +-
gdb/objfile-flags.h | 4 +
gdb/objfiles.h | 51 +++-
gdb/symfile-debug.c | 43 +--
gdb/symfile.c | 4 +-
.../gdb.debuginfod/deferred-auto-load-lib.c | 26 ++
.../deferred-auto-load-py-script.h | 41 +++
.../gdb.debuginfod/deferred-auto-load.c | 27 ++
.../gdb.debuginfod/deferred-auto-load.exp | 223 ++++++++++++++++
.../gdb.debuginfod/deferred-download-lib1.c | 43 +++
.../gdb.debuginfod/deferred-download-lib2.c | 37 +++
.../gdb.debuginfod/deferred-download.c | 29 ++
.../gdb.debuginfod/deferred-download.exp | 158 +++++++++++
.../gdb.debuginfod/deferred-dwz-common.h | 41 +++
.../gdb.debuginfod/deferred-dwz-lib1.c | 24 ++
.../gdb.debuginfod/deferred-dwz-lib2.c | 24 ++
gdb/testsuite/gdb.debuginfod/deferred-dwz.c | 30 +++
gdb/testsuite/gdb.debuginfod/deferred-dwz.exp | 247 ++++++++++++++++++
.../deferred-expand-warning-lib.c | 36 +++
.../gdb.debuginfod/deferred-expand-warning.c | 25 ++
.../deferred-expand-warning.exp | 177 +++++++++++++
.../gdb.debuginfod/deferred-expand-warning.py | 61 +++++
.../gdb.debuginfod/deferred-frame-cache-lib.c | 25 ++
.../gdb.debuginfod/deferred-frame-cache.c | 28 ++
.../gdb.debuginfod/deferred-frame-cache.exp | 180 +++++++++++++
.../gdb.debuginfod/deferred-frame-cache.py | 68 +++++
.../deferred-no-debuginfo-lib.c | 22 ++
.../gdb.debuginfod/deferred-no-debuginfo.c | 25 ++
.../gdb.debuginfod/deferred-no-debuginfo.exp | 194 ++++++++++++++
.../deferred-observer-symbols.exp | 161 ++++++++++++
.../deferred-observer-symbols.py | 62 +++++
.../gdb.debuginfod/deferred-pending.exp | 172 ++++++++++++
.../deferred-select-frame-lib1.c | 26 ++
.../deferred-select-frame-lib2.c | 26 ++
.../gdb.debuginfod/deferred-select-frame.c | 61 +++++
.../gdb.debuginfod/deferred-select-frame.exp | 240 +++++++++++++++++
.../gdb.debuginfod/deferred-select-frame.py | 44 ++++
.../gdb.python/py-frame-cache-flushing-lib1.c | 26 ++
.../gdb.python/py-frame-cache-flushing-lib2.c | 26 ++
.../gdb.python/py-frame-cache-flushing.c | 55 ++++
.../gdb.python/py-frame-cache-flushing.exp | 115 ++++++++
.../gdb.python/py-frame-cache-flushing.py | 52 ++++
gdb/testsuite/lib/debuginfod-support.exp | 31 ++-
gdb/testsuite/lib/gdb.exp | 8 +-
gdbsupport/traits.h | 7 +
55 files changed, 3557 insertions(+), 68 deletions(-)
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-pending.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.py
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.c
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.exp
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.py
base-commit: cef1fbf3b02ff6e5bc62e1b8a7d30c0a32b8b9f7
--
2.25.4
^ permalink raw reply [flat|nested] 4+ messages in thread
* [RFC 1/3] gdb: limited support for frame cache flushing while building stack
2026-08-14 15:06 [RFC 0/3] Deferred, or on-demand, debuginfod downloading Andrew Burgess
@ 2026-08-14 15:06 ` Andrew Burgess
2026-08-14 15:06 ` [RFC 2/3] gdb: use basic_safe_range to iterate over objfile::qf Andrew Burgess
2026-08-14 15:06 ` [RFC 3/3] gdb/debuginfod: support on-demand debuginfo downloading Andrew Burgess
2 siblings, 0 replies; 4+ messages in thread
From: Andrew Burgess @ 2026-08-14 15:06 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
This commit adds a mechanism to GDB to allow for a limit number of
frame cache flushes while GDB is building the inferior's stack.
Normally GDB takes care to ensure that the frame cache is never
flushed while building the inferior's stack, i.e. while creating new
frames. If we did allow flushing the frame cache at will during frame
creation then we would (potentially) never finish building the stack,
as each frame cache flush requires that we restart building the stack
from the start.
The problem we have, is that as we add more Python API hooks into GDB,
especially around stack unwinding, the more chance we have that a user
action is going to trigger a frame cache flush. We have already seen
at least one bug in this area PR gdb/32120 where an apparently
innocent RemoteTargetConnection.send_packet call can trigger a frame
cache flush.
In the next commit I am adding support for deferred debug information
downloading via debuginfod. With that commit we will initially
download the .gdb_index for a debug info file, and only download the
full debug info file later when GDB realises that it needs it.
The problem that this cases is that GDB might realise that it needs a
debug info file while it is building the stack. For example if frame
5 is in a shared library for which we are using deferred debug info
downloading, then GDB will now download the required debug info file
until it tries to build frame 5. Loading the deferred debug info file
will trigger the new_objfile observer, which will then load any
associated Python scripts. These Python scripts could trigger a frame
cache flush, e.g. by installing a custom stack unwinder.
The change presented here is designed specifically to handle the
deferred debug info download case. In that case we only expect to see
a frame cache flush the first time we attempt to create a frame; the
debug info is downloaded, the support scripts are loaded, and the
frame cache is flushed. If we try to create the frame again, then the
debug information is already downloaded and added to GDB so we no
longer expect to see a second debug info download (for the same
frame), and so there should be no new objfile event, and no possible
frame cache flush.
This is achieved by adding a mechanism to defer the frame cache
flushing, scoped_defer_reinit_frame_cache. While a
scoped_defer_reinit_frame_cache is active on the stack, any frame
cache flushes are postponed, this means that the frame_info objects
will not be invalidated, and frame creation can continue as if the
flush had not happened. This does mean that the generated frame could
be invalid though. When the scoped_defer_reinit_frame_cache goes out
of scope then any pending frame cache flushes are performed,
invalidating any (potentially) invalid frames that have been created.
In addition to the above a new helper function,
with_protected_frame_cache has been added. This helper wraps around
some other function call, performing the wrapped function call with a
scoped_defer_reinit_frame_cache in place. After calling the wrapped
function, if there is no frame cache flush pending then we are done.
If there is a pending frame cache flush then the wrapped function is
called a second time. Again, after this call, if there are no pending
frame cache flushes, then we are done. But, if after the second call
there is still a pending flush, then an error is thrown. This
indicates the case where (apparently) the frame cache is being flushed
every time GDB tries to build a frame. We could imagine situations
where exactly 3 attempts is needed to reach a stable situation. Or 4
attempts. Or 5. Or any number we choose. The problem is we cannot
just spin forever hoping that GDB eventually breaks out of the loop.
At some point we need to just give up. I believe debuginfod offers a
compelling case for making 2 attempts, so that's what this patch
proposes.
In order to support the new with_protected_frame_cache template
function there's a new type trait which can be used to check that
every argument in an argument pack is const. This is needed because,
if the function with_protected_frame_cache is wrapping can update its
arguments, then calling the wrapped function twice might not produce
the expected results.
---
gdb/frame.c | 124 +++++++++++++++++-
.../gdb.python/py-frame-cache-flushing-lib1.c | 26 ++++
.../gdb.python/py-frame-cache-flushing-lib2.c | 26 ++++
.../gdb.python/py-frame-cache-flushing.c | 55 ++++++++
.../gdb.python/py-frame-cache-flushing.exp | 115 ++++++++++++++++
.../gdb.python/py-frame-cache-flushing.py | 52 ++++++++
gdbsupport/traits.h | 7 +
7 files changed, 402 insertions(+), 3 deletions(-)
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.c
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.exp
create mode 100644 gdb/testsuite/gdb.python/py-frame-cache-flushing.py
diff --git a/gdb/frame.c b/gdb/frame.c
index b91e18fad99..912404cd26a 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -97,6 +97,14 @@ static frame_info_ptr selected_frame;
static frame_info_ptr sentinel_frame;
+/* When nonzero, reinit_frame_cache is deferred: frame_info objects
+ are kept alive so that mid-operation code (e.g. the DWARF unwinder
+ computing a frame ID) does not encounter freed memory. */
+static unsigned int defer_reinit_frame_cache_depth = 0;
+
+/* Set when reinit_frame_cache was requested while deferred. */
+static bool defer_reinit_frame_cache_pending = false;
+
/* See frame.h. */
unsigned int
@@ -424,6 +432,100 @@ scoped_restore_selected_frame::~scoped_restore_selected_frame ()
set_language (m_lang);
}
+/* RAII class to defer reinit_frame_cache calls. While an instance of
+ this class is alive, calls to reinit_frame_cache are deferred: the
+ frame cache is not cleared and frame_info objects remain valid.
+ When the last instance goes out of scope, a single
+ reinit_frame_cache call is made if any were deferred.
+
+ This is used during deferred debuginfo downloads to prevent the
+ frame cache from being destroyed mid-operation (e.g. while the
+ DWARF unwinder is computing a frame ID). */
+
+class scoped_defer_reinit_frame_cache
+{
+public:
+ scoped_defer_reinit_frame_cache ()
+ {
+ defer_reinit_frame_cache_depth++;
+ }
+
+ ~scoped_defer_reinit_frame_cache ()
+ {
+ gdb_assert (defer_reinit_frame_cache_depth > 0);
+ defer_reinit_frame_cache_depth--;
+
+ if (defer_reinit_frame_cache_depth == 0
+ && defer_reinit_frame_cache_pending)
+ {
+ frame_debug_printf ("performing deferred frame cache reinit");
+ defer_reinit_frame_cache_pending = false;
+ reinit_frame_cache ();
+ }
+ }
+
+ bool will_trigger_reinit () const
+ {
+ gdb_assert (defer_reinit_frame_cache_depth > 0);
+ return this->has_pending_reinit () && defer_reinit_frame_cache_depth == 1;
+ }
+
+ bool has_pending_reinit () const
+ {
+ gdb_assert (defer_reinit_frame_cache_depth > 0);
+ return defer_reinit_frame_cache_pending;
+ }
+
+ DISABLE_COPY_AND_ASSIGN (scoped_defer_reinit_frame_cache);
+};
+
+/* Call Func passing in Args while a scoped_defer_reinit_frame_cache is in
+ effect. Once Func completes, if the frame cache has been reinitialised
+ then try calling Func again. If after the second call the frame cache
+ has again been initialised then raise an error. Because Func can be
+ called multiple times, it is required that every argument in ARGS be
+ 'const'.
+
+ This can be used to wrap frame unwinding related calls where an
+ extension language hook might trigger a frame cache flush. The hope is
+ that whatever action triggers the flush will only happen the first
+ time, and that the second time through will not result in a frame cache
+ flush. */
+
+template <typename Func, typename... Args,
+ typename = gdb::Requires<gdb::all_args_are_const<Args...>>>
+static decltype(auto)
+with_protected_frame_cache (Func&& func, Args&&... args)
+{
+ FRAME_SCOPED_DEBUG_ENTER_EXIT;
+
+ using ReturnType = std::invoke_result_t<Func, Args...>;
+
+ for (int i = 0; i < 2; ++i)
+ {
+ scoped_defer_reinit_frame_cache defer_reinit_frame_cache;
+
+ if constexpr (std::is_void_v<ReturnType>)
+ {
+ std::invoke (std::forward<Func> (func),
+ std::forward<Args> (args)...);
+
+ if (!defer_reinit_frame_cache.will_trigger_reinit ())
+ return;
+ }
+ else
+ {
+ decltype(auto) result = std::invoke (std::forward<Func> (func),
+ std::forward<Args> (args)...);
+
+ if (!defer_reinit_frame_cache.will_trigger_reinit ())
+ return result;
+ }
+ }
+
+ error ("frame cache repeatedly reinitialized");
+}
+
/* Flag to control debugging. */
bool frame_debug;
@@ -1985,8 +2087,8 @@ invalidate_selected_frame ()
/* See frame.h. */
-void
-select_frame (const frame_info_ptr &fi)
+static void
+select_frame_1 (const frame_info_ptr &fi)
{
gdb_assert (fi != nullptr);
@@ -2062,6 +2164,12 @@ select_frame (const frame_info_ptr &fi)
}
}
+void
+select_frame (const frame_info_ptr &fi)
+{
+ with_protected_frame_cache (select_frame_1, fi);
+}
+
/* Create an arbitrary (i.e. address specified by user) or innermost frame.
Always returns a non-NULL value. */
@@ -2169,6 +2277,15 @@ frame_observer_target_changed (struct target_ops *target)
void
reinit_frame_cache (void)
{
+ if (defer_reinit_frame_cache_depth > 0)
+ {
+ frame_debug_printf ("mark frame cache flush as pending");
+ defer_reinit_frame_cache_pending = true;
+ return;
+ }
+
+ frame_debug_printf ("flushing the frame cache");
+
++frame_cache_generation;
if (!frame_stash.empty ())
@@ -2551,7 +2668,8 @@ get_prev_frame_always (const frame_info_ptr &this_frame)
try
{
- prev_frame = get_prev_frame_always_1 (this_frame);
+ prev_frame = with_protected_frame_cache (get_prev_frame_always_1,
+ this_frame);
}
catch (const gdb_exception_error &ex)
{
diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c
new file mode 100644
index 00000000000..b954375fe50
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib1.c
@@ -0,0 +1,26 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* ... */
+
+typedef void (*callback_t) (void);
+
+void
+library1_function (callback_t cb)
+{
+ cb ();
+}
diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c
new file mode 100644
index 00000000000..b742f2c4347
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing-lib2.c
@@ -0,0 +1,26 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* ... */
+
+typedef void (*callback_t) (void);
+
+void
+library2_function (callback_t cb)
+{
+ cb ();
+}
diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing.c b/gdb/testsuite/gdb.python/py-frame-cache-flushing.c
new file mode 100644
index 00000000000..5f169d64751
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing.c
@@ -0,0 +1,55 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* ... */
+
+typedef void (*callback_t) (void);
+extern void library1_function (callback_t cb);
+extern void library2_function (callback_t cb);
+
+volatile int global_var;
+
+void
+f0 (void)
+{
+ global_var = 42; /* Break here. */
+}
+
+void
+f1 (void)
+{
+ f0 ();
+}
+
+void
+f3 (void)
+{
+ library2_function (f1);
+}
+
+void
+f5 (void)
+{
+ library1_function (f3);
+}
+
+int
+main (void)
+{
+ f5 ();
+ return global_var - 42;
+}
diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing.exp b/gdb/testsuite/gdb.python/py-frame-cache-flushing.exp
new file mode 100644
index 00000000000..988a34e66e4
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing.exp
@@ -0,0 +1,115 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# ....
+
+load_lib gdb-python.exp
+
+standard_testfile .c -lib1.c -lib2.c
+
+# Build the first shared library.
+set lib1_testfile "lib1-${testfile}.so"
+set lib1_srcfile $srcfile2
+set lib1_binfile [standard_output_file $lib1_testfile]
+if { [build_executable "build $lib1_testfile" $lib1_testfile $lib1_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+# Build the second shared library.
+set lib2_testfile "lib2-${testfile}.so"
+set lib2_srcfile $srcfile3
+set lib2_binfile [standard_output_file $lib2_testfile]
+if { [build_executable "build $lib2_testfile" $lib2_testfile $lib2_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+if { [build_executable "build executable" $testfile $srcfile \
+ [list debug build-id shlib=$lib1_binfile \
+ shlib=$lib2_binfile]] != 0 } {
+ return
+}
+
+set remote_python_file [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+clean_restart
+
+proc test { flush_levels } {
+ clean_restart $::testfile
+
+ if {![runto_main]} {
+ return
+ }
+
+ gdb_test_no_output "source $::remote_python_file" "load python file"
+
+ set expected_failure_level -1
+ set counter 0
+ foreach lvl $flush_levels {
+ incr counter
+ with_test_prefix "setup step $counter" {
+ gdb_test_no_output "python flush_cache_at_levels\[$lvl\] \
+ = flush_cache_at_levels.get($lvl, 0) + 1" \
+ "setup step $counter, prepare for cache flush at level $lvl"
+ set times [get_python_valueof "flush_cache_at_levels\[$lvl\]" \
+ UNKNOWN "get flush count for level $lvl"]
+ if { $times > 1 && ($lvl < $expected_failure_level \
+ || $expected_failure_level == -1) } {
+ set expected_failure_level $lvl
+ }
+ }
+ }
+
+ gdb_breakpoint [gdb_get_line_number "Break here." $::srcfile]
+
+ gdb_continue_to_breakpoint "breakpoint in f0"
+
+ set full_bt_regexp \
+ [list "#0 f0 \\(\\) at \[^\r\n\]+" \
+ "#1 $::hex in f1 \\(\\) at \[^\r\n\]+" \
+ "#2 $::hex in library2_function \\(cb=$::hex <f1>\\) at \[^\r\n\]+" \
+ "#3 $::hex in f3 \\(\\) at \[^\r\n\]+" \
+ "#4 $::hex in library1_function \\(cb=$::hex <f3>\\) at \[^\r\n\]+" \
+ "#5 $::hex in f5 \\(\\) at \[^\r\n\]+" \
+ "#6 $::hex in main \\(\\) at \[^\r\n\]+"]
+
+ set lvl 0
+ set re {}
+ foreach line $full_bt_regexp {
+ if { $lvl == $expected_failure_level } {
+ lappend re "frame cache repeatedly reinitialized"
+ break
+ }
+
+ lappend re $line
+ incr lvl
+ }
+
+ if { [llength $re] > 1 } {
+ set re [multi_line {*}$re]
+ } else {
+ set re [lindex $re 0]
+ }
+
+ gdb_test "bt" $re "check backtrace"
+}
+
+# Each LEVEL_LIST is a list of the frame levels at which the frame
+# cache should be flushed. The frame cache will be flush once for
+# each occurance of a frame level within a list.
+foreach_with_prefix level_list { {} {0} {1} {2} {3} {4} {5} {6} {2 4} {0 1 2 3 4 5 6} {4 4} } {
+ test $level_list
+}
diff --git a/gdb/testsuite/gdb.python/py-frame-cache-flushing.py b/gdb/testsuite/gdb.python/py-frame-cache-flushing.py
new file mode 100644
index 00000000000..73644d4110b
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-frame-cache-flushing.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+from gdb.unwinder import Unwinder
+
+test_debugging = False
+
+
+def debug_log(msg):
+ global test_debugging
+ if not test_debugging:
+ return
+ print(msg)
+
+
+flush_cache_at_levels = {}
+
+
+class cache_flush_unwinder(Unwinder):
+ def __init__(self):
+ super().__init__("cache_flush_unwinder")
+
+ def __call__(self, pending_frame):
+ global flush_cache_at_levels
+
+ level = pending_frame.level()
+ debug_log("Cache flushing unwinder at level %d" % (level))
+
+ if level in flush_cache_at_levels:
+ if flush_cache_at_levels[level] > 0:
+ debug_log(" '-> Flushing the frame cache")
+ gdb.invalidate_cached_frames()
+ flush_cache_at_levels[level] -= 1
+
+ # This unwinder never claims any frames.
+ return None
+
+
+gdb.unwinder.register_unwinder(None, cache_flush_unwinder(), True)
diff --git a/gdbsupport/traits.h b/gdbsupport/traits.h
index 4fe05ee0e0e..be738186da2 100644
--- a/gdbsupport/traits.h
+++ b/gdbsupport/traits.h
@@ -104,6 +104,13 @@ using And = std::conjunction<T...>;
/* Concepts-light-like helper to make SFINAE logic easier to read. */
template<typename Condition>
using Requires = typename std::enable_if<Condition::value, void>::type;
+
+/* Type trait that can be used to ensure an argument pack contains only
+ constant arguments. */
+template <typename... Args>
+struct all_args_are_const
+ : std::bool_constant<(std::is_const_v<std::remove_reference_t<Args>> && ...)>
+{ /* Nothing. */ };
}
template<typename T>
--
2.25.4
^ permalink raw reply [flat|nested] 4+ messages in thread
* [RFC 2/3] gdb: use basic_safe_range to iterate over objfile::qf
2026-08-14 15:06 [RFC 0/3] Deferred, or on-demand, debuginfod downloading Andrew Burgess
2026-08-14 15:06 ` [RFC 1/3] gdb: limited support for frame cache flushing while building stack Andrew Burgess
@ 2026-08-14 15:06 ` Andrew Burgess
2026-08-14 15:06 ` [RFC 3/3] gdb/debuginfod: support on-demand debuginfo downloading Andrew Burgess
2 siblings, 0 replies; 4+ messages in thread
From: Andrew Burgess @ 2026-08-14 15:06 UTC (permalink / raw)
To: gdb-patches; +Cc: Aaron Merey
From: Aaron Merey <amerey@redhat.com>
Change objfile's quick_symbol_function iteration to use a safe
iterator which allows for a quick_symbol_function to be deleted while
iterating over the list.
The next patch adds deferred debug information downloading. To make
this work GDB needs to be able to delete a quick_symbol_function from
an objfile's list while iterating over the quick_symbol_function
list. Such deletion requires that we use a deletion safe iterator.
Make objfile::qf private and rename to objfile::m_qf. Add member
function objfile::add_qf and objfile::clear_qf for manipulating the
quick_symbol_function list now that it is private.
A new member function objfile::qf returns a basic_safe_range object
suitably templated to allow iteration over objfile::m_qf.
All the other changes are straight forward updates to handle the
objfile API changes.
---
gdb/ctfread.c | 4 ++--
gdb/dwarf2/read.c | 4 ++--
gdb/jit.c | 4 ++--
gdb/objfiles.h | 33 ++++++++++++++++++++++++++++++---
gdb/symfile-debug.c | 32 ++++++++++++++++----------------
gdb/symfile.c | 4 ++--
6 files changed, 54 insertions(+), 27 deletions(-)
diff --git a/gdb/ctfread.c b/gdb/ctfread.c
index 5f8c54a34df..c5d623a9b11 100644
--- a/gdb/ctfread.c
+++ b/gdb/ctfread.c
@@ -1324,8 +1324,8 @@ elfctf_build_symtabs (objfile *objfile)
styled_string (file_name_style.style (), bfd_get_filename (abfd)),
ctf_errmsg (err));
- objfile->qf.emplace_front (std::make_unique<expanded_symbols_functions>
- (std::move (iter_data.compunit_symtabs)));
+ objfile->add_qf (std::make_unique<expanded_symbols_functions>
+ (std::move (iter_data.compunit_symtabs)));
}
#else
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 114c608fde3..3557d88833a 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -2139,7 +2139,7 @@ dwarf2_initialize_objfile (struct objfile *objfile,
dwarf_read_debug_printf ("readnow requested");
create_all_units (per_objfile);
- objfile->qf.emplace_front (new readnow_functions);
+ objfile->add_qf (std::make_unique<readnow_functions> ());
}
/* Was a GDB index already read when we processed an objfile sharing
PER_BFD? */
@@ -2169,7 +2169,7 @@ dwarf2_initialize_objfile (struct objfile *objfile,
{
if (dwarf_synchronous)
per_bfd->index_table->wait_completely ();
- objfile->qf.push_front (per_bfd->index_table->make_quick_functions ());
+ objfile->add_qf (per_bfd->index_table->make_quick_functions ());
}
return true;
diff --git a/gdb/jit.c b/gdb/jit.c
index c0aeb41e5aa..73e16e0a704 100644
--- a/gdb/jit.c
+++ b/gdb/jit.c
@@ -683,8 +683,8 @@ jit_object_close_impl (struct gdb_symbol_callbacks *cb,
for (gdb_symtab &symtab : obj->symtabs)
compunit_symtabs.emplace_back (finalize_symtab (&symtab, objfile));
- objfile->qf.emplace_front (std::make_unique<expanded_symbols_functions>
- (std::move (compunit_symtabs)));
+ objfile->add_qf (std::make_unique<expanded_symbols_functions>
+ (std::move (compunit_symtabs)));
add_objfile_entry (objfile, priv_data->entry_addr,
priv_data->entry.symfile_addr,
diff --git a/gdb/objfiles.h b/gdb/objfiles.h
index 406ab36b39d..12e77d51b74 100644
--- a/gdb/objfiles.h
+++ b/gdb/objfiles.h
@@ -344,6 +344,12 @@ class separate_debug_iterator
using separate_debug_range = iterator_range<separate_debug_iterator>;
+/* See objfile::qf_safe. */
+
+using qf_list = std::forward_list<quick_symbol_functions_up>;
+using qf_range = iterator_range<qf_list::iterator>;
+using qf_safe_range = basic_safe_range<qf_range>;
+
/* Sections in an objfile. The section offsets are stored in the
OBJFILE. */
@@ -743,9 +749,22 @@ struct objfile : intrusive_list_node<objfile>
const struct sym_fns *sf = nullptr;
- /* The "quick" (aka partial) symbol functions for this symbol
- reader. */
- std::forward_list<quick_symbol_functions_up> qf;
+ /* Returns an iterable object that allows for safe deletion during
+ iteration. See gdbsupport/safe-iterator.h. */
+ qf_safe_range qf ()
+ {
+ return qf_safe_range (qf_range (m_qf.begin (), m_qf.end ()));
+ }
+
+ void add_qf (quick_symbol_functions_up qf)
+ {
+ m_qf.push_front (std::move (qf));
+ }
+
+ void clear_qf ()
+ {
+ m_qf.clear ();
+ }
/* Per objfile data-pointers required by other GDB modules. */
@@ -861,6 +880,14 @@ struct objfile : intrusive_list_node<objfile>
mechanism as ELF should set this flag too. This flag is used in
conjunction with the minimal_symbol::maybe_copied method. */
bool object_format_has_copy_relocs = false;
+
+private:
+ /* The "quick" (aka partial) symbol functions for this symbol
+ reader. Many quick_symbol_functions methods may result
+ in the deletion of a quick_symbol_functions from this
+ qf_list. It is recommended that qf_safe be used to iterate
+ over the qf_list. */
+ qf_list m_qf;
};
/* A deleter for objfile. */
diff --git a/gdb/symfile-debug.c b/gdb/symfile-debug.c
index 2432a709ddf..e009821f78c 100644
--- a/gdb/symfile-debug.c
+++ b/gdb/symfile-debug.c
@@ -83,7 +83,7 @@ objfile::has_partial_symbols ()
them, then that is an indication that they are in fact available. Without
this function the symbols may have been already read in but they also may
not be present in this objfile. */
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
{
retval = iter->has_symbols (this);
if (retval)
@@ -106,7 +106,7 @@ objfile::has_unexpanded_symtabs ()
objfile_debug_name (this));
bool result = false;
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
{
if (iter->has_unexpanded_symtabs (this))
{
@@ -131,7 +131,7 @@ objfile::find_last_source_symtab ()
gdb_printf (gdb_stdlog, "qf->find_last_source_symtab (%s)\n",
objfile_debug_name (this));
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
{
retval = iter->find_last_source_symtab (this);
if (retval != nullptr)
@@ -155,7 +155,7 @@ objfile::forget_cached_source_info ()
for (compunit_symtab &cu : compunits ())
cu.forget_cached_source_info ();
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
iter->forget_cached_source_info (this);
}
@@ -273,7 +273,7 @@ objfile::find_symtab_matching_filename (const char *name,
: iteration_status::stop);
};
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
if (iter->search (this, match_one_filename, nullptr, nullptr,
compunit_callback,
SEARCH_GLOBAL_BLOCK | SEARCH_STATIC_BLOCK,
@@ -325,7 +325,7 @@ objfile::lookup_symbol (block_enum kind, const lookup_name_info &name,
return iteration_status::keep_going;
};
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
{
if (iter->search (this, nullptr, &name, nullptr, search_one_symtab,
kind == GLOBAL_BLOCK
@@ -352,7 +352,7 @@ objfile::print_stats (bool print_bcache)
gdb_printf (gdb_stdlog, "qf->print_stats (%s, %d)\n",
objfile_debug_name (this), print_bcache);
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
iter->print_stats (this, print_bcache);
}
@@ -363,7 +363,7 @@ objfile::dump ()
gdb_printf (gdb_stdlog, "qf->dump (%s)\n",
objfile_debug_name (this));
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
iter->dump (this);
}
@@ -374,7 +374,7 @@ objfile::expand_all_symtabs ()
gdb_printf (gdb_stdlog, "qf->expand_all_symtabs (%s)\n",
objfile_debug_name (this));
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
iter->expand_all_symtabs (this);
}
@@ -392,7 +392,7 @@ objfile::expand_symtabs_with_fullname (const char *fullname)
return filename_cmp (basenames ? basename : fullname, filename) == 0;
};
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
iter->search (this, file_matcher, nullptr, nullptr, nullptr,
SEARCH_GLOBAL_BLOCK | SEARCH_STATIC_BLOCK,
SEARCH_ALL_DOMAINS);
@@ -419,7 +419,7 @@ objfile::search (search_symtabs_file_matcher file_matcher,
host_address_to_string (&compunit_callback),
domain_name (domain).c_str ());
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
if (iter->search (this, file_matcher, lookup_name, symbol_matcher,
compunit_callback, search_flags, domain, lang_matcher)
== iteration_status::stop)
@@ -445,7 +445,7 @@ objfile::find_pc_sect_compunit_symtab (bound_minimal_symbol msymbol,
host_address_to_string (section),
warn_if_readin);
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
{
retval = iter->find_pc_sect_compunit_symtab (this, msymbol, pc, section,
warn_if_readin);
@@ -472,7 +472,7 @@ objfile::map_symbol_filenames (symbol_filename_listener fun, bool need_fullname)
objfile_debug_name (this),
need_fullname);
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
iter->map_symbol_filenames (this, fun, need_fullname);
}
@@ -484,7 +484,7 @@ objfile::compute_main_name ()
"qf->compute_main_name (%s)\n",
objfile_debug_name (this));
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
iter->compute_main_name (this);
}
@@ -498,7 +498,7 @@ objfile::find_symbol_by_address (CORE_ADDR address)
hex_string (address));
struct symbol *result = NULL;
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
{
result = iter->find_symbol_by_address (this, address);
if (result != nullptr)
@@ -523,7 +523,7 @@ objfile::lookup_global_symbol_language (const char *name,
enum language result = language_unknown;
*symbol_found_p = false;
- for (const auto &iter : qf)
+ for (const auto &iter : qf ())
{
result = iter->lookup_global_symbol_language (this, name, domain,
symbol_found_p);
diff --git a/gdb/symfile.c b/gdb/symfile.c
index 017f7a49d8d..ce37d764485 100644
--- a/gdb/symfile.c
+++ b/gdb/symfile.c
@@ -881,7 +881,7 @@ syms_from_objfile_1 (struct objfile *objfile,
scoped_objfile_unlinker objfile_holder (objfile);
objfile_set_sym_fns (objfile, find_sym_fns (objfile->obfd.get ()));
- objfile->qf.clear ();
+ objfile->clear_qf ();
if (objfile->sf == NULL)
{
@@ -2603,7 +2603,7 @@ reread_symbols (int from_tty)
based on whether .gdb_index is present, and we need it to
start over. PR symtab/15885 */
objfile_set_sym_fns (&objfile, find_sym_fns (objfile.obfd.get ()));
- objfile.qf.clear ();
+ objfile.clear_qf ();
build_objfile_section_table (&objfile);
--
2.25.4
^ permalink raw reply [flat|nested] 4+ messages in thread
* [RFC 3/3] gdb/debuginfod: support on-demand debuginfo downloading
2026-08-14 15:06 [RFC 0/3] Deferred, or on-demand, debuginfod downloading Andrew Burgess
2026-08-14 15:06 ` [RFC 1/3] gdb: limited support for frame cache flushing while building stack Andrew Burgess
2026-08-14 15:06 ` [RFC 2/3] gdb: use basic_safe_range to iterate over objfile::qf Andrew Burgess
@ 2026-08-14 15:06 ` Andrew Burgess
2 siblings, 0 replies; 4+ messages in thread
From: Andrew Burgess @ 2026-08-14 15:06 UTC (permalink / raw)
To: gdb-patches; +Cc: Aaron Merey
From: Aaron Merey <amerey@redhat.com>
At the beginning of a session, GDB may attempt to download debuginfo
for all shared libraries associated with the process or core file
being debugged. This can be a waste of time and storage space when much
of the debuginfo ends up not being used during the session.
To reduce the GDB's startup latency and to download only the debuginfo
that is really needed, this patch adds on-demand downloading of debuginfo.
The command 'set debuginfo enabled on' now causes GDB to attempt to
download a .gdb_index for each shared library instead of its full
debuginfo. Each corresponding separate debuginfo will be deferred
until GDB needs to expand symtabs associated with the debuginfo's
index.
Because these indices are significantly smaller than their corresponding
debuginfo, this generally reduces the total amount of data GDB downloads.
Reductions of 80%-95% have been observed when debugging large GUI programs.
(gdb) set debuginfod enabled on
(gdb) start
Downloading section .gdb_index for /lib64/libcurl.so.4
[...]
1826 client->server_mhandle = curl_multi_init ();
(gdb) step
Downloading separate debug info for /lib64/libcurl.so.4
Downloading separate debug info for [libcurl dwz]
Downloading source file /usr/src/debug/curl-7.85.0-6.fc37.x86_64/build-full/lib/../../lib/multi.c
curl_multi_init () at ../../lib/multi.c:457
457 {
(gdb)
Some of the key functions in this patch include dwarf2_has_separate_index
which downloads the separate .gdb_index. If successful, a shared library
objfile owns the index until its separate debug objfile is downloaded
or found to be unavailable.
read_full_dwarf_from_debuginfod downloads the full debuginfo and
initializes the separate debug objfile. It is called by functions
such as dw2_expand_symtabs_matching_symbol and
dwarf2_gdb_index::expand_all_symtabs when symtab expansion is required.
The deferred-download.exp test is a basic test for the core functionality.
The deferred-expand-warning.exp test is testing for the following edge case:
1. When find_pc_sect_compunit_symtab matches a PC in a deferred
objfile's index, it triggers download of full debuginfo and
creation of a separate debug objfile.
2. An observer for this new objfile (e.g., an auto-loaded Python
script) might trigger symtab expansion, for example, by searching
for a symbol within the objfile.
4. When the loop in find_pc_sect_compunit_symtab continues to the
new objfile, its symtabs may already be expanded, but the first
loop in find_pc_sect_compunit_symtab (which searches expanded
symtabs) never had a chance to see this expanded symtab because
the objfile didn't exist then.
5. When the second loop finds a matching symtab, which is already
expanded, which the first loop didn't find, GDB emits a warning
with text like: "Internal error: pc ... in read in CU, but not in
symtab". This warning is only emitted when warn_if_readin is
true. To avoid this warning, we pass warn_if_readin as false
when checking an objfile that was downloaded due to deferred
debug download.
The deferred-pending.exp test is testing for the following situation:
1. When on-demand downloading triggers a download, finish_new_objfile
is called for the new separate debug objfile, which can then call
breakpoint_re_set.
2. If there are deferred breakpoints then GDB will search for debug
information relating to the breakpoint symbol.
3. If the objfile for which the new debug info was downloaded is still
marked as having deferred debug information, and there is a
deferred breakpoint within this objfile, then GDB will try to
re-download the debug information for this objfile, which cases GDB
to then add another new objfile, which calls finish_new_objfile,
and thus GDB gets stuck in a loop.
The solution is to clear the deferred debug status flag early in
read_full_dwarf_from_debuginfod. Once read_full_dwarf_from_debuginfod
is reached we have already decided to download the deferred debug
information. After this the fact that this download was deferred is no
longer relevant.
The deferred-pending.exp test sets pending breakpoints on functions in
two libraries with deferred downloads, then runs the inferior. If the
deferred status is still set when finish_new_objfile is called GDB will
enter the recursion loop, and eventually crash.
The deferred-frame-cache.exp test is checking that GDB is able to to
establish the current frame when a core file is loaded. In order to
establish the current frame GDB calls lookup_selected_frame (frame.c),
which may cause on-demand debuginfo download. If a new_objfile
observer calls gdb.invalidate_cached_frames() during this download,
the frame cache is cleared.
The problem is that frame selection is done early in select_frame
(frame.c), when selected_frame is set. The on-demand debug info
downloading is triggered late within select_frame, by the call to
find_compunit_symtab_for_pc. As a consequence, of clearing the frame
cache, the selected_frame will be reset back to NULL.
The fix in lookup_selected_frame handles this by retrying the frame
lookup if selected_frame is nullptr after the initial attempt. This
works because we assume the second lookup will not trigger the same
observers (all debug should now be downloaded), and so, for the second
call, the selected_frame will not be reset.
Without this fix, get_selected_frame hits an assertion failure because
selected_frame is NULL after a call to lookup_selected_frame.
The test:
1. Builds a shared library that crashes via null pointer dereference.
2. Strips debuginfo for debuginfod to serve on-demand.
3. Runs the program outside GDB to generate a core file.
4. Starts GDB and loads a Python observer that invalidates frame
cache from the new_objfile observer. This will trigger for on
demand debuginfo load.
5. Loads the core file, triggering the frame cache invalidation.
6. Checks for assertions from GDB.
The new deferred-observer-symbols.exp test is checking that when a
deferred separate debug file is loaded, a Python observer, triggered
from the new_objfile handler, is able to search for, and find, symbols
within the newly added separate debug file.
If GDB incorrectly manages the deferred debug state within the objfile
then trying to access these symbols as soon as the new objfile is
loaded can cause GDB to try and re-download the separate debug info
file, in a recursive loop. This will eventually cause GDB to crash.
The deferred-no-debuginfo.exp test is checking that GDB can handle the
problem case where debuginfod serves a valid .gdb_index, but when
asked for the full debug information, either gives back a file
containing no debug information, or doesn't respond at all. In both
cases, GDB should continue as if there is no debug infomration
available, the test is ensuring that there are no crashes or other
strange behaviours.
The deferred-auto-load.exp test is checking that GDB will correctly
auto-load from any .debug_gdb_scripts sections within separate debug
info file that are downloaded from debuginfod. The test places a
script within the main executable and within a shared library, the
debug info is then split into a separate file, the separate debug file
contains the .debug_gdb_scripts section. The separate debug files are
then served to GDB from debuginfod.
The deferred-dwz.exp test sets up a DWZ file with content pulled from
two shared libraries. The libraries make use of deferred debug
information downloading. The test ensures that the DWZ file is also
downloaded correctly.
---
gdb/dwarf2/index-cache.c | 22 +-
gdb/dwarf2/index-cache.h | 11 +
gdb/dwarf2/public.h | 12 +
gdb/dwarf2/read-gdb-index.c | 185 +++++++++++++
gdb/dwarf2/read-gdb-index.h | 9 +
gdb/dwarf2/read.c | 183 ++++++++++++-
gdb/dwarf2/read.h | 9 +
gdb/elfread.c | 3 +-
gdb/frame.c | 52 ++--
gdb/objfile-flags.h | 4 +
gdb/objfiles.h | 18 ++
gdb/symfile-debug.c | 11 +
.../gdb.debuginfod/deferred-auto-load-lib.c | 26 ++
.../deferred-auto-load-py-script.h | 41 +++
.../gdb.debuginfod/deferred-auto-load.c | 27 ++
.../gdb.debuginfod/deferred-auto-load.exp | 223 ++++++++++++++++
.../gdb.debuginfod/deferred-download-lib1.c | 43 +++
.../gdb.debuginfod/deferred-download-lib2.c | 37 +++
.../gdb.debuginfod/deferred-download.c | 29 ++
.../gdb.debuginfod/deferred-download.exp | 158 +++++++++++
.../gdb.debuginfod/deferred-dwz-common.h | 41 +++
.../gdb.debuginfod/deferred-dwz-lib1.c | 24 ++
.../gdb.debuginfod/deferred-dwz-lib2.c | 24 ++
gdb/testsuite/gdb.debuginfod/deferred-dwz.c | 30 +++
gdb/testsuite/gdb.debuginfod/deferred-dwz.exp | 247 ++++++++++++++++++
.../deferred-expand-warning-lib.c | 36 +++
.../gdb.debuginfod/deferred-expand-warning.c | 25 ++
.../deferred-expand-warning.exp | 177 +++++++++++++
.../gdb.debuginfod/deferred-expand-warning.py | 61 +++++
.../gdb.debuginfod/deferred-frame-cache-lib.c | 25 ++
.../gdb.debuginfod/deferred-frame-cache.c | 28 ++
.../gdb.debuginfod/deferred-frame-cache.exp | 180 +++++++++++++
.../gdb.debuginfod/deferred-frame-cache.py | 68 +++++
.../deferred-no-debuginfo-lib.c | 22 ++
.../gdb.debuginfod/deferred-no-debuginfo.c | 25 ++
.../gdb.debuginfod/deferred-no-debuginfo.exp | 194 ++++++++++++++
.../deferred-observer-symbols.exp | 161 ++++++++++++
.../deferred-observer-symbols.py | 62 +++++
.../gdb.debuginfod/deferred-pending.exp | 172 ++++++++++++
.../deferred-select-frame-lib1.c | 26 ++
.../deferred-select-frame-lib2.c | 26 ++
.../gdb.debuginfod/deferred-select-frame.c | 61 +++++
.../gdb.debuginfod/deferred-select-frame.exp | 240 +++++++++++++++++
.../gdb.debuginfod/deferred-select-frame.py | 44 ++++
gdb/testsuite/lib/debuginfod-support.exp | 31 ++-
gdb/testsuite/lib/gdb.exp | 8 +-
46 files changed, 3102 insertions(+), 39 deletions(-)
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-download.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-dwz.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-pending.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.c
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp
create mode 100644 gdb/testsuite/gdb.debuginfod/deferred-select-frame.py
diff --git a/gdb/dwarf2/index-cache.c b/gdb/dwarf2/index-cache.c
index 0e0aca3ac6d..709f99003be 100644
--- a/gdb/dwarf2/index-cache.c
+++ b/gdb/dwarf2/index-cache.c
@@ -217,14 +217,22 @@ index_cache::lookup_gdb_index (const bfd_build_id *build_id,
/* Compute where we would expect a gdb index file for this build id to be. */
std::string filename = make_index_filename (build_id, INDEX4_SUFFIX);
+ return lookup_gdb_index (filename.c_str (), resource);
+}
+
+/* See index-cache.h. */
+
+gdb::array_view<const gdb_byte>
+index_cache::lookup_gdb_index (const char *filename,
+ std::unique_ptr<index_cache_resource> *resource)
+{
try
{
- index_cache_debug ("trying to read %s",
- filename.c_str ());
+ index_cache_debug ("trying to read %s", filename);
/* Try to map that file. */
index_cache_resource_mmap *mmap_resource
- = new index_cache_resource_mmap (filename.c_str ());
+ = new index_cache_resource_mmap (filename);
/* Yay, it worked! Hand the resource to the caller. */
resource->reset (mmap_resource);
@@ -236,7 +244,7 @@ index_cache::lookup_gdb_index (const bfd_build_id *build_id,
catch (const gdb_exception_error &except)
{
index_cache_debug ("couldn't read %s: %s",
- filename.c_str (), except.what ());
+ filename, except.what ());
}
return {};
@@ -253,6 +261,12 @@ index_cache::lookup_gdb_index (const bfd_build_id *build_id,
return {};
}
+gdb::array_view<const gdb_byte>
+index_cache::lookup_gdb_index_debuginfod
+ (const char *index_path, std::unique_ptr<index_cache_resource> *resource)
+{
+ return {};
+}
#endif
/* See dwarf-index-cache.h. */
diff --git a/gdb/dwarf2/index-cache.h b/gdb/dwarf2/index-cache.h
index 4307bf97cf8..74ba3094444 100644
--- a/gdb/dwarf2/index-cache.h
+++ b/gdb/dwarf2/index-cache.h
@@ -93,6 +93,17 @@ class index_cache
lookup_gdb_index (const bfd_build_id *build_id,
index_cache_resource_up *resource);
+ /* Look for an index file located at INDEX_PATH. If found, return the
+ contents as an array_view and store the underlying resources (allocated
+ memory, mapped file, etc) in RESOURCE. The returned array_view is valid
+ as long as RESOURCE is not destroyed.
+
+ If no matching index file is found, return an empty array view. This
+ function does not exit early if the index cache has not been enabled. */
+ gdb::array_view<const gdb_byte>
+ lookup_gdb_index (const char *filename,
+ std::unique_ptr<index_cache_resource> *resource);
+
/* Return the number of cache hits. */
unsigned int n_hits () const
{ return m_n_hits; }
diff --git a/gdb/dwarf2/public.h b/gdb/dwarf2/public.h
index 2fb81a0749c..a439958d596 100644
--- a/gdb/dwarf2/public.h
+++ b/gdb/dwarf2/public.h
@@ -50,6 +50,11 @@ extern void dwarf2_build_frame_info (struct objfile *);
void dwarf2_append_unwinders (struct gdbarch *gdbarch);
+/* Query debuginfod for the .gdb_index associated with OBJFILE.
+ Used to defer separate debuginfo downloading until necessary. */
+
+extern bool dwarf2_has_separate_index (struct objfile *);
+
#else /* DWARF_FORMAT_AVAILABLE */
static inline bool
@@ -67,6 +72,13 @@ dwarf2_build_frame_info (struct objfile *)
warning (_("No dwarf support available."));
}
+static inline bool
+dwarf2_has_separate_index (struct objfile *)
+{
+ warning (_("No dwarf support available."));
+ return false;
+}
+
#endif /* DWARF_FORMAT_AVAILABLE */
#endif /* GDB_DWARF2_PUBLIC_H */
diff --git a/gdb/dwarf2/read-gdb-index.c b/gdb/dwarf2/read-gdb-index.c
index 324984c0de2..f261171166a 100644
--- a/gdb/dwarf2/read-gdb-index.c
+++ b/gdb/dwarf2/read-gdb-index.c
@@ -72,6 +72,186 @@ class cooked_gdb_index : public cooked_index
int version;
};
+/* A quick_symbol_functions implementation for objfiles with deferred
+ debuginfo downloading. This class encapsulates the
+ download-and-delegate pattern: each method checks whether the index
+ indicates a match, downloads the full debug info if so, then
+ delegates the query to the newly-created child objfile.
+
+ By concentrating all deferred logic here, the normal (non-deferred)
+ code paths in dwarf2_gdb_index, cooked_index_functions, and
+ dwarf2_base_index_functions remain free of OBJF_DOWNLOAD_DEFERRED
+ checks. */
+
+struct deferred_index_functions : public dwarf2_gdb_index
+{
+ bool has_symbols (struct objfile *) override
+ { return true; }
+
+ bool has_unexpanded_symtabs (struct objfile *) override
+ { return true; }
+
+ void expand_all_symtabs (struct objfile *objfile) override;
+ struct symtab *find_last_source_symtab (struct objfile *objfile) override;
+
+ struct compunit_symtab *find_pc_sect_compunit_symtab
+ (struct objfile *objfile, bound_minimal_symbol msymbol,
+ CORE_ADDR pc, struct obj_section *section,
+ int warn_if_readin) override;
+
+ iteration_status search
+ (struct objfile *objfile,
+ search_symtabs_file_matcher file_matcher,
+ const lookup_name_info *lookup_name,
+ search_symtabs_symbol_matcher symbol_matcher,
+ compunit_symtab_iteration_callback compunit_callback,
+ block_search_flags search_flags,
+ domain_search_flags domain,
+ search_symtabs_lang_matcher lang_matcher) override;
+
+ void dump (struct objfile *objfile) override;
+
+ /* No-ops — deferred index has no data for these. */
+ void forget_cached_source_info (struct objfile *) override
+ { }
+
+ void print_stats (struct objfile *, bool) override
+ { }
+
+ void map_symbol_filenames (objfile *, symbol_filename_listener,
+ bool) override
+ { }
+
+ struct symbol *find_symbol_by_address (struct objfile *,
+ CORE_ADDR) override
+ { return nullptr; }
+};
+
+/* See read-gdb-index.h. */
+
+quick_symbol_functions_up
+make_deferred_gdb_index_functions ()
+{
+ return quick_symbol_functions_up (new deferred_index_functions);
+}
+
+void
+deferred_index_functions::expand_all_symtabs (struct objfile *objfile)
+{
+ struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this);
+ if (child != nullptr)
+ child->expand_all_symtabs ();
+}
+
+struct symtab *
+deferred_index_functions::find_last_source_symtab (struct objfile *objfile)
+{
+ struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this);
+ if (child != nullptr)
+ return child->find_last_source_symtab ();
+ return nullptr;
+}
+
+struct compunit_symtab *
+deferred_index_functions::find_pc_sect_compunit_symtab
+ (struct objfile *objfile,
+ bound_minimal_symbol msymbol,
+ CORE_ADDR pc,
+ struct obj_section *section,
+ int warn_if_readin)
+{
+ dwarf2_per_objfile *per_objfile = get_dwarf2_per_objfile (objfile);
+ dwarf2_per_bfd *per_bfd = per_objfile->per_bfd;
+
+ if (per_bfd->index_table == nullptr)
+ return nullptr;
+
+ CORE_ADDR baseaddr = objfile->text_section_offset ();
+ dwarf2_per_cu *data
+ = per_bfd->index_table->lookup ((unrelocated_addr) (pc - baseaddr));
+ if (data == nullptr)
+ return nullptr;
+
+ /* PC matches a symbol in the index but full debuginfo hasn't been
+ acquired yet. Download it and search the separate debug objfile
+ directly. Pass warn_if_readin=0 because the download triggers a
+ new_objfile observer that may legitimately expand symtabs in the
+ child before we query it. */
+ struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this);
+ if (child != nullptr)
+ return child->find_pc_sect_compunit_symtab (msymbol, pc, section, 0);
+ return nullptr;
+}
+
+iteration_status
+deferred_index_functions::search
+ (struct objfile *objfile,
+ search_symtabs_file_matcher file_matcher,
+ const lookup_name_info *lookup_name,
+ search_symtabs_symbol_matcher symbol_matcher,
+ compunit_symtab_iteration_callback compunit_callback,
+ block_search_flags search_flags,
+ domain_search_flags domain,
+ search_symtabs_lang_matcher lang_matcher)
+{
+ if (lookup_name == nullptr)
+ {
+ /* No name to filter by — we must download to answer the query. */
+ struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this);
+ if (child != nullptr)
+ return child->search (file_matcher, nullptr, nullptr,
+ compunit_callback, search_flags, domain, lang_matcher);
+ return iteration_status::keep_going;
+ }
+
+ /* Check if the name exists in the index before downloading. */
+ cooked_index *table = wait (objfile, true);
+
+ lookup_name_info lookup_name_without_params
+ = lookup_name->make_ignore_params ();
+ bool completing = lookup_name->completion_mode ();
+
+ /* Unique styles of language splitting. */
+ static const enum language unique_styles[] =
+ {
+ language_c,
+ language_cplus,
+ language_d,
+ language_ada
+ };
+
+ for (enum language lang : unique_styles)
+ {
+ std::vector<std::string_view> name_vec
+ = lookup_name_without_params.split_name (lang);
+ std::vector<std::string> name_str_vec (name_vec.begin (),
+ name_vec.end ());
+
+ auto range = table->find (name_str_vec.back (), completing);
+ if (range.begin () != range.end ())
+ {
+ /* Found a potential match in the index. Download and
+ delegate. */
+ struct objfile *child = read_full_dwarf_from_debuginfod (objfile, this);
+ if (child != nullptr)
+ return child->search (file_matcher, lookup_name,
+ symbol_matcher, compunit_callback,
+ search_flags, domain, lang_matcher);
+ return iteration_status::keep_going;
+ }
+ }
+
+ /* Name not found in the index — no download needed. */
+ return iteration_status::keep_going;
+}
+
+void
+deferred_index_functions::dump (struct objfile *objfile)
+{
+ dwarf2_gdb_index::dump (objfile);
+ gdb_printf (" (deferred debuginfod download pending)\n");
+}
+
/* See above. */
void
@@ -542,6 +722,11 @@ create_cus_from_gdb_index (dwarf2_per_bfd *per_bfd,
gdb_assert (per_bfd->all_units.empty ());
per_bfd->all_units.reserve ((cu_list_elements + dwz_elements) / 2);
+ /* An index might be read before the debug_info section is available.
+ Create a placeholder section. */
+ if (per_bfd->infos.empty ())
+ per_bfd->infos.resize (1);
+
create_cus_from_gdb_index_list (per_bfd, cu_list, cu_list_elements,
&per_bfd->infos[0], 0, units);
diff --git a/gdb/dwarf2/read-gdb-index.h b/gdb/dwarf2/read-gdb-index.h
index 56e112c6cbb..f03a8b97dd9 100644
--- a/gdb/dwarf2/read-gdb-index.h
+++ b/gdb/dwarf2/read-gdb-index.h
@@ -26,6 +26,7 @@ struct dwarf2_per_bfd;
struct dwarf2_per_objfile;
struct dwz_file;
struct objfile;
+struct quick_symbol_functions;
/* .gdb_index doesn't distinguish between the various "other" symbols
-- but the symbol search machinery really wants to. For example,
@@ -58,4 +59,12 @@ bool dwarf2_read_gdb_index
get_gdb_index_contents_ftype get_gdb_index_contents,
get_gdb_index_contents_dwz_ftype get_gdb_index_contents_dwz);
+/* Create a quick_symbol_functions for an objfile with deferred
+ debuginfo downloading. The returned object uses the .gdb_index
+ to gate downloads, only downloading full debug info when the
+ index indicates a match. */
+
+extern std::unique_ptr<struct quick_symbol_functions>
+ make_deferred_gdb_index_functions ();
+
#endif /* GDB_DWARF2_READ_GDB_INDEX_H */
diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c
index 3557d88833a..8d434fc84aa 100644
--- a/gdb/dwarf2/read.c
+++ b/gdb/dwarf2/read.c
@@ -36,6 +36,7 @@
#include "dwarf2/cooked-index-worker.h"
#include "dwarf2/cooked-indexer.h"
#include "dwarf2/cu.h"
+#include "dwarf2/frame.h"
#include "dwarf2/index-cache.h"
#include "dwarf2/leb.h"
#include "dwarf2/line-header.h"
@@ -98,6 +99,8 @@
#include "gdbsupport/unordered_set.h"
#include "extract-store-integer.h"
#include "cli/cli-style.h"
+#include "inferior.h"
+#include "debuginfod-support.h"
/* See read.h. */
unsigned int dwarf_read_debug = 0;
@@ -2113,6 +2116,65 @@ get_gdb_index_contents_from_cache_dwz (objfile *obj, dwz_file *dwz)
return global_index_cache.lookup_gdb_index (build_id, &dwz->index_cache_res);
}
+/* Query debuginfod for the .gdb_index matching OBJFILE's build-id. Return the
+ contents if successful. */
+
+static gdb::array_view<const gdb_byte>
+get_gdb_index_contents_from_debuginfod (objfile *objfile,
+ dwarf2_per_bfd *per_bfd)
+{
+ const bfd_build_id *build_id = build_id_bfd_get (objfile->obfd.get ());
+ if (build_id == nullptr)
+ return {};
+
+ gdb::unique_xmalloc_ptr<char> index_path;
+ scoped_fd fd = debuginfod_section_query (build_id->data, build_id->size,
+ bfd_get_filename
+ (objfile->obfd.get ()),
+ ".gdb_index",
+ &index_path);
+ if (fd.get () < 0)
+ return {};
+
+ return global_index_cache.lookup_gdb_index
+ (index_path.get (), &per_bfd->index_cache_res);
+}
+
+/* Read the .gdb_index from DWZ, a dwz file, which we're looking at as
+ a consequence of loading OBJ.
+
+ This function is passed to dwarf2_read_gdb_index only in the case
+ where we are performing deferred debug info download, and will only
+ be called if OBJ has an associated dwz file.
+
+ In order to create a dwz_file for an objfile GDB checks two things:
+
+ 1. The objfile must have some debug information, and
+
+ 2. The objfile must have a link to the dwz file,
+ e.g. .gnu_debugaltlink.
+
+ However, if an objfile satisfies (1) then we shouldn't need to use
+ deferred debug information downloading, and so this function should
+ never be called.
+
+ As such, we don't actually try to fetch the .gdb_index for the dwz
+ file here, we just emit a warning, and return an empty array view.
+ Back in dwarf2_read_gdb_index the empty array view will trigger
+ another warning, and then GDB will proceed without any index for
+ OBJ.
+
+ Right now I cannot imagine how we'd trigger this situation, but if
+ we ever see this then we can updated this code as appropriate. */
+
+static gdb::array_view<const gdb_byte>
+get_gdb_index_contents_from_debuginfod_dwz (objfile *obj, dwz_file *dwz)
+{
+ warning (_("ignoring unexpected dwz file for %ps"),
+ styled_string (file_name_style.style (), objfile_name (obj)));
+ return {};
+}
+
static void start_debug_info_reader (dwarf2_per_objfile *);
/* See dwarf2/public.h. */
@@ -2122,11 +2184,13 @@ dwarf2_initialize_objfile (struct objfile *objfile,
const struct dwarf2_debug_sections *names,
bool can_copy)
{
- if (!dwarf2_has_info (objfile, names, can_copy))
+ if (!dwarf2_has_info (objfile, names, can_copy)
+ && (objfile->flags & OBJF_DOWNLOAD_DEFERRED) == 0)
return false;
dwarf2_per_objfile *per_objfile = get_dwarf2_per_objfile (objfile);
dwarf2_per_bfd *per_bfd = per_objfile->per_bfd;
+ bool separate_index = false;
dwarf_read_debug_printf ("called");
@@ -2153,12 +2217,21 @@ dwarf2_initialize_objfile (struct objfile *objfile,
dwarf_read_debug_printf ("found gdb index from file");
/* ... otherwise, try to find the index in the index cache. */
else if (dwarf2_read_gdb_index (per_objfile,
- get_gdb_index_contents_from_cache,
- get_gdb_index_contents_from_cache_dwz))
+ get_gdb_index_contents_from_cache,
+ get_gdb_index_contents_from_cache_dwz))
{
dwarf_read_debug_printf ("found gdb index from cache");
global_index_cache.hit ();
}
+ /* Try to read just a separately downloaded gdb index. */
+ else if ((objfile->flags & OBJF_DOWNLOAD_DEFERRED) != 0
+ && dwarf2_read_gdb_index (per_objfile,
+ get_gdb_index_contents_from_debuginfod,
+ get_gdb_index_contents_from_debuginfod_dwz))
+ {
+ dwarf_read_debug_printf ("found .gdb_index from debuginfod");
+ separate_index = true;
+ }
else
{
global_index_cache.miss ();
@@ -2169,7 +2242,11 @@ dwarf2_initialize_objfile (struct objfile *objfile,
{
if (dwarf_synchronous)
per_bfd->index_table->wait_completely ();
- objfile->add_qf (per_bfd->index_table->make_quick_functions ());
+
+ if (separate_index)
+ objfile->add_qf (make_deferred_gdb_index_functions ());
+ else
+ objfile->add_qf (per_bfd->index_table->make_quick_functions ());
}
return true;
@@ -2177,6 +2254,104 @@ dwarf2_initialize_objfile (struct objfile *objfile,
/* See read.h. */
+objfile *
+read_full_dwarf_from_debuginfod (struct objfile *objfile,
+ quick_symbol_functions *qf)
+{
+ gdb_assert ((objfile->flags & OBJF_DOWNLOAD_DEFERRED) != 0);
+
+ /* However this function exits, we no longer want OBJFILE to be
+ marked as having deferred debug information. Of particular
+ interest is the case where we do load some separate debug
+ information and then add it into the program space. This can
+ trigger a call to breakpoint_re_set, which, if this object is
+ still marked as having deferred debug information, could result
+ in recursion, where we try to re-download the same debug
+ information.
+
+ Clearing the deferred marker now is fine, nothing called from
+ here relies on this flag. */
+ objfile->remove_deferred_status (qf);
+
+ const struct bfd_build_id *build_id = build_id_bfd_get (objfile->obfd.get ());
+ if (build_id == nullptr)
+ return nullptr;
+
+ const char *filename = bfd_get_filename (objfile->obfd.get ());
+ gdb::unique_xmalloc_ptr<char> symfile_path;
+ scoped_fd fd;
+
+ fd = debuginfod_debuginfo_query (build_id->data, build_id->size,
+ filename, &symfile_path);
+ if (fd.get () < 0)
+ return nullptr;
+
+ /* Separate debuginfo successfully retrieved from server. */
+ gdb_bfd_ref_ptr debug_bfd = symfile_bfd_open (symfile_path.get ());
+ if (debug_bfd == nullptr
+ || !build_id_verify (debug_bfd.get (), build_id->size, build_id->data))
+ {
+ warning (_("File \"%s\" from debuginfod cannot be opened as bfd"),
+ filename);
+ return nullptr;
+ }
+
+ /* This may trigger a dwz download. */
+ symbol_file_add_separate (debug_bfd, symfile_path.get (),
+ current_inferior ()->symfile_flags, objfile);
+ return objfile->separate_debug_objfile;
+}
+
+/* See public.h. */
+
+bool
+dwarf2_has_separate_index (struct objfile *objfile)
+{
+ /* An objfile already marked as OBJF_DOWNLOAD_DEFERRED will not get
+ here, as GDB will see such an objfile as having debug
+ information. See dwarf2_initialize_objfile for details. */
+ gdb_assert ((objfile->flags & OBJF_DOWNLOAD_DEFERRED) == 0);
+
+ /* There is no point deferring the download of the debug information
+ for the main objfile, we (almost) always end up needing the debug
+ information, so downloading the index first is just adding
+ additional work. */
+ if ((objfile->flags & OBJF_MAINLINE) != 0)
+ return false;
+
+ /* Objfiles marked OBJF_NOT_FILENAME include things like the vDSO
+ objfile, and JIT registered objfiles, for both of which,
+ downloading an index doesn't make much sense. */
+ if ((objfile->flags & OBJF_NOT_FILENAME) != 0)
+ return false;
+
+ const bfd_build_id *build_id = build_id_bfd_get (objfile->obfd.get ());
+
+ if (build_id == nullptr)
+ return false;
+
+ gdb::unique_xmalloc_ptr<char> index_path;
+ scoped_fd fd = debuginfod_section_query (build_id->data,
+ build_id->size,
+ bfd_get_filename
+ (objfile->obfd.get ()),
+ ".gdb_index",
+ &index_path);
+
+ if (fd.get () < 0)
+ return false;
+
+ /* We found a separate .gdb_index file so a separate debuginfo file should
+ exist, but we don't want to download it until necessary. Associate the
+ index with this objfile and defer the debuginfo download until symtabs
+ referenced by the index need to be expanded. */
+ objfile->flags |= OBJF_DOWNLOAD_DEFERRED;
+ dwarf2_initialize_objfile (objfile);
+ return true;
+}
+
+\f
+
void
dwarf2_find_base_address (struct die_info *die, struct dwarf2_cu *cu)
{
diff --git a/gdb/dwarf2/read.h b/gdb/dwarf2/read.h
index 15dd2abf3a1..765ab54cfa1 100644
--- a/gdb/dwarf2/read.h
+++ b/gdb/dwarf2/read.h
@@ -1489,4 +1489,13 @@ extern struct dwarf2_section_info *get_debug_line_section
extern bool is_ada_import_or_export (dwarf2_cu *cu, const char *name,
const char *linkagename);
+/* If OBJFILE contains information from a separately downloaded .gdb_index,
+ attempt to download the full debuginfo. QF is the deferred
+ quick_symbol_functions that triggered the download; it will be
+ removed from OBJFILE's qf list. Returns the separate debug
+ objfile on success, or nullptr on failure. */
+
+extern objfile *read_full_dwarf_from_debuginfod (struct objfile *,
+ quick_symbol_functions *);
+
#endif /* GDB_DWARF2_READ_H */
diff --git a/gdb/elfread.c b/gdb/elfread.c
index e3890ae0270..f51636090db 100644
--- a/gdb/elfread.c
+++ b/gdb/elfread.c
@@ -1222,7 +1222,8 @@ elf_symfile_read_dwarf2 (struct objfile *objfile,
&& objfile->separate_debug_objfile_backlink == NULL)
{
if (objfile->find_and_add_separate_symbol_file (symfile_flags))
- gdb_assert (objfile->separate_debug_objfile != nullptr);
+ gdb_assert (objfile->separate_debug_objfile != nullptr
+ || (objfile->flags & OBJF_DOWNLOAD_DEFERRED) != 0);
else
has_dwarf2 = false;
}
diff --git a/gdb/frame.c b/gdb/frame.c
index 912404cd26a..077be739710 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -756,44 +756,42 @@ skip_tailcall_frames (const frame_info_ptr &initial_frame)
frame. */
static void
-compute_frame_id (const frame_info_ptr &fi)
+compute_frame_id_1 (const frame_info_ptr &fi)
{
FRAME_SCOPED_DEBUG_START_END ("fi=%d", fi->level);
gdb_assert (fi->this_id.p == frame_id_status::NOT_COMPUTED);
- unsigned int entry_generation = get_frame_cache_generation ();
+ scoped_defer_reinit_frame_cache defer_reinit;
- try
- {
- /* Mark this frame's id as "being computed. */
- fi->this_id.p = frame_id_status::COMPUTING;
+ auto restore_not_computed = make_scope_exit ([&] () {
+ fi->this_id.p = frame_id_status::NOT_COMPUTED;
+ });
- /* Find the unwinder. */
- if (fi->unwind == NULL)
- frame_unwind_find_by_frame (fi, &fi->prologue_cache);
+ /* Mark this frame's id as "being computed. */
+ fi->this_id.p = frame_id_status::COMPUTING;
- /* Find THIS frame's ID. */
- /* Default to outermost if no ID is found. */
- fi->this_id.value = outer_frame_id;
- fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
- gdb_assert (frame_id_p (fi->this_id.value));
+ /* Find the unwinder. */
+ if (fi->unwind == NULL)
+ frame_unwind_find_by_frame (fi, &fi->prologue_cache);
- /* Mark this frame's id as "computed". */
- fi->this_id.p = frame_id_status::COMPUTED;
+ /* Find THIS frame's ID. Default to outermost if no ID is
+ found. */
+ fi->this_id.value = outer_frame_id;
+ fi->unwind->this_id (fi, &fi->prologue_cache, &fi->this_id.value);
+ gdb_assert (frame_id_p (fi->this_id.value));
- frame_debug_printf (" -> %s", fi->this_id.value.to_string ().c_str ());
- }
- catch (const gdb_exception &ex)
- {
- /* On error, revert the frame id status to not computed. If the frame
- cache generation changed, the frame object doesn't exist anymore, so
- don't touch it. */
- if (get_frame_cache_generation () == entry_generation)
- fi->this_id.p = frame_id_status::NOT_COMPUTED;
+ /* Mark this frame's id as "computed". */
+ fi->this_id.p = frame_id_status::COMPUTED;
+ restore_not_computed.release ();
- throw;
- }
+ frame_debug_printf (" -> %s", fi->this_id.value.to_string ().c_str ());
+}
+
+static void
+compute_frame_id (const frame_info_ptr &fi)
+{
+ with_protected_frame_cache (compute_frame_id_1, fi);
}
/* Return a frame uniq ID that can be used to, later, re-find the
diff --git a/gdb/objfile-flags.h b/gdb/objfile-flags.h
index 3f362fc5fa5..f5338c45ece 100644
--- a/gdb/objfile-flags.h
+++ b/gdb/objfile-flags.h
@@ -56,6 +56,10 @@ enum objfile_flag : unsigned
/* User requested that we do not read this objfile's symbolic
information. */
OBJF_READNEVER = 1 << 6,
+
+ /* A separate .gdb_index has been downloaded for this objfile.
+ Debuginfo for this objfile can be downloaded when required. */
+ OBJF_DOWNLOAD_DEFERRED = 1 << 7,
};
DEF_ENUM_FLAGS_TYPE (enum objfile_flag, objfile_flags);
diff --git a/gdb/objfiles.h b/gdb/objfiles.h
index 12e77d51b74..7ba30c04cfe 100644
--- a/gdb/objfiles.h
+++ b/gdb/objfiles.h
@@ -629,6 +629,24 @@ struct objfile : intrusive_list_node<objfile>
domain_search_flags domain,
bool *symbol_found_p);
+ /* Used to clear OBJF_DOWNLOAD_DEFERRED status when the debug objfile has
+ either been acquired or could not be found. QF is the
+ quick_symbol_functions entry to remove from the qf list (the
+ deferred index that triggered the download). */
+ void remove_deferred_status (quick_symbol_functions *qf_to_remove)
+ {
+ flags &= ~OBJF_DOWNLOAD_DEFERRED;
+
+ /* Remove the deferred quick_symbol_functions from the qf list.
+ If available the separate debug objfile's index will be used
+ instead, since that objfile actually contains the symbols and CUs
+ referenced in the index. */
+ m_qf.remove_if ([&] (const quick_symbol_functions_up &qf_up)
+ {
+ return qf_up.get () == qf_to_remove;
+ });
+ }
+
/* Return the relocation offset applied to SECTION. */
CORE_ADDR section_offset (bfd_section *section) const
{
diff --git a/gdb/symfile-debug.c b/gdb/symfile-debug.c
index e009821f78c..4e425c1ae79 100644
--- a/gdb/symfile-debug.c
+++ b/gdb/symfile-debug.c
@@ -35,6 +35,7 @@
#include "filenames.h"
#include "build-id.h"
#include "debuginfod-support.h"
+#include "dwarf2/public.h"
/* We need to save a pointer to the real symbol functions.
Plus, the debug versions are malloc'd because we have to NULL out the
@@ -630,6 +631,16 @@ objfile::find_and_add_separate_symbol_file (symfile_add_flags symfile_flags)
= simple_find_and_open_separate_symbol_file
(this, find_separate_debug_file_by_debuglink, &warnings);
+ /* Attempt to download only a '.gdb_index' from the separate
+ debug info. As with the full debuginfo download below, only
+ attempt this once. */
+ if (debug_bfd == nullptr && attempt == 0
+ && dwarf2_has_separate_index (this))
+ {
+ has_dwarf2 = true;
+ break;
+ }
+
/* Only try debuginfod on the first attempt. Sure, we could imagine
an extension that somehow adds the required debug info to the
debuginfod server but, at least for now, we don't support this
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c
new file mode 100644
index 00000000000..5b086cbe336
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-lib.c
@@ -0,0 +1,26 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "deferred-auto-load-py-script.h"
+
+volatile int global_lib_var = 0;
+
+void
+lib_func (void)
+{
+ global_lib_var = 42; /* lib_func breakpoint. */
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h
new file mode 100644
index 00000000000..d6ed8eaeacd
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load-py-script.h
@@ -0,0 +1,41 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "symcat.h"
+#include "gdb/section-scripts.h"
+
+/* Ensure SCRIPT_SUFFIX is defined. */
+#ifndef SCRIPT_SUFFIX
+#error missing SCRIPT_SUFFIX definition
+#endif
+
+/* Create a Python script in a section. */
+#define DEFINE_GDB_SCRIPT_TEXT \
+asm( \
+".pushsection \".debug_gdb_scripts\", \"S\",%progbits\n" \
+".byte " XSTRING (SECTION_SCRIPT_ID_PYTHON_TEXT) "\n" \
+".ascii \"gdb.inlined-script." XSTRING(SCRIPT_SUFFIX) "\\n\"\n" \
+".ascii \"filename = gdb.current_objfile().filename\\n\"\n" \
+".ascii \"if not filename in global_auto_load_tracker:\\n\"\n" \
+".ascii \" global_auto_load_tracker[filename] = 0\\n\"\n" \
+".ascii \"global_auto_load_tracker[filename] += 1\\n\"\n" \
+ ".ascii \"print('deferred-auto-load: script loaded: " XSTRING(SCRIPT_SUFFIX) "')\\n\"\n" \
+".byte 0\n" \
+".popsection\n" \
+);
+
+DEFINE_GDB_SCRIPT_TEXT
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load.c b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.c
new file mode 100644
index 00000000000..2c814d9c6ae
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.c
@@ -0,0 +1,27 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "deferred-auto-load-py-script.h"
+
+extern void lib_func (void);
+
+int
+main ()
+{
+ lib_func ();
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp
new file mode 100644
index 00000000000..23468b0fc3e
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-auto-load.exp
@@ -0,0 +1,223 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test that auto-loaded Python scripts from .debug_gdb_scripts
+# sections in separate debug info files works correctly with on-demand
+# debuginfo downloading.
+#
+# The test embeds a .debug_gdb_scripts section into the separate debug
+# file of both the executable, and a shared library. When the
+# deferred debuginfo is downloaded, GDB should auto-load and execute
+# the embedded script.
+
+standard_testfile .c -lib.c -script.S
+
+load_lib debuginfod-support.exp
+
+require is_elf_target
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+require allow_python_tests
+
+set lib_testfile "lib${testfile}.so"
+set lib_srcfile $srcfile2
+set lib_binfile [standard_output_file $lib_testfile]
+
+# Build the shared library.
+if { [build_executable "build $lib_testfile" $lib_binfile $lib_srcfile \
+ [list debug build-id shlib \
+ additional_flags=-I${srcdir}/../../include \
+ additional_flags=-DSCRIPT_SUFFIX=lib]] != 0 } {
+ return
+}
+
+# Build the main executable.
+if { [build_executable "build executable" $binfile $srcfile \
+ [list debug build-id shlib=$lib_binfile \
+ additional_flags=-I${srcdir}/../../include \
+ additional_flags=-DSCRIPT_SUFFIX=main]] != 0 } {
+ return
+}
+
+# Start GDB, required for ensure_gdb_index call.
+clean_restart
+
+# Add .gdb_index to the shared library.
+if { [ensure_gdb_index $lib_binfile "" $lib_testfile] != 1 } {
+ untested "failed to add .gdb_index to $lib_testfile"
+ return
+}
+
+# Add .gdb_index to the executable.
+if { [ensure_gdb_index $binfile "" $testfile] != 1 } {
+ untested "failed to add .gdb_index to $testfile"
+ return
+}
+
+# Strip debuginfo from shared library into a separate file.
+if { [gdb_gnu_strip_debug $lib_binfile] != 0 } {
+ fail "strip $lib_testfile debuginfo"
+ return
+}
+set lib_debuginfo $lib_binfile.debug
+
+# Strip debuginfo from executable into a separate file.
+if { [gdb_gnu_strip_debug $binfile] != 0 } {
+ fail "strip $testfile debuginfo"
+ return
+}
+set debuginfo $binfile.debug
+
+# Move debuginfo files into the directory that debuginfod will serve.
+set debugdir [standard_output_file "debug"]
+file mkdir $debugdir
+file rename -force $lib_debuginfo $debugdir
+file rename -force $debuginfo $debugdir
+
+# Helper proc to return the filename of the debuginfod cache file
+# containing the full separate debuginfo for FILENAME. CACHE is the
+# absolute directory name of the debuginfod client cache. The
+# resulting filename is calculated based on the build-id of FILENAME,
+# we don't check if the file actually exists in the cache.
+proc debuginfod_cache_file { cache filename } {
+ set buildid [get_build_id $filename]
+ return [file join $cache $buildid debuginfo]
+}
+
+# Test that the auto-loaded script from .debug_gdb_scripts in the
+# separate debug files are executed when the deferred debuginfo is
+# downloaded.
+proc_with_prefix test_auto_load_script { cache } {
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ # Setup a global Python dictionary which will track scripts as
+ # they are auto-loaded. The keys of this dictionary are the
+ # objfile filenames from which scipts were loaded, and the values
+ # are the number of times the script was loaded.
+ gdb_test_no_output "python global_auto_load_tracker = {}"
+
+ # Allow auto-loading from any file relating to this test. This
+ # will allow scripts to be loaded from the executable, the shared
+ # library, and from anything downloaded from debuginfod. We only
+ # expect to load scripts from the debuginfod client cache, but we
+ # want the other things in the safe path in case anything goes
+ # wrong, then we'll get to see GDB loading things from the wrong
+ # place.
+ #
+ # In real life it might be a bad idea to allow scripts downloaded
+ # from debuginfod to be auto-loaded and run, but for this test it
+ # should be fine as we only use a local debuginfod, and we control
+ # the script content.
+ gdb_test_no_output "set auto-load safe-path [standard_output_file {}]" \
+ "set auto-load safe-path"
+
+ gdb_load $::binfile
+
+ if { ![runto_main] } {
+ return
+ }
+
+ # At this point the .gdb_index should have been downloaded for the
+ # shared library, but its full debuginfo should still be deferred,
+ # so the auto-load script within the library should not have been
+ # loaded yet.
+ #
+ # However, the auto-load script for the main executable should
+ # have been loaded as the main executable's full debuginfo will
+ # have been downloaded by now.
+ gdb_test "info auto-load python-scripts" \
+ "Yes\\s+gdb\\.inlined-script\\.main\\s*" \
+ "check auto-loaded python scripts before download"
+ set cache_file [debuginfod_cache_file $cache $::binfile]
+ gdb_test_no_output "python assert(global_auto_load_tracker\[\"$cache_file\"\] == 1)" \
+ "check executable script was loaded once before library has loaded"
+ gdb_test_no_output "python assert(len(global_auto_load_tracker.keys()) == 1)" \
+ "script from just one source has been loaded"
+
+ set lineno [gdb_get_line_number "lib_func breakpoint" $::srcfile2]
+
+ # Set a breakpoint in the library to trigger the deferred
+ # debuginfo download.
+ set saw_script_loaded false
+ set saw_download false
+ set saw_breakpoint false
+ gdb_test_multiple "break lib_func" "break lib_func" {
+ -re "^break lib_func\r\n" {
+ exp_continue
+ }
+
+ -re "^deferred-auto-load: script loaded: lib\r\n" {
+ set saw_script_loaded true
+ exp_continue
+ }
+ -re "^Downloading \[^\r\n\]*separate debug info\[^\r\n\]*\r\n" {
+ set saw_download true
+ exp_continue
+ }
+ -re "^Breakpoint $::decimal at $::hex\[^\r\n\]+\r\n" {
+ set saw_breakpoint true
+ exp_continue
+ }
+ -re "^$::gdb_prompt $" {
+ gdb_assert { $saw_download && $saw_script_loaded && $saw_breakpoint } \
+ $gdb_test_name
+ }
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+
+ # Verify the auto-loaded script appears in the list.
+ gdb_test "info auto-load python-scripts" \
+ [multi_line \
+ "Yes\\s+gdb\\.inlined-script\\.lib\\s*" \
+ "Yes\\s+gdb\\.inlined-script\\.main\\s*"] \
+ "scripts loaded after library debuginfo download"
+
+ # Continue to the breakpoint.
+ gdb_test "continue" "Breakpoint $::decimal, lib_func.*" \
+ "continue to lib_func"
+
+ # We should now have loaded the auto-load script from both the
+ # main executable, and from the shared library. Check each script
+ # has been loaded exactly once.
+ set lib_cache_file [debuginfod_cache_file $cache $::lib_binfile]
+ gdb_test_no_output "python assert(global_auto_load_tracker\[\"$lib_cache_file\"\] == 1)" \
+ "check library script was loaded once"
+ gdb_test_no_output "python assert(global_auto_load_tracker\[\"$cache_file\"\] == 1)" \
+ "check executable script was loaded once"
+ gdb_test_no_output "python assert(len(global_auto_load_tracker.keys()) == 2)" \
+ "scripts from two sources loaded"
+}
+
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ setenv DEBUGINFOD_URLS $url
+ test_auto_load_script $cache
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c b/gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c
new file mode 100644
index 00000000000..93e79c72f86
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-download-lib1.c
@@ -0,0 +1,43 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2023-2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include <stdio.h>
+#include <pthread.h>
+#include <unistd.h>
+
+extern void libdeferred2_test ();
+extern void *libdeferred2_thread_test (void *);
+
+static volatile int flag = 0;
+
+void
+libdeferred1_test ()
+{
+ pthread_t thr;
+
+ printf ("In libdeferred1\n");
+ libdeferred2_test ();
+
+ pthread_create (&thr, NULL, libdeferred2_thread_test, (void *) &flag);
+
+ /* Give the new thread a chance to actually enter libdeferred2_thread_test. */
+ while (!flag)
+ ;
+
+ printf ("Cancelling thread\n");
+ pthread_cancel (thr);
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c b/gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c
new file mode 100644
index 00000000000..97630e8f4ee
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-download-lib2.c
@@ -0,0 +1,37 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2023-2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include <stdio.h>
+#include <pthread.h>
+
+void
+libdeferred2_test ()
+{
+ printf ("In libdeferred2\n");
+}
+
+void *
+libdeferred2_thread_test (void *arg)
+{
+ int *flag = (int *) arg;
+ printf ("In thread test\n");
+
+ while (1)
+ *flag = 1;
+
+ return NULL;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download.c b/gdb/testsuite/gdb.debuginfod/deferred-download.c
new file mode 100644
index 00000000000..6fd34cddd60
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-download.c
@@ -0,0 +1,29 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2023-2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include <stdio.h>
+
+extern void libdeferred1_test ();
+
+int
+main ()
+{
+ libdeferred1_test ();
+ printf ("in deferred exec\n");
+
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-download.exp b/gdb/testsuite/gdb.debuginfod/deferred-download.exp
new file mode 100644
index 00000000000..60dee8f28e5
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-download.exp
@@ -0,0 +1,158 @@
+# Copyright 2023-2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test debuginfod's ability to download .gdb-index section prior to
+# downloading the full debug information file.
+
+standard_testfile .c -lib1.c -lib2.c
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+
+# BINFILE calls a function from LIB_SL1.
+set lib1_testfile "libdeferred1.so"
+set lib1_srcfile $srcfile2
+set lib1_binfile [standard_output_file $lib1_testfile]
+
+# LIB1_BINFILE calls functions from LIB2_BINFILE.
+set lib2_testfile "libdeferred2.so"
+set lib2_srcfile $srcfile3
+set lib2_binfile [standard_output_file $lib2_testfile]
+
+# Build LIB1_BINFILE, LIB2_BINFILE, and BINFILE.
+if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \
+ {debug build-id shlib}] != 0 } {
+ return
+}
+
+if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \
+ [list debug build-id shlib_pthreads shlib=$lib2_binfile]] != 0 } {
+ return
+}
+
+if { [build_executable "build executable" $binfile $srcfile \
+ [list debug build-id shlib=$lib1_binfile shlib=$lib2_binfile]] != 0 } {
+ return
+}
+
+# Start GDB now, we need GDB running in order to check the index types
+# within the compiled executable and shared libraries.
+clean_restart
+
+# Make sure libdeferred1 and libdeferred2 contain .gdb_index.
+if { [ensure_gdb_index $lib1_binfile "" "libdeferred1"] != 1 } {
+ untested "failed to add .gdb_index to $lib1_testfile"
+ return
+}
+
+if { [ensure_gdb_index $lib2_binfile "" "libdeferred2"] != 1 } {
+ untested "failed to add .gdb_index to $lib2_testfile"
+ return
+}
+
+# Strip solib debuginfo into separate files.
+if { [gdb_gnu_strip_debug $lib1_binfile ""] != 0} {
+ fail "strip $lib1_testfile debuginfo"
+ return
+}
+
+if { [gdb_gnu_strip_debug $lib2_binfile ""] != 0} {
+ fail "strip $lib2_testfile debuginfo"
+ return
+}
+
+# Move debuginfo files into directory that debuginfod will serve from.
+set debugdir [standard_output_file "debug"]
+set debuginfo_sl1 [standard_output_file $lib1_testfile.debug]
+set debuginfo_sl2 [standard_output_file $lib2_testfile.debug]
+
+file mkdir $debugdir
+file rename -force $debuginfo_sl1 $debugdir
+file rename -force $debuginfo_sl2 $debugdir
+
+# Restart GDB and clear the debuginfod client cache. Then load BINFILE into
+# GDB and start running it. Match output with pattern RES and use TESTNAME
+# as the test name.
+proc_with_prefix clean_restart_with_prompt { binfile testname } {
+ global cache
+
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on" \
+ "clean_restart enable $testname"
+ gdb_test_no_output "set progress-bars enabled off" \
+ "disable progress bars $testname"
+ gdb_load $binfile
+
+ runto_main
+}
+
+# Tests with no debuginfod server running.
+proc_with_prefix no_url { } {
+ gdb_load $::binfile
+ if {![runto_main]} {
+ return
+ }
+
+ # Check that no section is downloaded and no debuginfo is found.
+ gdb_test "info sharedlibrary" ".*Yes \\(\\*\\).*$::lib1_testfile.*" \
+ "found no url lib1"
+ gdb_test "info sharedlibrary" ".*Yes \\(\\*\\).*$::lib2_testfile.*" \
+ "found no url lib2"
+}
+
+# Tests with a debuginfod server running.
+proc_with_prefix local_url { } {
+ global debugdir db
+
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ # Point GDB to the server.
+ setenv DEBUGINFOD_URLS $url
+
+ clean_restart_with_prompt $::binfile "index"
+
+ # Download debuginfo when stepping into a function.
+ set res ".*separate debug info for [string_to_regexp $::lib1_binfile].*\"In libdeferred1\\\\n\".*"
+ gdb_test "step" $res "step"
+
+ clean_restart_with_prompt $::binfile "break"
+
+ # Download debuginfo when setting a breakpoint.
+ set res ".*separate debug info for [string_to_regexp $::lib2_binfile].*"
+ gdb_test "br libdeferred2_test" $res "break set"
+
+ # Hit the breakpoint.
+ set res ".*Breakpoint 2, libdeferred2_test.*\"In libdeferred2\\\\n\".*"
+ gdb_test "c" $res "break continue"
+}
+
+# Create CACHE and DB directories ready for debuginfod to use.
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ no_url
+ local_url
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h b/gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h
new file mode 100644
index 00000000000..89ba734fa7a
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz-common.h
@@ -0,0 +1,41 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* Common type shared between the two libraries. Having a shared type
+ that appears in both libraries gives the DWZ tool something to
+ extract into the common DWZ file. */
+
+struct common_type
+{
+ int x;
+ int y;
+};
+
+/* A common function used in both libraries. Being defined in a
+ header file means its debug info will appear in both libraries'
+ DWARF, giving DWZ something to deduplicate. We must force this
+ function to be inlined though, as it is the abstact instance of
+ this function that will be deduplicate, not the concrete instance.
+ If we don't force the function inline, then all we get is a
+ non-inline instance within each library, and the DWARF for these
+ two instances will not be moved into the DWZ file. */
+
+static inline int __attribute__((always_inline))
+common_add (struct common_type *ct)
+{
+ return ct->x + ct->y;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c
new file mode 100644
index 00000000000..7ebc153a99b
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib1.c
@@ -0,0 +1,24 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "deferred-dwz-common.h"
+
+int
+lib1_func (struct common_type *ct)
+{
+ return common_add (ct) + 1; /* lib1_func breakpoint. */
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c
new file mode 100644
index 00000000000..748476b33ba
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz-lib2.c
@@ -0,0 +1,24 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "deferred-dwz-common.h"
+
+int
+lib2_func (struct common_type *ct)
+{
+ return common_add (ct) + 2; /* lib2_func breakpoint. */
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz.c b/gdb/testsuite/gdb.debuginfod/deferred-dwz.c
new file mode 100644
index 00000000000..0d144fe937e
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz.c
@@ -0,0 +1,30 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "deferred-dwz-common.h"
+
+extern int lib1_func (struct common_type *ct);
+extern int lib2_func (struct common_type *ct);
+
+int
+main ()
+{
+ struct common_type ct = { 10, 20 };
+ int r1 = lib1_func (&ct);
+ int r2 = lib2_func (&ct);
+ return r1 + r2;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-dwz.exp b/gdb/testsuite/gdb.debuginfod/deferred-dwz.exp
new file mode 100644
index 00000000000..8b6198acb21
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-dwz.exp
@@ -0,0 +1,247 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test on-demand debuginfo downloading when the separate debug files use the
+# DWZ tool to share common debug info.
+#
+# Two shared libraries are compiled with a common header that provides a
+# shared type and an inline function. The debug info is stripped into
+# separate files and then 'dwz -m' is used to extract common DWARF into a
+# DWZ file.
+#
+# This test is checking that having a DWZ file in the mix doesn't cause
+# problems for the deferred debug information downloading.
+
+standard_testfile .c -lib1.c -lib2.c
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+require {dwz_version_at_least 0.13}
+
+set lib1_testfile "libdeferred-dwz1.so"
+set lib1_srcfile $srcfile2
+set lib1_binfile [standard_output_file $lib1_testfile]
+
+set lib2_testfile "libdeferred-dwz2.so"
+set lib2_srcfile $srcfile3
+set lib2_binfile [standard_output_file $lib2_testfile]
+
+# Build the two shared libraries.
+if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \
+ {debug build-id shlib}] != 0 } {
+ return
+}
+
+if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \
+ {debug build-id shlib}] != 0 } {
+ return
+}
+
+# Build the main executable.
+if { [build_executable "build executable" $binfile $srcfile \
+ [list debug build-id \
+ shlib=$lib1_binfile shlib=$lib2_binfile]] != 0 } {
+ return
+}
+
+# If we are running with a board that splits the debug information out and
+# adds a .gnu_debuglink, then this test isn't going to work. We want to be
+# in control of splitting out the debug information.
+if {[section_get $lib1_binfile ".gnu_debuglink"] ne ""} {
+ unsupported "debug information has already been split out"
+ return
+}
+
+# Start GDB, required for ensure_gdb_index calls.
+clean_restart
+
+# Add .gdb_index to both libraries and the executable.
+if { [ensure_gdb_index $lib1_binfile "" $lib1_testfile] != 1 } {
+ untested "failed to add .gdb_index to $lib1_testfile"
+ return
+}
+
+if { [ensure_gdb_index $lib2_binfile "" $lib2_testfile] != 1 } {
+ untested "failed to add .gdb_index to $lib2_testfile"
+ return
+}
+
+if { [ensure_gdb_index $binfile "" $testfile] != 1 } {
+ untested "failed to add .gdb_index to $testfile"
+ return
+}
+
+# Strip debuginfo into separate files. Use no-debuglink so that debuginfod
+# is the only way to find the debug info.
+if { [gdb_gnu_strip_debug $lib1_binfile no-debuglink] != 0 } {
+ fail "strip $lib1_testfile debuginfo"
+ return
+}
+
+if { [gdb_gnu_strip_debug $lib2_binfile no-debuglink] != 0 } {
+ fail "strip $lib2_testfile debuginfo"
+ return
+}
+
+if { [gdb_gnu_strip_debug $binfile no-debuglink] != 0 } {
+ fail "strip $testfile debuginfo"
+ return
+}
+
+# Move the .debug files into the debug directory.
+set debugdir [standard_output_file "debug"]
+file mkdir $debugdir
+file rename -force $lib1_binfile.debug $debugdir
+file rename -force $lib2_binfile.debug $debugdir
+file rename -force $binfile.debug $debugdir
+
+# Run dwz -m on the separate debug files to extract common DWARF into a
+# shared DWZ file. Use relative filenames so that the .gnu_debugaltlink in
+# the .debug files contains a relative path; GDB will need debuginfod to
+# find the DWZ file.
+with_cwd $debugdir {
+ set status \
+ [remote_exec build "dwz -m ./common.dwz \
+ ./$lib1_testfile.debug \
+ ./$lib2_testfile.debug"]
+ if {[lindex $status 0] != 0} {
+ unsupported "unable to run dwz tool"
+ return
+ }
+}
+
+# Some versions of dwz exit with code 0 but print error output.
+if {[lindex $status 1] ne ""} {
+ unsupported "unexpected output from dwz tool"
+ return
+}
+
+# Verify the DWZ file was created.
+if { ![file exists $debugdir/common.dwz] } {
+ unsupported "dwz did not produce common.dwz"
+ return
+}
+
+# The build-id for the first shared library. When the full debug
+# information for this library is downloaded, we will also download the DWZ
+# file. This build-id will appear in the 'Downloading ...' line, and we use
+# this to check the line is seen.
+set lib1_buildid [get_build_id $lib1_binfile]
+
+# Test that on-demand debuginfo downloading works correctly when the
+# separate debug files reference a DWZ common file.
+proc_with_prefix test_deferred_dwz { cache } {
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ gdb_load $::binfile
+
+ if { ![runto_main] } {
+ return
+ }
+
+ # At this point the .gdb_index should have been downloaded for both
+ # shared libraries, and the full debuginfo downloads should be deferred.
+
+ # Set a breakpoint in lib1. This should trigger the full debuginfo
+ # download for lib1, which includes downloading the DWZ common file.
+ set saw_lib_debuginfo_download false
+ set saw_dwz_download false
+ set saw_breakpoint false
+ set saw_other_download false
+ gdb_test_multiple "break lib1_func" "" {
+ -re "^Downloading \[^\r\n\]*separate debug info for [string_to_regexp $::lib1_binfile]\\.\\.\\.\r\n" {
+ set saw_lib_debuginfo_download true
+ exp_continue
+ }
+ -re "^Downloading \[^\r\n\]*separate debug info for \[^\r\n\]*/$::lib1_buildid/debuginfo\\.\\.\\.\r\n" {
+ set saw_dwz_download true
+ exp_continue
+ }
+ -re "^Downloading \[^\r\n\]*\r\n" {
+ set saw_other_download true
+ exp_continue
+ }
+ -re "^Breakpoint $::decimal at $::hex: \[^\r\n\]+\r\n" {
+ set saw_breakpoint true
+ exp_continue
+ }
+ -re "^$::gdb_prompt " {
+ gdb_assert { $saw_lib_debuginfo_download && $saw_dwz_download \
+ && $saw_breakpoint && !$saw_other_download } \
+ $gdb_test_name
+ }
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+
+ # Continue to the breakpoint.
+ gdb_continue_to_breakpoint "breakpoint in lib1"
+
+ # Set a breakpoint in lib2. This should trigger the full debuginfo
+ # download for lib2. The DWZ common file should already be available
+ # from the earlier download.
+ set saw_lib_debuginfo_download false
+ set saw_breakpoint false
+ set saw_other_download false
+ gdb_test_multiple "break lib2_func" "" {
+ -re "^Downloading \[^\r\n\]*separate debug info for [string_to_regexp $::lib2_binfile]\\.\\.\\.\r\n" {
+ set saw_lib_debuginfo_download true
+ exp_continue
+ }
+ -re "^Downloading \[^\r\n\]*\r\n" {
+ set saw_other_download true
+ exp_continue
+ }
+ -re "^Breakpoint $::decimal at $::hex: \[^\r\n\]+\r\n" {
+ set saw_breakpoint true
+ exp_continue
+ }
+ -re "^$::gdb_prompt " {
+ gdb_assert { $saw_lib_debuginfo_download && $saw_breakpoint \
+ && !$saw_other_download } $gdb_test_name
+ }
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+
+ # Continue to the breakpoint.
+ gdb_continue_to_breakpoint "breakpoint in lib2"
+}
+
+# Create CACHE and DB directories ready for debuginfod to use.
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ setenv DEBUGINFOD_URLS $url
+ test_deferred_dwz $cache
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c
new file mode 100644
index 00000000000..f92f55136cf
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning-lib.c
@@ -0,0 +1,36 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+extern void libdeferred_expand_warning_test (void);
+
+static volatile int global_var = 0;
+
+/* A function that the Python observer will look up to trigger symtab
+ expansion. */
+
+void
+libdeferred_expand_warning_func (void)
+{
+ ++global_var;
+}
+
+void
+libdeferred_expand_warning_test (void)
+{
+ ++global_var; /* Library entry point. */
+ libdeferred_expand_warning_func ();
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c
new file mode 100644
index 00000000000..c7a988cfa94
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.c
@@ -0,0 +1,25 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+extern void libdeferred_expand_warning_test (void);
+
+int
+main (void)
+{
+ libdeferred_expand_warning_test ();
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp
new file mode 100644
index 00000000000..b7607e181a6
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.exp
@@ -0,0 +1,177 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test that on-demand debuginfo downloading handles the case where a
+# new_objfile observer triggers symtab expansion in the newly-downloaded
+# separate debug objfile.
+#
+# The code in GDB that causes the full debug information to be
+# downloaded has two steps. First we check already expanded symtabs,
+# then if no result is found, we check the unexpanded symtabs, and
+# potentially expand them.
+#
+# Full debug information download only happens during the second
+# phase, when a match is found in the index. After finding a match in
+# the index GDB will give a warning if the symtab is already expanded.
+#
+# But with deferred debug information download, this is OK, as a new
+# objfile observer, triggered when the full debug information is
+# downloaded, might force symtab expansion, so we suppress the warning
+# in this case.
+#
+# This test replicates this setup and checks that the warning doesn't
+# trigger.
+
+standard_testfile .c -lib.c
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+
+set lib_testfile "lib${testfile}.so"
+set lib_srcfile $srcfile2
+set lib_binfile [standard_output_file $lib_testfile]
+set pyscript $srcdir/$subdir/$testfile.py
+
+# Build the shared library.
+if { [build_executable "build $lib_testfile" $lib_testfile $lib_srcfile \
+ {debug build-id shlib}] != 0 } {
+ return
+}
+
+# Build the main executable.
+if { [build_executable "build executable" $testfile $srcfile \
+ [list debug build-id shlib=$lib_binfile]] != 0 } {
+ return
+}
+
+# Start GDB, required for ensure_gdb_index call.
+clean_restart
+
+# Add .gdb_index to the shared library.
+if { [ensure_gdb_index $lib_binfile "" $lib_testfile] != 1 } {
+ untested "failed to add .gdb_index to $lib_testfile"
+ return
+}
+
+# Strip debuginfo into a separate file.
+if { [gdb_gnu_strip_debug $lib_binfile] != 0 } {
+ fail "strip $lib_testfile debuginfo"
+ return
+}
+
+# Move debuginfo file into directory that debuginfod will serve.
+set debugdir [standard_output_file "debug"]
+set lib_debuginfo [standard_output_file $lib_testfile.debug]
+
+file mkdir $debugdir
+file rename -force $lib_debuginfo $debugdir
+
+# Test that stepping into the library triggers debuginfo download and
+# the observer runs without causing an "Internal error".
+proc_with_prefix test_observer_symtab_expansion { } {
+ global debugdir db
+
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ # Point GDB to the server.
+ setenv DEBUGINFOD_URLS $url
+
+ # Start GDB.
+ clean_restart $::testfile
+
+ # Enable debuginfod.
+ gdb_test_no_output "set debuginfod enabled on" \
+ "enable debuginfod"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ # Load the Python script that registers the new_objfile observer.
+ gdb_test "source $::pyscript" \
+ "Python observer: registered new_objfile handler" \
+ "load python script"
+
+ # Run to main. This will download the .gdb_index for the shared
+ # library, but there should be no need, at this point, to download
+ # the full debug information.
+ if {![runto_main]} {
+ return
+ }
+
+ set lineno [gdb_get_line_number "Library entry point" $::srcfile2]
+ verbose -log "APB: Line $lineno"
+
+ # Step into the library function. This should trigger the
+ # download of the full debug info. Downloading the full debug
+ # information adds a new objfile, which triggers the new objfile
+ # observer from our Python script. The new objfile observer
+ # performs a symbol lookup within the debug information which was
+ # just downloaded. If this is not handled correctly by GDB then
+ # there is the possibility that an internal error could be
+ # triggered.
+ set saw_observer false
+ set saw_error false
+ set saw_download false
+ set saw_location_line false
+ set found_symbol false
+ gdb_test_multiple "step" "step into library" {
+ -re "^step\r\n" {
+ exp_continue
+ }
+ -re "^Downloading.*separate debug info for \[^\r\n\]+/$::lib_testfile\\.\\.\\.\r\n" {
+ set saw_download true
+ exp_continue
+ }
+ -re "^Python observer: triggering symtab expansion for \[^\r\n\]+\r\n" {
+ set saw_observer 1
+ exp_continue
+ }
+ -re "^Python observer: found symbol \[^\r\n\]+\r\n" {
+ set found_symbol true
+ exp_continue
+ }
+ -re "^libdeferred_expand_warning_test \\(\\) at \[^\r\n\]+/$::srcfile2:$lineno\r\n" {
+ set saw_location_line true
+ # We don't have a specific pattern for the source code
+ # line which follows this one. That's not really relevant
+ # to this test.
+ exp_continue
+ }
+ -re "^\[^\r\n\]*Internal error\[^\r\n\]*\r\n" {
+ set saw_error 1
+ exp_continue
+ }
+ -re "^$::gdb_prompt $" {
+ gdb_assert { !$saw_error && $saw_observer && $saw_download && $saw_location_line && $found_symbol } \
+ $gdb_test_name
+ }
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+}
+
+# Create CACHE and DB directories ready for debuginfod to use.
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ test_observer_symtab_expansion
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py
new file mode 100644
index 00000000000..eedccabc160
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-expand-warning.py
@@ -0,0 +1,61 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# This script registers a new_objfile event handler that triggers
+# symtab expansion by looking up a symbol. This is used to test
+# that on-demand debuginfo downloading correctly handles the case
+# where an observer expands symtabs in the newly-downloaded objfile.
+
+import gdb
+
+
+def new_objfile_handler(event):
+ """Handler for new_objfile events.
+
+ When a new objfile is loaded, check if it's the separate debug
+ objfile for our test library. If so, look up a symbol to trigger
+ symtab expansion. See the .exp file for why we do this.
+ """
+ objfile = event.new_objfile
+
+ # Only trigger for separate debug objfiles (those with an owner).
+ if objfile.owner is None:
+ return
+
+ # Check if this is the separate debug objfile for our test library.
+ # The owner's filename should contain our library name.
+ owner_filename = objfile.owner.filename
+ if owner_filename is None:
+ return
+
+ if "libdeferred-expand-warning" not in owner_filename:
+ return
+
+ print(
+ "Python observer: triggering symtab expansion for {}".format(objfile.filename)
+ )
+
+ # Look up a global symbol from this objfile to trigger symtab
+ # expansion.
+ sym = gdb.lookup_global_symbol("libdeferred_expand_warning_func")
+ if sym is not None:
+ print("Python observer: found symbol {}".format(sym.name))
+ else:
+ raise RuntimeError("No symbol found")
+
+
+# Register the new objfile event handler.
+gdb.events.new_objfile.connect(new_objfile_handler)
+print("Python observer: registered new_objfile handler")
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c
new file mode 100644
index 00000000000..4d4671596cf
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache-lib.c
@@ -0,0 +1,25 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* Library function that causes a crash (SIGSEGV). */
+
+void
+crash_in_lib (void)
+{
+ volatile int *p = (volatile int *) 0;
+ *p = 42; /* This will crash. */
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c
new file mode 100644
index 00000000000..0331f65dd2d
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.c
@@ -0,0 +1,28 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* Test program that crashes in a shared library to trigger debuginfo
+ download during crash handling. */
+
+extern void crash_in_lib (void);
+
+int
+main (void)
+{
+ crash_in_lib ();
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp
new file mode 100644
index 00000000000..e44f80b8c25
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.exp
@@ -0,0 +1,180 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test that frame cache invalidation during debuginfo download is handled.
+#
+# When on-demand debuginfo downloading triggers, a new objfile is added
+# and new_objfile observers are notified. If an observer calls
+# gdb.invalidate_cached_frames(), the frame cache is cleared.
+#
+# GDB's lookup_selected_frame function handles this by retrying the frame
+# lookup if selected_frame is nullptr after the initial lookup attempt.
+# This retry mechanism ensures GDB doesn't hit an assertion failure in
+# get_selected_frame() when an observer clears the frame cache during
+# debuginfo download.
+#
+# This test creates a program that crashes in a shared library with
+# deferred debuginfo, then uses a Python new_objfile observer to
+# invalidate the frame cache when the debuginfo is downloaded. This
+# exercises the frame cache invalidation code path and verifies GDB
+# handles it gracefully.
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+
+standard_testfile .c -lib.c
+
+set lib_testfile "lib-${testfile}.so"
+set lib_srcfile $srcfile2
+set lib_binfile [standard_output_file $lib_testfile]
+
+# Build the library and executable.
+if { [build_executable "build $lib_testfile" $lib_binfile $lib_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+if { [build_executable "build executable" $binfile $srcfile \
+ [list debug build-id shlib=$lib_binfile]] != 0 } {
+ return
+}
+
+clean_restart
+
+# Add .gdb_index to the library.
+if { [ensure_gdb_index $lib_binfile "" "$lib_testfile"] != 1 } {
+ untested "failed to add .gdb_index to $lib_testfile"
+ return
+}
+
+# Strip debuginfo into a separate file.
+if { [gdb_gnu_strip_debug $lib_binfile ""] != 0 } {
+ fail "strip $lib_testfile debuginfo"
+ return
+}
+
+# Move debuginfo file into directory that debuginfod will serve.
+set debugdir [standard_output_file "debug"]
+set debuginfo_sl [standard_output_file $lib_testfile.debug]
+
+file mkdir $debugdir
+file rename -force $debuginfo_sl $debugdir
+
+# Python script that invalidates the frame cache when the separate debug
+# objfile is loaded.
+set pyscript [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Test that frame lookup handles cache invalidation when loading a core file.
+#
+# When loading a core file, GDB needs to establish the current frame
+# immediately. This triggers debuginfo download during lookup_selected_frame,
+# and if an observer invalidates the frame cache, the retry mechanism is
+# needed to avoid an assertion failure.
+proc_with_prefix test_frame_cache_core { } {
+ global cache
+
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+
+ # Run the program outside of GDB to generate a core file.
+ set corefile [core_find $::binfile]
+ if {$corefile == ""} {
+ untested "could not generate core file"
+ return
+ }
+
+ # Now start GDB fresh without the executable loaded yet. We want
+ # the Python script (and its new_objfile observer) in place before
+ # we start loading anything.
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ # Load the Python script BEFORE loading anything else. This
+ # ensures the observer is registered when the core file triggers
+ # debuginfo download.
+ gdb_test "source $::pyscript" "Python registered new_objfile handler" \
+ "load python script"
+
+ # Load the executable.
+ gdb_load $::binfile
+
+ # Now load the core file. This will:
+ # 1. GDB needs to establish the current frame from the core.
+ # 2. This triggers lookup_selected_frame.
+ # 3. select_frame calls find_compunit_symtab_for_pc.
+ # 4. This triggers debuginfo download for the library.
+ # 5. The new_objfile handler invalidates the frame cache.
+ # 6. Without the fix, get_selected_frame() hits an assertion
+ set saw_invalidate false
+ set saw_error false
+ set saw_prog_terminated false
+ set saw_crash_frame false
+ set saw_debug_download false
+ gdb_test_multiple "core-file $corefile" "load core file" {
+ -re "^Python new_objfile handler: frame cache invalidated\r\n" {
+ set saw_invalidate true
+ exp_continue
+ }
+ -re "^\[^\r\n\]*Internal error\[^\r\n\]*\r\n" {
+ set saw_error true
+ exp_continue
+ }
+ -re "^\[^\r\n\]*Assertion\[^\r\n\]*failed\[^\r\n\]*\r\n" {
+ set saw_error true
+ exp_continue
+ }
+ -re "^#0\\s+\[^\r\n\]*crash_in_lib \\(\\) at \[^\r\n\]+\r\n" {
+ set saw_crash_frame true
+ exp_continue
+ }
+ -re "^Program terminated with signal SIG\[^\r\n\]+\r\n" {
+ set saw_prog_terminated true
+ exp_continue
+ }
+ -re "^Downloading \[^\r\n\]*separate debug info for \[^\r\n\]*/$::lib_testfile\\.\\.\\.\r\n" {
+ set saw_debug_download true
+ exp_continue
+ }
+ -re "^$::gdb_prompt $" {
+ gdb_assert { !$saw_error \
+ && $saw_invalidate \
+ && $saw_prog_terminated \
+ && $saw_crash_frame } \
+ $gdb_test_name
+ }
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+}
+
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ setenv DEBUGINFOD_URLS $url
+ test_frame_cache_core
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py
new file mode 100644
index 00000000000..e58285e279f
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-frame-cache.py
@@ -0,0 +1,68 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Python script that invalidates the frame cache when the separate
+# debug objfile for lib-deferred-frame-cache.so is loaded. This
+# simulates an observer that needs to reset frame-related state when
+# new debug info arrives.
+
+import gdb
+
+# Guard to only invalidate once.
+_already_invalidated = False
+
+def new_objfile_handler(event):
+ """Called when a new objfile is loaded.
+
+ When the separate debug objfile for our library is loaded,
+ invalidate the frame cache. This happens during the crash
+ handling when GDB is trying to establish the current frame.
+ If lookup_selected_frame doesn't handle this correctly, GDB
+ will hit an assertion failure.
+ """
+ global _already_invalidated
+
+ if _already_invalidated:
+ return
+
+ objfile = event.new_objfile
+
+ # Only interested in the separate debug objfile (has an owner).
+ if objfile.owner is None:
+ return
+
+ owner_filename = objfile.owner.filename
+ if owner_filename is None or "lib-deferred-frame-cache.so" not in owner_filename:
+ return
+
+ _already_invalidated = True
+
+ print("Python new_objfile handler: invalidating frame cache")
+
+ # This calls reinit_frame_cache() which clears selected_frame.
+ # We're inside the crash handling call chain where GDB is trying
+ # to establish the current frame. The call sequence is:
+ # handle_inferior_event -> ... -> get_selected_frame()
+ # -> lookup_selected_frame() -> select_frame()
+ # -> find_compunit_symtab_for_pc() -> download -> here
+ #
+ # After this, selected_frame is nullptr. Without the fix in
+ # lookup_selected_frame, get_selected_frame would hit an assertion.
+ gdb.invalidate_cached_frames()
+
+ print("Python new_objfile handler: frame cache invalidated")
+
+gdb.events.new_objfile.connect(new_objfile_handler)
+print("Python registered new_objfile handler")
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c
new file mode 100644
index 00000000000..7d13adf7846
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo-lib.c
@@ -0,0 +1,22 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+int
+lib_func (int x)
+{
+ return x + 1;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c
new file mode 100644
index 00000000000..9d02f809557
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.c
@@ -0,0 +1,25 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+extern int lib_func (int x);
+
+int
+main (void)
+{
+ int res = lib_func (42);
+ return res;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp
new file mode 100644
index 00000000000..bd209a9027d
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-no-debuginfo.exp
@@ -0,0 +1,194 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test on-demand debuginfo downloading when debuginfod can serve a
+# .gdb_index section but the corresponding full debuginfo has had its
+# DWARF sections stripped.
+#
+# This tests the edge case where the .gdb_index download succeeds
+# (so GDB defers the full debuginfo download) but the full debuginfo
+# file doesn't contain any useful debug information. GDB should
+# handle this gracefully without crashing or asserting.
+#
+# This is probably as close as we can easily get to a debuginfod
+# server that serves a .gdb_index, but then fails to serve the full
+# debug information.
+
+standard_testfile .c -lib.c
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+
+set lib_testfile "lib${testfile}.so"
+set lib_srcfile $srcfile2
+set lib_binfile [standard_output_file $lib_testfile]
+
+# Build the shared library and executable.
+if { [build_executable "build $lib_testfile" $lib_binfile $lib_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+if { [build_executable "build executable" $binfile $srcfile \
+ [list debug build-id shlib=$lib_binfile]] } {
+ return
+}
+
+# Start GDB for the ensure_gdb_index call.
+clean_restart
+
+# Add .gdb_index to the shared library.
+if { [ensure_gdb_index $lib_binfile "" $lib_testfile] != 1 } {
+ untested "failed to add .gdb_index to $lib_testfile"
+ return
+}
+
+# Strip debuginfo into a separate file.
+if { [gdb_gnu_strip_debug $lib_binfile] != 0 } {
+ fail "strip $lib_testfile debuginfo"
+ return
+}
+
+# Now strip all DWARF debug sections from the separate debug file,
+# leaving only the .gdb_index. This simulates the case where
+# debuginfod can serve the index but the full debuginfo is not
+# actually available.
+set debuginfo_file [standard_output_file $lib_testfile.debug]
+
+set dwarf_sections {info abbrev line str line_str aranges ranges rnglists \
+ macro loc loclists frame addr str_offsets types }
+
+set objcopy_args {}
+foreach section $dwarf_sections {
+ lappend objcopy_args --remove-section=.debug_${section}
+ lappend objcopy_args --remove-section=.zdebug_${section}
+ lappend objcopy_args --remove-section=.debug_${section}.dwo
+ lappend objcopy_args --remove-section=.zdebug_${section}.dwo
+}
+
+set objcopy_program [gdb_find_objcopy]
+set result [catch {exec $objcopy_program {*}$objcopy_args $debuginfo_file} output]
+if { $result != 0 } {
+ fail "strip DWARF sections from $debuginfo_file: $output"
+ return
+}
+
+# Verify .gdb_index is still present but .debug_info is gone.
+set readelf_program [gdb_find_readelf]
+set result [catch {exec $readelf_program -S $debuginfo_file} output]
+if { $result != 0 } {
+ unresolved "readelf on $debuginfo_file: $output"
+ return
+}
+
+if { [string first ".gdb_index" $output] == -1 } {
+ unresolved ".gdb_index missing from stripped debuginfo"
+ return
+}
+
+# Final sanity check, make sure we cannot find .debug_info (or one of
+# its variants) within DEBUGINFO_FILE.
+foreach section { .debug_info .zdebug_info .debug_info.dwo .zdebug_info.dwo } {
+ if { [string first $section $output] != -1 } {
+ unresolved "$section still present in stripped debuginfo"
+ return
+ }
+}
+
+# Move the debuginfo file into the directory that debuginfod will
+# serve from.
+set debugdir [standard_output_file "debug"]
+file mkdir $debugdir
+file rename -force $debuginfo_file $debugdir
+
+# Test that GDB handles the missing DWARF gracefully. When
+# EARLY_SHUTDOWN is true terminate debuginfod after downloading the
+# .gdb_index, but before triggering download of the full debug
+# information.
+proc_with_prefix test_no_debuginfo { early_shutdown } {
+ global cache
+
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ gdb_load $::binfile
+
+ if { ![runto_main] } {
+ return
+ }
+
+ # At this point the .gdb_index should have been downloaded for the
+ # shared library, and the full debuginfo download should still be
+ # deferred.
+
+ if { $early_shutdown } {
+ # Shutdown debuginfod. This proc doesn't actually wait for
+ # the shutdown to complete, so give the process a couple of
+ # seconds. This isn't perfect, but should be good enough to
+ # surface any bugs.
+ #
+ # We don't want to perform a blocking wait as doing so risks
+ # deadlocking the testsuite if debuginfod misbehaves. And,
+ # having closed the process, we can no longer 'expect' on its
+ # output to watch it shutdown.
+ #
+ # Even if we could be sure that debuginfod has shut down, then
+ # we cannot guarantee that a parallel test run isn't going to
+ # start another debuginfod server in its place.
+ #
+ # At the end of the day, we're just hoping that if this does
+ # trigger a bug someone will think to look into it.
+ stop_debuginfod
+ sleep 2
+ }
+
+ # Try to set a breakpoint on lib_func. This should trigger the
+ # full debuginfo download. The downloaded file won't contain
+ # DWARF, so GDB won't get debug info, but it should not crash.
+ gdb_breakpoint "lib_func" -message
+
+ # Continue to the breakpoint to verify the inferior still works.
+ # Also, as we are missing debug information for the shared library
+ # containing this breakpoint, the breakpoint will be attributed to
+ # the .so file rather than the original .c source file.
+ gdb_continue_to_breakpoint "run to breakpoint" \
+ "lib_func \\(\\) from \[^\r\n\]+/${::lib_testfile}"
+}
+
+# Create CACHE and DB directories ready for debuginfod to use.
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ foreach_with_prefix early_shutdown { false true } {
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ setenv DEBUGINFOD_URLS $url
+
+ test_no_debuginfo $early_shutdown
+
+ stop_debuginfod
+ }
+}
+
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp
new file mode 100644
index 00000000000..c6632c65b51
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.exp
@@ -0,0 +1,161 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test that full symbol info is available to new_objfile observers.
+#
+# When on-demand debuginfo downloading completes, the new_objfile observer
+# is notified about the separate debug objfile. This test verifies that
+# observers can access full symbol and type information at this point.
+#
+# The test registers a Python observer that looks up a symbol's type
+# information when the separate debug objfile is loaded.
+
+standard_testfile deferred-download.c \
+ deferred-download-lib1.c \
+ deferred-download-lib2.c
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+
+# BINFILE calls a function from LIB1_BINFILE.
+set lib1_testfile "libdeferred1.so"
+set lib1_srcfile $srcfile2
+set lib1_binfile [standard_output_file $lib1_testfile]
+
+# LIB1_BINFILE calls functions from LIB2_BINFILE.
+set lib2_testfile "libdeferred2.so"
+set lib2_srcfile $srcfile3
+set lib2_binfile [standard_output_file $lib2_testfile]
+
+# Build LIB1_BINFILE, LIB2_BINFILE, and BINFILE.
+if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \
+ [list debug build-id shlib_pthreads shlib=$lib2_binfile]] } {
+ return
+}
+
+if { [build_executable "build executable" $binfile $srcfile \
+ [list debug build-id shlib=$lib1_binfile shlib=$lib2_binfile]] } {
+ return
+}
+
+# Start GDB now, we need GDB running in order to check the index types
+# within the compiled executable and shared libraries.
+clean_restart
+
+# Add .gdb_index to the library.
+if { [ensure_gdb_index $lib1_binfile "" "libdeferred1"] != 1 } {
+ untested "failed to add .gdb_index to $lib1_testfile"
+ return
+}
+
+# Strip debuginfo into a separate file.
+if { [gdb_gnu_strip_debug $lib1_binfile ""] != 0 } {
+ fail "strip $lib1_testfile debuginfo"
+ return
+}
+
+# Move debuginfo file into a directory that debuginfod will serve.
+set debugdir [standard_output_file "debug"]
+set debuginfo_sl1 [standard_output_file $lib1_testfile.debug]
+
+file mkdir $debugdir
+file rename -force $debuginfo_sl1 $debugdir
+
+# Python script that looks up type information from a new_objfile
+# observer.
+set pyscript [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Test that symbol type info is available in the observer.
+proc_with_prefix test_deferred_status { } {
+ global cache
+
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ # Load the Python script.
+ gdb_test "source $::pyscript" "Python observer registered" \
+ "load python script"
+
+ gdb_load $::binfile
+
+ if { ![runto_main] } {
+ return
+ }
+
+ # Step into the library to trigger debuginfo download.
+ # The observer should see full type info.
+ set saw_void_type false
+ set saw_int_type false
+ set saw_error false
+ set saw_lib_func false
+ set saw_dbg_dl false
+ gdb_test_multiple "step" "step into library" {
+ -re "^Python observer: found type void \\(\\)\r\n" {
+ set saw_void_type true
+ exp_continue
+ }
+ -re "^Python observer: found type volatile int\r\n" {
+ set saw_int_type true
+ exp_continue
+ }
+ -re "^Python observer: ERROR\[^\r\n\]+" {
+ set saw_error true
+ exp_continue
+ }
+ -re "^libdeferred1_test \\(\\) at \[^\r\n\]+\r\n" {
+ set saw_lib_func true
+ exp_continue
+ }
+ -re "^Downloading \[^\r\n\]*separate debug info for \[^\r\n\]+${::lib1_testfile}\\.\\.\\.\r\n" {
+ set saw_dbg_dl true
+ exp_continue
+ }
+ -re "^$::gdb_prompt $" {
+ gdb_assert { $saw_void_type && $saw_int_type && $saw_lib_func \
+ && $saw_dbg_dl && !$saw_error } \
+ $gdb_test_name
+ }
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+}
+
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ setenv DEBUGINFOD_URLS $url
+ test_deferred_status
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py
new file mode 100644
index 00000000000..f45762e855d
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-observer-symbols.py
@@ -0,0 +1,62 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+
+# Look up a symbol NAME in OBJFILE and check we can get its type. If
+# STATIC_P is true then NAME is a static symbol, otherwise it's a
+# non-static global.
+#
+# Print a status message containing the symbol's type, or an error if
+# the symbol cannot be found.
+def lookup_symbol_in_objfile(objfile, name, static_p):
+ try:
+ if static_p:
+ sym = objfile.lookup_static_symbol(name)
+ else:
+ sym = objfile.lookup_global_symbol(name)
+ if sym is not None:
+ # Try to get the symbol's type - this needs full debug info.
+ sym_type = sym.type
+ if sym_type is not None:
+ print("Python observer: found type {}".format(sym_type))
+ else:
+ print("Python observer: ERROR - symbol type is None")
+ else:
+ print("Python observer: ERROR - symbol not found")
+ except Exception as e:
+ print("Python observer: ERROR - exception: {}".format(e))
+
+
+# New objfile observer.
+def new_objfile_handler(event):
+ objfile = event.new_objfile
+
+ # Only interested in the separate debug objfile for libdeferred1.
+ if objfile.owner is None:
+ return
+ owner_filename = objfile.owner.filename
+ if owner_filename is None or "libdeferred1.so" not in owner_filename:
+ return
+
+ print("Python observer: checking symbols in {}".format(objfile.filename))
+
+ lookup_symbol_in_objfile(objfile, "libdeferred1_test", False)
+ lookup_symbol_in_objfile(objfile, "flag", True)
+
+
+gdb.events.new_objfile.connect(new_objfile_handler)
+print("Python observer registered")
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-pending.exp b/gdb/testsuite/gdb.debuginfod/deferred-pending.exp
new file mode 100644
index 00000000000..9d3dcd4fab2
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-pending.exp
@@ -0,0 +1,172 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test that pending breakpoints don't cause reentrant debuginfo downloads.
+#
+# When on-demand debuginfo downloading triggers a download,
+# finish_new_objfile is called for the new separate debug objfile. At this
+# point, the parent objfile still has OBJF_DOWNLOAD_DEFERRED set. If
+# breakpoint_re_set() were called here, it could try to resolve other
+# pending breakpoints, which might trigger symbol lookups that see
+# OBJF_DOWNLOAD_DEFERRED still set and attempt to download again - causing
+# reentrancy issues.
+#
+# This test sets pending breakpoints on functions in libraries with deferred
+# downloads, then triggers the downloads to verify no crashes or assertions
+# occur.
+
+# This test relies on 'run'.
+require !use_gdb_stub
+
+# Reuse source from deferred-download.exp test.
+standard_testfile deferred-download.c \
+ deferred-download-lib1.c \
+ deferred-download-lib2.c
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+
+set lib1_testfile "libdeferred1.so"
+set lib1_srcfile $srcfile2
+set lib1_binfile [standard_output_file $lib1_testfile]
+
+set lib2_testfile "libdeferred2.so"
+set lib2_srcfile $srcfile3
+set lib2_binfile [standard_output_file $lib2_testfile]
+
+# Build LIB2_BINFILE, LIB1_BINFILE, and BINFILE.
+if { [build_executable "build $lib2_testfile" $lib2_binfile $lib2_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+if { [build_executable "build $lib1_testfile" $lib1_binfile $lib1_srcfile \
+ [list debug build-id shlib_pthreads shlib=$lib2_binfile]] } {
+ return
+}
+
+if { [build_executable "build executable" $binfile $srcfile \
+ [list debug build-id shlib=$lib1_binfile shlib=$lib2_binfile]] } {
+ return
+}
+
+# Start GDB for the ensure_gdb_index calls.
+clean_restart
+
+# Add .gdb_index to both libraries.
+if { [ensure_gdb_index $lib1_binfile "" $lib1_testfile] != 1 } {
+ untested "failed to add .gdb_index to $lib1_testfile"
+ return
+}
+
+if { [ensure_gdb_index $lib2_binfile "" $lib2_testfile] != 1 } {
+ untested "failed to add .gdb_index to $lib2_testfile"
+ return
+}
+
+# Strip debuginfo into separate files.
+if { [gdb_gnu_strip_debug $lib1_binfile] != 0 } {
+ fail "strip $lib1_testfile debuginfo"
+ return
+}
+
+if { [gdb_gnu_strip_debug $lib2_binfile] != 0 } {
+ fail "strip $lib2_testfile debuginfo"
+ return
+}
+
+# Move debuginfo files into directory that debuginfod will serve.
+set debugdir [standard_output_file "debug"]
+set lib1_debuginfo [standard_output_file $lib1_testfile.debug]
+set lib2_debuginfo [standard_output_file $lib2_testfile.debug]
+
+file mkdir $debugdir
+file rename -force $lib1_debuginfo $debugdir
+file rename -force $lib2_debuginfo $debugdir
+
+# Test that setting pending breakpoints on functions in deferred libraries,
+# then running to trigger the downloads, doesn't cause reentrancy issues.
+proc_with_prefix test_pending_breakpoints { } {
+ global cache
+
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ # Load the executable but don't run yet.
+ gdb_load $::binfile
+
+ # Set pending breakpoints on functions in both libraries.
+ # These will be pending because the libraries aren't loaded yet.
+ gdb_breakpoint "libdeferred1_test" -allow-pending
+ gdb_breakpoint "libdeferred2_test" -allow-pending
+
+ # Now run. This will:
+ # 1. Load the shared libraries.
+ # 2. Download .gdb_index for each, full debuginfo download will be
+ # deferred though.
+ # 3. Resolve pending breakpoints using the indices.
+ # 4. When the first breakpoint is hit, full debuginfo will be
+ # downloaded.
+ #
+ # If breakpoint_re_set() is incorrectly called during the download,
+ # it might try to re-resolve the other breakpoint, potentially causing
+ # reentrant download attempts.
+ #
+ # We check that we can run to the first breakpoint without crashing.
+ set saw_error false
+ gdb_test_multiple "run" "run to first breakpoint" {
+ -re "Internal error|Error in re-setting breakpoint" {
+ set saw_error true
+ exp_continue
+ }
+ -re -wrap "Breakpoint \[0-9\]+, libdeferred1_test \\(\\) at .*" {
+ gdb_assert { !$saw_error } $gdb_test_name
+ }
+ }
+
+ # Continue to the second breakpoint to verify both libraries work.
+ set saw_error false
+ gdb_test_multiple "continue" "continue to second breakpoint" {
+ -re "Internal error|Error in re-setting breakpoint" {
+ set saw_error true
+ exp_continue
+ }
+ -re -wrap "Breakpoint \[0-9\]+, libdeferred2_test \\(\\) at .*" {
+ gdb_assert { !$saw_error } $gdb_test_name
+ }
+ }
+}
+
+# Create CACHE and DB directories ready for debuginfod to use.
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ set url [start_debuginfod $db $debugdir]
+ if { $url == "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ setenv DEBUGINFOD_URLS $url
+ test_pending_breakpoints
+}
+
+stop_debuginfod
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c
new file mode 100644
index 00000000000..b954375fe50
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib1.c
@@ -0,0 +1,26 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* ... */
+
+typedef void (*callback_t) (void);
+
+void
+library1_function (callback_t cb)
+{
+ cb ();
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c
new file mode 100644
index 00000000000..b742f2c4347
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame-lib2.c
@@ -0,0 +1,26 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* ... */
+
+typedef void (*callback_t) (void);
+
+void
+library2_function (callback_t cb)
+{
+ cb ();
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame.c b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.c
new file mode 100644
index 00000000000..26a46ea27d8
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.c
@@ -0,0 +1,61 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 Free Software Foundation, Inc.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* ... */
+
+typedef void (*callback_t) (void);
+extern void library1_function (callback_t cb);
+extern void library2_function (callback_t cb);
+
+volatile int global_var;
+
+void
+f0 (void)
+{
+ global_var = 42; /* Breakpoint in f0. */
+}
+
+void
+f1 (void)
+{
+ f0 (); /* Breakpoint in f1. */
+}
+
+void
+f3 (void)
+{
+ library2_function (f1);
+}
+
+void
+f4 (void)
+{
+ f3 ();
+}
+
+void
+f5 (void)
+{
+ library1_function (f4);
+}
+
+int
+main (void)
+{
+ f5 ();
+ return global_var - 42;
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp
new file mode 100644
index 00000000000..ea84b3a97c8
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.exp
@@ -0,0 +1,240 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Test that frame operations work correctly when deferred debuginfo
+# download is triggered mid-operation.
+#
+# The call chain is: main -> library_function -> callback_in_main
+# We stop at a breakpoint in callback_in_main. Debug info for the
+# library is served via debuginfod in deferred mode. A Python
+# new_objfile observer calls gdb.invalidate_cached_frames() when the
+# library's separate debug objfile is loaded.
+
+load_lib debuginfod-support.exp
+
+require allow_debuginfod_tests
+require allow_debuginfod_section_downloads
+
+standard_testfile .c -lib1.c -lib2.c
+
+# Build the first shared library.
+set lib1_testfile "lib1-${testfile}.so"
+set lib1_srcfile $srcfile2
+set lib1_binfile [standard_output_file $lib1_testfile]
+if { [build_executable "build $lib1_testfile" $lib1_testfile $lib1_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+# Build the second shared library.
+set lib2_testfile "lib2-${testfile}.so"
+set lib2_srcfile $srcfile3
+set lib2_binfile [standard_output_file $lib2_testfile]
+if { [build_executable "build $lib2_testfile" $lib2_testfile $lib2_srcfile \
+ {debug build-id shlib}] } {
+ return
+}
+
+if { [build_executable "build executable" $testfile $srcfile \
+ [list debug build-id shlib=$lib1_binfile \
+ shlib=$lib2_binfile]] != 0 } {
+ return
+}
+
+clean_restart
+
+# Add .gdb_index to both libraries.
+if { [ensure_gdb_index $lib1_binfile "" "$lib1_testfile"] != 1 } {
+ untested "failed to add .gdb_index to $lib1_testfile"
+ return
+}
+
+if { [ensure_gdb_index $lib2_binfile "" "$lib2_testfile"] != 1 } {
+ untested "failed to add .gdb_index to $lib2_testfile"
+ return
+}
+
+# Strip debuginfo into a separate file.
+if { [gdb_gnu_strip_debug $lib1_binfile ""] != 0 } {
+ fail "strip $lib1_testfile debuginfo"
+ return
+}
+
+if { [gdb_gnu_strip_debug $lib2_binfile ""] != 0 } {
+ fail "strip $lib2_testfile debuginfo"
+ return
+}
+
+# Move debuginfo file into directory that debuginfod will serve.
+set debugdir [standard_output_file "debug"]
+set lib1_debuginfo [standard_output_file $lib1_testfile.debug]
+set lib2_debuginfo [standard_output_file $lib2_testfile.debug]
+
+file mkdir $debugdir
+file rename -force $lib1_debuginfo $debugdir
+file rename -force $lib2_debuginfo $debugdir
+
+# Python script that invalidates the frame cache when the separate debug
+# objfile is loaded.
+set pyscript [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Start GDB, enable debuginfod, load the Python observer, run to the
+# breakpoint in callback_in_main. Returns true on success.
+proc setup_for_test {} {
+ clean_restart
+
+ gdb_test_no_output "set debuginfod enabled on"
+ gdb_test_no_output "set progress-bars enabled off"
+
+ # Load the Python script BEFORE loading anything else so the
+ # observer is registered when the deferred download triggers.
+ gdb_test "source $::pyscript" "Python registered new_objfile handler" \
+ "load python script"
+
+ gdb_load $::binfile
+
+ if {![runto_main]} {
+ return false
+ }
+
+ gdb_breakpoint "f1"
+ gdb_continue_to_breakpoint "breakpoint in f1"
+
+ return true
+}
+
+# Setup a breakpoint in a function, then use Python to select the
+# previous frame, which will require downloading the deferred debug
+# information. This download will trigger the new objfile event
+# handler, which will in turn invalidate the frame cache.
+#
+# Check that GDB handles this, and that the correct frame is selected.
+proc test_select_frame { } {
+ if {![setup_for_test]} {
+ return
+ }
+
+ # Navigate to the library frame via Python. The .older() call
+ # triggers the deferred download; the new_objfile observer calls
+ # gdb.invalidate_cached_frames() but GDB should still manage to
+ # select the expected frame.
+ set invalidate_count 0
+ set saw_selected false
+ gdb_test_multiple \
+ [multi_line_input \
+ "python" \
+ "f = gdb.selected_frame().older()" \
+ "if f is None:" \
+ " print('older frame is None after download')" \
+ "else:" \
+ " f.select()" \
+ " print('selected frame: ' + gdb.selected_frame().name())" \
+ "end"] \
+ "select library frame" {
+ -re "^Python new_objfile handler: frame cache invalidated\r\n" {
+ incr invalidate_count
+ exp_continue
+ }
+ -re "^selected frame: library2_function\r\n" {
+ set saw_selected true
+ exp_continue
+ }
+ -re "^$::gdb_prompt $" {
+ gdb_assert { $invalidate_count == 1 && $saw_selected } \
+ $gdb_test_name
+ }
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+
+ # Verify GDB is still functional.
+ gdb_test "info frame" ".*library2_function.*" "info frame after select"
+
+ # Verify we can get a full backtrace. The '.*' here matches
+ # against the deferred download for lib1 debug information.
+ gdb_test "bt" \
+ [multi_line \
+ "#0 ($::hex in )?f1 \\(\\) at \[^\r\n\]+" \
+ "#1 ($::hex in )?library2_function \\(cb=$::hex <f1>\\) at \[^\r\n\]+" \
+ "#2 ($::hex in )?f3 \\(\\) at \[^\r\n\]+" \
+ ".*" \
+ "#3 ($::hex in )?f4 \\(\\) at \[^\r\n\]+" \
+ "#4 ($::hex in )?library1_function \\(cb=$::hex <f4>\\) at \[^\r\n\]+" \
+ "#5 ($::hex in )?f5 \\(\\) at \[^\r\n\]+" \
+ "#6 ($::hex in )?main \\(\\) at \[^\r\n\]+"] \
+ "backtrace after select"
+}
+
+# Test that backtrace handles cache invalidation when unwinding
+# through a library frame with deferred debug info.
+#
+# The backtrace must unwind through library_function (frame #1),
+# which triggers deferred download. The new_objfile observer
+# invalidates the frame cache during the download.
+proc test_backtrace { } {
+ if {![setup_for_test]} {
+ return
+ }
+
+ set saw_invalidate false
+ set bt_line_count 0
+ gdb_test_multiple "bt" "backtrace through deferred library" {
+ -re "^Python new_objfile handler: frame cache invalidated\r\n" {
+ set saw_invalidate true
+ exp_continue
+ }
+
+ -re "^#$::decimal ($::hex in )?\[^\r\n\]+" {
+ incr bt_line_count
+ exp_continue
+ }
+
+ -re "^$::gdb_prompt $" {
+ gdb_assert { $saw_invalidate } "frame cache was invalidated"
+ gdb_assert { $bt_line_count == 7 } "saw expected number of bt lines"
+ }
+
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+ }
+}
+
+# Setup directory for the debuginfod server to serve from, and create
+# CACHE and DB directories ready for debuginfod to use.
+prepare_for_debuginfod cache db
+
+with_debuginfod_env $cache {
+ save_vars { env(DEBUGINFOD_URLS) } {
+ foreach_with_prefix test { backtrace select_frame } {
+ ### select_frame backtrace
+ # Delete client cache so debuginfo downloads again.
+ file delete -force $cache
+
+ set url [start_debuginfod $db $debugdir]
+ if { $url eq "" } {
+ unresolved "failed to start debuginfod server"
+ return
+ }
+
+ setenv DEBUGINFOD_URLS $url
+
+ test_${test}
+
+ stop_debuginfod
+ }
+ }
+}
diff --git a/gdb/testsuite/gdb.debuginfod/deferred-select-frame.py b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.py
new file mode 100644
index 00000000000..911ddae40d9
--- /dev/null
+++ b/gdb/testsuite/gdb.debuginfod/deferred-select-frame.py
@@ -0,0 +1,44 @@
+# Copyright 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Python script that invalidates the frame cache when the separate
+# debug objfile for lib-deferred-select-frame.so is loaded.
+#
+# This simulates an observer that resets frame-related state when new
+# debug info arrives. The invalidation happens during select_frame's
+# call to find_compunit_symtab_for_pc, after selected_frame has
+# already been set.
+
+import gdb
+
+def new_objfile_handler(event):
+ objfile = event.new_objfile
+
+ if objfile.owner is None:
+ return
+
+ owner_filename = objfile.owner.filename
+ if owner_filename is None:
+ return
+
+ if ("lib1-deferred-select-frame.so" not in owner_filename
+ and "lib2-deferred-select-frame.so" not in owner_filename):
+ return
+
+ gdb.invalidate_cached_frames()
+ print("Python new_objfile handler: frame cache invalidated")
+
+gdb.events.new_objfile.connect(new_objfile_handler)
+print("Python registered new_objfile handler")
diff --git a/gdb/testsuite/lib/debuginfod-support.exp b/gdb/testsuite/lib/debuginfod-support.exp
index 66b4098d4f9..e5be83cf3a8 100644
--- a/gdb/testsuite/lib/debuginfod-support.exp
+++ b/gdb/testsuite/lib/debuginfod-support.exp
@@ -115,6 +115,8 @@ proc with_debuginfod_env { cache body } {
proc start_debuginfod { db debugdir } {
global debuginfod_spawn_id spawn_id
+ set logfile [standard_output_file "server_log"]
+
# Find an unused port.
set port 7999
set found false
@@ -129,7 +131,8 @@ proc start_debuginfod { db debugdir } {
set old_spawn_id $spawn_id
}
- spawn debuginfod -vvvv -d $db -p $port -F $debugdir
+ spawn sh -c "debuginfod -vvvv -d $db -p $port -F $debugdir 2>&1 \
+ | tee $logfile"
set debuginfod_spawn_id $spawn_id
if { [info exists old_spawn_id] } {
@@ -207,3 +210,29 @@ proc stop_debuginfod { } {
unset debuginfod_spawn_id
}
}
+
+# Return true if gdb is configured to download ELF/DWARF sections from
+# debuginfod servers. Otherwise return false.
+gdb_caching_proc allow_debuginfod_section_downloads { } {
+ set cmd "maint set debuginfod download-sections on"
+ set msg "enable section downloads"
+
+ gdb_exit
+ gdb_start
+
+ set supported false
+ gdb_test_multiple $cmd $msg {
+ -re -wrap ".*not compiled into GDB.*" {
+ }
+ -re -wrap "^" {
+ set supported true
+ }
+ -re -wrap "" {
+ fail "$gdb_test_name (unexpected output)"
+ }
+ }
+
+ gdb_exit
+
+ return $supported
+}
diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp
index a40c87c6727..cc633f998b3 100644
--- a/gdb/testsuite/lib/gdb.exp
+++ b/gdb/testsuite/lib/gdb.exp
@@ -10985,10 +10985,14 @@ proc get_index_type { objfile { testname "" } } {
# STYLE controls which style of index to add, if needed. The empty
# string (the default) means .gdb_index; "-dwarf-5" means .debug_names.
-proc ensure_gdb_index { binfile {style ""} } {
+proc ensure_gdb_index { binfile {style ""} {testname_prefix ""} } {
set testfile [file tail $binfile]
-
set test "check if index present"
+
+ if { $testname_prefix != "" } {
+ set test "$testname_prefix $test"
+ }
+
set index_type [get_index_type $testfile $test]
if { $index_type eq "gdb" || $index_type eq "dwarf5" } {
--
2.25.4
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-14 15:07 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-14 15:06 [RFC 0/3] Deferred, or on-demand, debuginfod downloading Andrew Burgess
2026-08-14 15:06 ` [RFC 1/3] gdb: limited support for frame cache flushing while building stack Andrew Burgess
2026-08-14 15:06 ` [RFC 2/3] gdb: use basic_safe_range to iterate over objfile::qf Andrew Burgess
2026-08-14 15:06 ` [RFC 3/3] gdb/debuginfod: support on-demand debuginfo downloading Andrew Burgess
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox