From 64296d87c8db921ec58528d5a460a1f1ded1219c Mon Sep 17 00:00:00 2001 From: David Malcolm Date: Fri, 26 Feb 2010 23:51:19 -0500 Subject: Add tests of the handling of corrupt data Rework get_stack_trace to support the injection of additional gdb commands after hitting the breakpoint, before running "bt" Rework get_gdb_repr so that it captures the value in frame #0 of the backtrace, rather than the value when the breakpoint hits. Add four more unit tests, using the above to corrupt the data before getting the backtrace representation. --- test_gdb.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 15 deletions(-) (limited to 'test_gdb.py') diff --git a/test_gdb.py b/test_gdb.py index 36aa8de..e9ce82a 100644 --- a/test_gdb.py +++ b/test_gdb.py @@ -43,11 +43,16 @@ class DebuggerTests(unittest.TestCase): ).communicate() return out, err - def get_stack_trace(self, source, breakpoint='PyObject_Print'): + def get_stack_trace(self, source, breakpoint='PyObject_Print', + commands_after_breakpoint=None): ''' Run 'python -c SOURCE' under gdb with a breakpoint. + Support injecting commands after the breakpoint is reached + Returns the stdout from gdb + + commands_after_breakpoint: if provided, a list of strings: gdb commands ''' # We use "set breakpoint pending yes" to avoid blocking with a: # Function "foo" not defined. @@ -61,14 +66,28 @@ class DebuggerTests(unittest.TestCase): # Initially I had "--eval-command=continue" here, but removed it to # avoid repeated print breakpoints when traversing hierarchical data # structures - out, err = self.run_gdb("gdb", "--batch", - "--eval-command=set breakpoint pending yes", - "--eval-command=break %s" % breakpoint, - - "--eval-command=run", - "--eval-command=backtrace", - "--args", - sys.executable, "-S", "-c", source) + + # Generate a list of commands in gdb's language: + commands = ['set breakpoint pending yes', + 'break %s' % breakpoint, + 'run'] + if commands_after_breakpoint: + commands += commands_after_breakpoint + commands += ['backtrace'] + + # print commands + + # Use "commands" to generate the arguments with which to invoke "gdb": + args = ["gdb", "--batch"] + args += ['--eval-command=%s' % cmd for cmd in commands] + args += ["--args", + sys.executable, "-S", "-c", source] + # -S suppresses the default 'import site' + + # print args + + # Use "args" to invoke gdb, capturing stdout, stderr: + out, err = self.run_gdb(*args) # Ignore some noise on stderr due to the pending breakpoint: err = err.replace('Function "%s" not defined.\n' % breakpoint, '') @@ -78,7 +97,8 @@ class DebuggerTests(unittest.TestCase): return out - def get_gdb_repr(self, source): + def get_gdb_repr(self, source, + commands_after_breakpoint=None): # Given an input python source representation of data, # run "python -c'print DATA'" under gdb with a breakpoint on # PyObject_Print and scrape out gdb's representation of the "op" @@ -86,19 +106,19 @@ class DebuggerTests(unittest.TestCase): # # For a nested structure, the first time we hit the breakpoint will # give us the top-level structure - gdb_output = self.get_stack_trace(source) - m = re.match('.*Breakpoint 1, PyObject_Print \(op\=(.*?), fp=.*\).*', gdb_output, re.DOTALL) - # print m.groups() + gdb_output = self.get_stack_trace(source, 'PyObject_Print', commands_after_breakpoint) + m = re.match('.*#0 PyObject_Print \(op\=(.*?), fp=.*\).*', gdb_output, re.DOTALL) + #print m.groups() return m.group(1), gdb_output def test_getting_backtrace(self): gdb_output = self.get_stack_trace('print 42') self.assertTrue('PyObject_Print' in gdb_output) - def assertGdbRepr(self, val): + def assertGdbRepr(self, val, commands_after_breakpoint=None): # Ensure that gdb's rendering of the value in a debugged process # matches repr(value) in this process: - gdb_repr, gdb_output = self.get_gdb_repr('print ' + repr(val)) + gdb_repr, gdb_output = self.get_gdb_repr('print ' + repr(val), commands_after_breakpoint) self.assertEquals(gdb_repr, repr(val), gdb_output) def test_int(self): @@ -139,6 +159,14 @@ class DebuggerTests(unittest.TestCase): self.assertGdbRepr( u'hello world', ) self.assertGdbRepr( u'\u2620') + def assert_is_failsafe_repr(self, gdb_repr, exp_type='unknown'): + '''Verify that the given gdb_repr string is the expected failsafe + representation for when there's corrupt data within the inferior + process''' + self.assertTrue(re.match('<%s at remote 0x[0-9a-f]+>' % exp_type, + gdb_repr), + 'Unexpected gdb representation: %r' % gdb_repr) + def test_classic_class(self): gdb_repr, gdb_output = self.get_gdb_repr(''' class Foo: @@ -149,6 +177,26 @@ print foo''') # FIXME: is there an "assertMatches"; should there be? m = re.match(r'', gdb_repr) self.assertTrue(m, msg='Unexpected classic-class rendering %r' % gdb_repr) + + def test_NULL_ob_type(self): + gdb_repr, gdb_output = self.get_gdb_repr('print 42', + commands_after_breakpoint=['set op->ob_type=0']) + self.assert_is_failsafe_repr(gdb_repr) + + def test_corrupt_ob_type(self): + gdb_repr, gdb_output = self.get_gdb_repr('print "this string will have its ob_type corrupted"', + commands_after_breakpoint=['set op->ob_type=0xDEADBEEF']) + self.assert_is_failsafe_repr(gdb_repr) + + def test_corrupt_tp_flags(self): + gdb_repr, gdb_output = self.get_gdb_repr('print 42', + commands_after_breakpoint=['set op->ob_type->tp_flags=0x0']) + self.assert_is_failsafe_repr(gdb_repr, exp_type='int') + + def test_corrupt_tp_name(self): + gdb_repr, gdb_output = self.get_gdb_repr('print 42', + commands_after_breakpoint=['set op->ob_type->tp_name=0xDEADBEEF']) + self.assert_is_failsafe_repr(gdb_repr) # TODO: # new-style classes -- cgit