diff --git a/gdb/ChangeLog b/gdb/ChangeLog index 7581c0949df..b6930dff39c 100644 --- a/gdb/ChangeLog +++ b/gdb/ChangeLog @@ -1,3 +1,9 @@ +2021-06-01 Andrew Burgess + Richard Bunt + + * breakpoint.c (check_longjmp_breakpoint_for_call_dummy): Add + check for why the backtrace stopped. + 2021-05-31 Simon Marchi * dwarf2/read.h (struct structured_type) : New. diff --git a/gdb/breakpoint.c b/gdb/breakpoint.c index aa6b348dffd..fb011fc1e0f 100644 --- a/gdb/breakpoint.c +++ b/gdb/breakpoint.c @@ -7357,9 +7357,10 @@ set_longjmp_breakpoint_for_call_dummy (void) TP. Remove those which can no longer be found in the current frame stack. - You should call this function only at places where it is safe to currently - unwind the whole stack. Failed stack unwind would discard live dummy - frames. */ + If the unwind fails then there is not sufficient information to discard + dummy frames. In this case, elide the clean up and the dummy frames will + be cleaned up next time this function is called from a location where + unwinding is possible. */ void check_longjmp_breakpoint_for_call_dummy (struct thread_info *tp) @@ -7371,12 +7372,55 @@ check_longjmp_breakpoint_for_call_dummy (struct thread_info *tp) { struct breakpoint *dummy_b = b->related_breakpoint; + /* Find the bp_call_dummy breakpoint in the list of breakpoints + chained off b->related_breakpoint. */ while (dummy_b != b && dummy_b->type != bp_call_dummy) dummy_b = dummy_b->related_breakpoint; + + /* If there was no bp_call_dummy breakpoint then there's nothing + more to do. Or, if the dummy frame associated with the + bp_call_dummy is still on the stack then we need to leave this + bp_call_dummy in place. */ if (dummy_b->type != bp_call_dummy || frame_find_by_id (dummy_b->frame_id) != NULL) continue; - + + /* We didn't find the dummy frame on the stack, this could be + because we have longjmp'd to a stack frame that is previous to + the dummy frame, or it could be because the stack unwind is + broken at some point between the longjmp frame and the dummy + frame. + + Next we figure out why the stack unwind stopped. If it looks + like the unwind is complete then we assume the dummy frame has + been jumped over, however, if the unwind stopped for an + unexpected reason then we assume the stack unwind is currently + broken, and that we will (eventually) return to the dummy + frame. + + It might be tempting to consider using frame_id_inner here, but + that is not safe. There is no guarantee that the stack frames + we are looking at here are even on the same stack as the + original dummy frame, hence frame_id_inner can't be used. See + the comments on frame_id_inner for more details. */ + bool unwind_finished_unexpectedly = false; + for (struct frame_info *fi = get_current_frame (); fi != nullptr; ) + { + struct frame_info *prev = get_prev_frame (fi); + if (prev == nullptr) + { + /* FI is the last stack frame. Why did this frame not + unwind further? */ + auto stop_reason = get_frame_unwind_stop_reason (fi); + if (stop_reason != UNWIND_NO_REASON + && stop_reason != UNWIND_OUTERMOST) + unwind_finished_unexpectedly = true; + } + fi = prev; + } + if (unwind_finished_unexpectedly) + continue; + dummy_frame_discard (dummy_b->frame_id, tp); while (b->related_breakpoint != b) diff --git a/gdb/testsuite/ChangeLog b/gdb/testsuite/ChangeLog index e2a95d48c33..6c7079325b9 100644 --- a/gdb/testsuite/ChangeLog +++ b/gdb/testsuite/ChangeLog @@ -1,3 +1,9 @@ +2021-06-01 Andrew Burgess + + * gdb.base/premature-dummy-frame-removal.c: New file. + * gdb.base/premature-dummy-frame-removal.exp: New file. + * gdb.base/premature-dummy-frame-removal.py: New file. + 2021-05-27 Simon Marchi * gdb.base/reverse-init-functions.exp: New. diff --git a/gdb/testsuite/gdb.base/premature-dummy-frame-removal.c b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.c new file mode 100644 index 00000000000..32deefc962f --- /dev/null +++ b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.c @@ -0,0 +1,65 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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 . */ + +#include +#include + +jmp_buf env; + +void +worker (void) +{ + longjmp (env, 1); +} + +void +test_inner (void) +{ + if (setjmp (env) == 0) + { + /* Direct call. */ + worker (); + + /* Will never get here. */ + abort (); + } + else + { + /* Called from longjmp. */ + } +} + +void +break_bt_here (void) +{ + test_inner (); +} + +int +some_func (void) +{ + break_bt_here (); + return 0; +} + +int +main (void) +{ + some_func (); + + return 0; +} diff --git a/gdb/testsuite/gdb.base/premature-dummy-frame-removal.exp b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.exp new file mode 100644 index 00000000000..bf2a2a79756 --- /dev/null +++ b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.exp @@ -0,0 +1,53 @@ +# Copyright 2021 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 . + +# Make an inferior call to a function which uses longjmp. However, +# the backtrace for the function that is called is broken at the point +# where the longjmp is handled. This test is checking to see if the +# inferior call still completes successfully. +# +# This test forces a broken backtrace using Python, but in real life a +# broken backtrace can easily occur when calling through code for +# which there is no debug information if the prologue unwinder fails, +# which can often happen if the code has been optimized. +# +# The problem was that, due to the broken backtrace, GDB failed to +# find the inferior call's dummy frame. GDB then concluded that the +# inferior had longjmp'd backward past the dummy frame and so garbage +# collected the dummy frame, this causes the breakpoint within the +# dummy frame to be deleted. +# +# When the inferior continued, and eventually returned to the dummy +# frame, it would try to execute instruction from the dummy frame +# (which for most, or even all, targets, is on the stack), and then +# experience undefined behaviuor, often a SIGSEGV. + +standard_testfile .c + +if { [prepare_for_testing "failed to prepare" $testfile $srcfile] } { + return -1 +} + +if ![runto_main] then { + return 0 +} + +# Skip this test if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py] +gdb_test_no_output "source ${pyfile}" "load python file" + +gdb_test "p some_func ()" " = 0" diff --git a/gdb/testsuite/gdb.base/premature-dummy-frame-removal.py b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.py new file mode 100644 index 00000000000..31936658788 --- /dev/null +++ b/gdb/testsuite/gdb.base/premature-dummy-frame-removal.py @@ -0,0 +1,60 @@ +# Copyright (C) 2021 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 . + +# This dummy unwinder will break GDB's backtrce at the function called +# 'break_bt_here'. + +import gdb +from gdb.unwinder import Unwinder + + +class FrameId(object): + def __init__(self, sp, pc): + self._sp = sp + self._pc = pc + + @property + def sp(self): + return self._sp + + @property + def pc(self): + return self._pc + + +class TestUnwinder(Unwinder): + def __init__(self): + Unwinder.__init__(self, "break unwinding") + + def __call__(self, pending_frame): + pc_desc = pending_frame.architecture().registers().find("pc") + pc = pending_frame.read_register(pc_desc) + + sp_desc = pending_frame.architecture().registers().find("sp") + sp = pending_frame.read_register(sp_desc) + + block = gdb.block_for_pc(int(pc)) + if block == None: + return None + func = block.function + if func == None: + return None + if str(func) != "break_bt_here": + return None + fid = FrameId(pc, sp) + return pending_frame.create_unwind_info(fid) + + +gdb.unwinder.register_unwinder(None, TestUnwinder(), True)