forked from Imagelibrary/binutils-gdb
I've been reviewing all uses of PyObject_IsInstance, and I believe
that the use of PyObject_IsInstance in py-unwind.c is not entirely
correct. The use of PyObject_IsInstance is in this code in
frame_unwind_python::sniff:
if (PyObject_IsInstance (pyo_unwind_info,
(PyObject *) &unwind_info_object_type) <= 0)
error (_("A Unwinder should return gdb.UnwindInfo instance."));
The problem is that PyObject_IsInstance can return -1 to indicate an
error, in which case a Python error will have been set. Now, the
above code appears to handle this case, it checks for '<= 0', however,
frame_unwind_python::sniff has this near the start:
gdbpy_enter enter_py (gdbarch);
And looking in python.c at 'gdbpy_enter::~gdbpy_enter ()', you'll
notice that if an error is set then the error is printed, but also, we
get a warning about an unhandled Python exception. Clearly, all
exceptions should have been handled by the time the gdbpy_enter
destructor is called.
I've added a test as part of this commit that exposes this problem,
the current output is:
(gdb) backtrace
Python Exception <class 'RuntimeError'>: error in Blah.__class__
warning: internal error: Unhandled Python exception
Python Exception <class 'gdb.error'>: A Unwinder should return gdb.UnwindInfo instance.
#0 corrupt_frame_inner () at /home/andrew/projects/binutils-gdb/build.dev-g/gdb/testsuite/../../../src.dev-g/gdb/test>
(gdb)
An additional observation is that we use PyObject_IsInstance to check
that the return value is a gdb.UnwindInfo, or a sub-class. However,
gdb.UnwindInfo lacks the Py_TPFLAGS_BASETYPE flag, and so cannot be
sub-classed. As such, PyObject_IsInstance is not really needed, we
could use PyObject_TypeCheck instead. The PyObject_TypeCheck function
only returns 0 or 1, there is no -1 error case. Switching to
PyObject_TypeCheck then, fixes the above problem.
There's a new test that exposes the problems that originally existed.
Approved-By: Tom Tromey <tom@tromey.com>
283 lines
11 KiB
Plaintext
283 lines
11 KiB
Plaintext
# Copyright (C) 2015-2025 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 file is part of the GDB testsuite. It verifies that frame
|
|
# unwinders can be implemented in Python.
|
|
|
|
load_lib gdb-python.exp
|
|
|
|
require allow_python_tests
|
|
|
|
standard_testfile
|
|
|
|
# Stack protection can make the stack look a bit different, breaking the
|
|
# assumptions this test has about its layout.
|
|
|
|
set flags "additional_flags=-fno-stack-protector"
|
|
|
|
if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile} "debug $flags"] } {
|
|
return -1
|
|
}
|
|
|
|
# This test runs on a specific platform.
|
|
require is_x86_64_m64_target
|
|
|
|
# The following tests require execution.
|
|
|
|
if {![runto_main]} {
|
|
return 0
|
|
}
|
|
|
|
# Check for the corrupt backtrace.
|
|
proc check_for_broken_backtrace {testname} {
|
|
gdb_test_sequence "where" $testname {
|
|
"\\r\\n#0 .* corrupt_frame_inner \\(\\) at "
|
|
"\\r\\n#1 .* corrupt_frame_outer \\(\\) at "
|
|
"Backtrace stopped: frame did not save the PC"
|
|
}
|
|
}
|
|
|
|
# Check for the correct backtrace.
|
|
proc check_for_fixed_backtrace {testname} {
|
|
gdb_test_sequence "where" $testname {
|
|
"\\r\\n#0 .* corrupt_frame_inner \\(\\) at "
|
|
"\\r\\n#1 .* corrupt_frame_outer \\(\\) at "
|
|
"\\r\\n#2 .* main \\(.*\\) at"
|
|
}
|
|
}
|
|
|
|
# Check the 'info unwinder' output.
|
|
proc check_info_unwinder {testname enabled} {
|
|
if {$enabled} {
|
|
set suffix ""
|
|
} else {
|
|
set suffix " \\\[disabled\\\]"
|
|
}
|
|
|
|
gdb_test_sequence "info unwinder" $testname \
|
|
[list \
|
|
"Global:" \
|
|
" test unwinder${suffix}"]
|
|
}
|
|
|
|
set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
|
|
|
|
gdb_breakpoint [gdb_get_line_number "break backtrace-broken"]
|
|
|
|
gdb_continue_to_breakpoint "break backtrace-broken"
|
|
|
|
check_for_broken_backtrace "Backtrace is initially broken"
|
|
|
|
gdb_test "source ${pyfile}" "Python script imported" \
|
|
"import python scripts"
|
|
|
|
check_info_unwinder "info unwinder after loading script" on
|
|
|
|
check_for_fixed_backtrace "check backtrace after loading unwinder"
|
|
|
|
# Check that the Python unwinder frames can be flushed / released.
|
|
gdb_test "maint flush register-cache" "Register cache flushed\\." "flush frames"
|
|
|
|
check_for_fixed_backtrace "check backtrace after flush"
|
|
|
|
# Try to disable the unwinder but instead set the enabled field to a
|
|
# non boolean value. This should fail. Check the 'info unwinder'
|
|
# output to be sure.
|
|
gdb_test "python global_test_unwinder.enabled = \"off\"" \
|
|
[multi_line \
|
|
"TypeError.*: incorrect type for enabled attribute: <class 'str'>" \
|
|
"Error occurred in Python.*"]
|
|
check_info_unwinder "info unwinder after failed disable" on
|
|
|
|
# While we're doing silly stuff, lets try to change the name of this
|
|
# unwider. Doing this is bad as the new name might clash with an
|
|
# already registered name, which violates the promises made during
|
|
# 'register_unwinder'.
|
|
set pattern_1 "can't set attribute(?: 'name')?"
|
|
set pattern_2 "property 'name' of 'TestUnwinder' object has no setter"
|
|
gdb_test "python global_test_unwinder.name = \"foo\"" \
|
|
[multi_line \
|
|
"AttributeError.*: (?:${pattern_1}|${pattern_2})" \
|
|
"Error occurred in Python.*"]
|
|
check_info_unwinder "info unwinder after failed name change" on
|
|
|
|
# Now actually disable the unwinder by manually adjusting the
|
|
# 'enabled' attribute. Check that the stack is once again broken, and
|
|
# that the unwinder shows as disabled in the 'info unwinder' output.
|
|
gdb_test_no_output "python global_test_unwinder.enabled = False"
|
|
check_for_broken_backtrace "stack is broken after disabling"
|
|
check_info_unwinder "info unwinder after manually disabling" off
|
|
|
|
# Now enable the unwinder using the 'enable unwinder' command.
|
|
gdb_test "enable unwinder global \"test unwinder\"" \
|
|
"1 unwinder enabled"
|
|
check_for_fixed_backtrace "check backtrace after enabling with command"
|
|
check_info_unwinder "info unwinder after command enabled" on
|
|
|
|
# And now disable using the command and check the stack is once again
|
|
# broken, and that the 'info unwinder' output updates correctly.
|
|
gdb_test "disable unwinder global \"test unwinder\"" \
|
|
"1 unwinder disabled"
|
|
check_for_broken_backtrace "stack is broken after command disabling"
|
|
check_info_unwinder "info unwinder after command disabling" off
|
|
|
|
# Check that invalid register names and values cause errors.
|
|
gdb_test "python print(add_saved_register_errors\[\"unknown_name\"\])" \
|
|
"Bad register" \
|
|
"add_saved_register error when an unknown register name is used"
|
|
gdb_test "python print(add_saved_register_errors\[\"unknown_number\"\])" \
|
|
"Bad register" \
|
|
"add_saved_register error when an unknown register number is used"
|
|
gdb_test "python print(add_saved_register_errors\[\"bad_value\"\])" \
|
|
"argument 2 must be gdb.Value, not int" \
|
|
"add_saved_register error when invalid register value is used"
|
|
gdb_test "python print(read_register_error)" "Bad register" \
|
|
"read_register error"
|
|
|
|
# Try to create an unwinder object with a non-string name.
|
|
gdb_test "python obj = simple_unwinder(True)" \
|
|
[multi_line \
|
|
"TypeError.*: incorrect type for name: <class 'bool'>" \
|
|
"Error occurred in Python.*"]
|
|
|
|
# Now register the simple_unwinder with a valid name, and use the
|
|
# unwinder to capture a PendingFrame object.
|
|
gdb_test_no_output "python obj = simple_unwinder(\"simple\")"
|
|
gdb_test_no_output "python gdb.unwinder.register_unwinder(None, obj)"
|
|
check_for_broken_backtrace "backtrace to capture a PendingFrame object"
|
|
|
|
# Check the captured PendingFrame is not valid.
|
|
gdb_test "python print(captured_pending_frame.is_valid())" "False"
|
|
|
|
# Check the __repr__ of an invalid PendingFrame.
|
|
gdb_test "python print(repr(captured_pending_frame))" \
|
|
"<gdb.PendingFrame \\(invalid\\)>"
|
|
|
|
# Check the __repr__ of an UnwindInfo for an invalid PendingFrame.
|
|
gdb_test "python print(captured_unwind_info)"
|
|
gdb_test "python print(repr(captured_unwind_info))" \
|
|
"<gdb.UnwindInfo for an invalid frame>"
|
|
|
|
# Check the repr of a PendingFrame that was copied (as a string) at a
|
|
# time the PendingFrame was valid.
|
|
gdb_test "python print(captured_pending_frame_repr)" \
|
|
"<gdb.PendingFrame level=0, sp=$hex, pc=$hex>"
|
|
|
|
# Check the repr of an UnwindInfo that was copied (as a string) at a
|
|
# time the UnwindInfo was valid.
|
|
gdb_test "python print(captured_unwind_info_repr)" \
|
|
"<gdb.UnwindInfo frame #0, saved_regs=\\(rip, rbp, rsp\\)>"
|
|
|
|
# Call methods on the captured gdb.PendingFrame and check we see the
|
|
# expected error.
|
|
gdb_test_no_output "python pf = captured_pending_frame"
|
|
foreach cmd {"pf.read_register(\"pc\")" \
|
|
"pf.create_unwind_info(None)" \
|
|
"pf.architecture()" \
|
|
"pf.level()" \
|
|
"pf.name()" \
|
|
"pf.pc()" \
|
|
"pf.language()" \
|
|
"pf.find_sal()" \
|
|
"pf.block()" \
|
|
"pf.function()" } {
|
|
gdb_test "python $cmd" \
|
|
[multi_line \
|
|
"ValueError.*: gdb\\.PendingFrame is invalid\\." \
|
|
"Error occurred in Python.*"]
|
|
}
|
|
|
|
# Turn on the useful unwinder so we have the full backtrace again, and
|
|
# disable the simple unwinder -- because we can!
|
|
gdb_test "enable unwinder global \"test unwinder\"" \
|
|
"1 unwinder enabled" \
|
|
"re-enable 'test unwinder' so we can check PendingFrame methods"
|
|
gdb_test "disable unwinder global \"simple\"" \
|
|
"1 unwinder disabled"
|
|
check_for_fixed_backtrace \
|
|
"check backtrace before testing PendingFrame methods"
|
|
|
|
# Turn the 'simple' unwinder back on.
|
|
gdb_test "enable unwinder global \"simple\"" \
|
|
"1 unwinder enabled"
|
|
|
|
# Replace the "simple" unwinder with a new version that doesn't set
|
|
# the 'sp' attribute. Also the 'pc' attribute is invalid, but we'll
|
|
# hit the missing 'sp' error first.
|
|
with_test_prefix "frame-id 'sp' is None" {
|
|
gdb_test_no_output "python obj = simple_unwinder(\"simple\", None, \"xyz\")"
|
|
gdb_test_no_output "python gdb.unwinder.register_unwinder(None, obj, replace=True)"
|
|
gdb_test_no_output "python captured_pending_frame = None"
|
|
gdb_test "backtrace" \
|
|
"Python Exception <class 'ValueError'>: frame_id should have 'sp' attribute\\.\r\n.*"
|
|
}
|
|
|
|
# Replace the "simple" unwinder with a new version that sets the 'sp'
|
|
# attribute to an invalid value. Also the 'pc' attribute is invalid, but we'll
|
|
# hit the invalid 'sp' error first.
|
|
with_test_prefix "frame-id 'sp' is invalid" {
|
|
gdb_test_no_output "python obj = simple_unwinder(\"simple\", \"jkl\", \"xyz\")"
|
|
gdb_test_no_output "python gdb.unwinder.register_unwinder(None, obj, replace=True)"
|
|
gdb_test_no_output "python captured_pending_frame = None"
|
|
gdb_test "backtrace" \
|
|
"Python Exception <class 'ValueError'>: invalid literal for int\\(\\) with base 10: 'jkl'\r\n.*"
|
|
}
|
|
|
|
# Replace the "simple" unwinder with a new version that sets the 'sp'
|
|
# to a valid value, but set the 'pc' attribute to an invalid value.
|
|
with_test_prefix "frame-id 'pc' is invalid" {
|
|
gdb_test_no_output "python obj = simple_unwinder(\"simple\", 0x123, \"xyz\")"
|
|
gdb_test_no_output "python gdb.unwinder.register_unwinder(None, obj, replace=True)"
|
|
gdb_test_no_output "python captured_pending_frame = None"
|
|
gdb_test "backtrace" \
|
|
"Python Exception <class 'ValueError'>: invalid literal for int\\(\\) with base 10: 'xyz'\r\n.*"
|
|
}
|
|
|
|
with_test_prefix "bad object unwinder" {
|
|
gdb_test_no_output "python obj = bad_object_unwinder(\"bad-object\")"
|
|
gdb_test_no_output "python gdb.unwinder.register_unwinder(None, obj, replace=True)"
|
|
gdb_test "backtrace" \
|
|
"Python Exception <class 'gdb.error'>: an Unwinder should return gdb.UnwindInfo, not Blah\\.\r\n.*"
|
|
}
|
|
|
|
# Gather information about every frame.
|
|
gdb_test_no_output "python capture_all_frame_information()"
|
|
gdb_test_no_output "python gdb.newest_frame().select()"
|
|
gdb_test_no_output "python pspace = gdb.selected_inferior().progspace"
|
|
gdb_test_no_output "python obj = validating_unwinder()"
|
|
gdb_test_no_output "python gdb.unwinder.register_unwinder(pspace, obj)"
|
|
|
|
check_for_fixed_backtrace \
|
|
"check backtrace to validate all information"
|
|
|
|
gdb_test_no_output "python check_all_frame_information_matched()"
|
|
|
|
# Check we can't sub-class from gdb.UnwindInfo.
|
|
gdb_test_multiline "Sub-class gdb.UnwindInfo " \
|
|
"python" "" \
|
|
"class my_unwind_info(gdb.UnwindInfo):" "" \
|
|
" def __init__(self):" "" \
|
|
" pass" "" \
|
|
"end" \
|
|
[multi_line \
|
|
"TypeError.*: type 'gdb\\.UnwindInfo' is not an acceptable base type" \
|
|
"Error occurred in Python.*"]
|
|
|
|
# Check we can't directly instantiate a gdb.UnwindInfo.
|
|
gdb_test "python uw = gdb.UnwindInfo()" \
|
|
[multi_line \
|
|
"TypeError.*: cannot create 'gdb\\.UnwindInfo' instances" \
|
|
"Error occurred in Python.*"]
|