blob: 894b5e4d31782040df0d2bc42ab0ac56fca10220 [file]
"""
Check that when data is written over a software breakpoint site, it does not
corrupt the breakpoint instruction, and is later written to memory when the
breakpoint is removed.
"""
import lldb
from lldbsuite.test.lldbtest import *
import lldbsuite.test.lldbutil as lldbutil
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbgdbproxy import GDBProxyTestBase
class TestCases(object):
NO_DEBUG_INFO_TESTCASE = True
def run_to_start(self):
is_proxy = isinstance(self, GDBProxyTestBase)
self.build()
exe = self.getBuildArtifact("a.out")
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
if is_proxy:
self.connect(target)
self.line = line_number("main.c", "// break here")
lldbutil.run_break_set_by_file_and_line(
self, "main.c", self.line, num_expected_locations=1, loc_exact=True
)
self.runCmd("continue" if is_proxy else "run")
self.expect(
"thread list",
STOPPED_DUE_TO_BREAKPOINT,
substrs=["stopped", "stop reason = breakpoint"],
)
loop_start_breakpoint_addr = (
target.breakpoints[0].GetLocationAtIndex(0).GetLoadAddress()
)
return target, target.GetProcess(), loop_start_breakpoint_addr
# Could not find a way to make place_break_here visible to lldb on Windows.
@skipIfWindows
@skipIfOutOfTreeDebugserver
def test_write_over_breakpoint(self):
target, process, loop_start_breakpoint_addr = self.run_to_start()
# Memory operations and breakpoint actions must be sent to the server
# right away instead of waiting for the next continue event.
self.runCmd("settings set target.process.disable-memory-cache on")
# Breakpoints set by address are never delayed but to make this test
# less fragile if later changed, disable delayed breakpoints entirely.
self.runCmd("settings set target.process.use-delayed-breakpoints false")
# At this point we are stopped at the start of the for loop.
# We will set a further breakpoint in foo, and this is the one we will
# test by writing over it then continuing to it.
# We could use the same breakpoint, but arranging for dead code
# immediately before it is more risky.
# The breakpoint must be on an exact address, because we do not want
# lldb to adjust it based on the debug information for prologues
# and epilogues.
symbol_contexts = target.FindSymbols("place_break_here")
self.assertEqual(1, len(symbol_contexts))
label_symbol = symbol_contexts[0].GetSymbol()
self.assertTrue(label_symbol.IsValid())
bkpt_address = label_symbol.GetStartAddress().GetLoadAddress(target)
# lldb-server has its algorithm unit tested as part of
# NativeProcessProtocol, but debugserver does not use that. So we will
# do a few different types of overwrite here so we have coverage for both
# debug servers.
#
# We cannot be sure what the software break size will be, so I'm assuming
# 4 bytes because that's what Arm/AArch64 use. This means we may not be
# getting full coverage on Thumb or x86 but the test should still pass
# there.
#
# We assume that the instruction immediately before the breakpoint in
# foo is dead code, so we are allowed to corrupt it.
writes = [
# Up to but not over breakpoint.
(-4, 4),
# Over start of breakpoint.
(-2, 4),
# Exactly over breakpoint.
(0, 4),
# Over end of breakpoint.
(2, 4),
# Immediately after breakpoint.
(4, 4),
# From before to after breakpoint.
(-4, 12),
]
for write_offset, write_size in writes:
# Place a breakpoint immediately after the dead code in foo.
bkpt = target.BreakpointCreateByAddress(bkpt_address)
self.assertTrue(bkpt.IsValid())
self.assertEqual(bkpt.GetNumLocations(), 1)
self.assertFalse(bkpt.IsHardware())
self.assertEqual(bkpt.GetLocationAtIndex(0).GetLoadAddress(), bkpt_address)
check_address = bkpt_address + write_offset
# Read around the breakpoint site. We assume that read subsitution
# is working, so the data here is the original contents of memory
# without the trap instruction.
err = lldb.SBError()
original_data = bytearray(
process.ReadMemory(check_address, write_size, err)
)
self.assertSuccess(err)
self.assertEqual(len(original_data), write_size)
# Write around/in/over the breakpoint site.
write_data = bytearray(range(write_size))
wrote = process.WriteMemory(check_address, write_data, err)
self.assertSuccess(err)
self.assertEqual(wrote, write_size)
# The data overlapping the breakpoint site should be in that breakpoint's
# saved data. So it will appear as if all the data was written to memory,
# even though it was not yet.
after_write = bytearray(process.ReadMemory(check_address, write_size, err))
self.assertSuccess(err)
self.assertEqual(after_write, write_data)
# The instruction in memory should still be intact so we can continue
# to the breakpoint.
process.Continue()
thread = process.thread[0]
self.assertState(process.GetState(), lldb.eStateStopped)
self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint)
# Should be stopped at the breakpoint we placed in foo. This proves that
# the breakpoint instruction was intact.
self.assertEqual(
bkpt_address,
thread.selected_frame.GetPC(),
)
# When the breakpoint is removed the saved bytes will be written to
# memory.
self.assertTrue(target.BreakpointDelete(bkpt.GetID()))
data = process.ReadMemory(check_address, write_size, err)
self.assertSuccess(err)
self.assertEqual(bytearray(data), write_data)
# Restore the original instruction data. The dead code before the
# breakpoint is ok but the instructions after it must be put back
# so we can continue.
wrote = process.WriteMemory(check_address, original_data, err)
self.assertSuccess(err)
self.assertEqual(wrote, write_size)
# Continue back to the start of the loop.
process.Continue()
self.assertState(process.GetState(), lldb.eStateStopped)
self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint)
self.assertEqual(loop_start_breakpoint_addr, thread.selected_frame.GetPC())
def test_write_over_uncommitted_breakpoint(self):
self.runCmd("settings set target.process.use-delayed-breakpoints true")
target, process, loop_start_breakpoint_addr = self.run_to_start()
bkpt = target.BreakpointCreateByName("foo")
self.assertTrue(bkpt.IsValid())
self.assertEqual(bkpt.GetNumLocations(), 1)
self.assertFalse(bkpt.IsHardware())
# At this point only lldb knows about the breakpoint, it has not been
# sent to the debug server yet. It is treated as software with a 0 size
# breakpoint site.
bkpt_address = bkpt.GetLocationAtIndex(0).GetLoadAddress()
# The largest breakpoint instruction we know of is 4 bytes.
read_size = 4
err = lldb.SBError()
original_data = bytearray(process.ReadMemory(bkpt_address, read_size, err))
self.assertSuccess(err)
self.assertEqual(len(original_data), read_size)
# The smallest break instruction we know about is 1 byte, so we will
# write only 1 byte. The value is ideally != byte 1 of the break and
# != to the value currently in memory. 0xcd is different to x86's 0xcc,
# and not used by any other platform we support. 0xdc is the fallback
# and if the original value is also 0xcd.
write_data = bytearray([0xCD if original_data[0] != 0xCD else 0xDC])
# Write something over the breakpoint. Use a single byte since x86's
# break is a single byte and we do not want to corrupt other code.
# LLDB used to crash at this point.
err = lldb.SBError()
wrote = process.WriteMemory(bkpt_address, write_data, err)
self.assertSuccess(err)
self.assertEqual(wrote, 1)
def check_memory():
err = lldb.SBError()
got = bytearray(process.ReadMemory(bkpt_address, read_size, err))
self.assertSuccess(err)
self.assertEqual(len(got), read_size)
read_expected = write_data + original_data[1:]
self.assertEqual(got, read_expected)
# We should see the original data with our new single byte at the start.
check_memory()
# The instruction in memory should still be intact so we can continue
# to the breakpoint.
process.Continue()
thread = process.thread[0]
self.assertState(process.GetState(), lldb.eStateStopped)
self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint)
# Should be stopped at the breakpoint we placed in foo. This proves that
# the breakpoint instruction was intact.
self.assertEqual(
bkpt_address,
thread.selected_frame.GetPC(),
)
# We should see the new first byte still. At this point the debug server
# should be managing the breakpoint, but this checks that the handover
# was done correctly.
check_memory()
@skipIfRemote
# x86, x86_64, s390x and all versions of Windows report a breakpoint address
# after the breakpoint and the test fails due to
# https://github.com/llvm/llvm-project/issues/222284.
@skipIf(archs=["i386", "x86_64", "s390x"])
@skipIfWindows
class WriteOverLLDBManagedSoftwareBreakpoint(GDBProxyTestBase, TestCases):
def respond(self, packet):
# Pretend that the server cannot handle breakpoints, which means lldb
# must manage them.
if packet.startswith("jMultiBreakpoint"):
return ""
elif packet.startswith("Z"):
return ""
return super().respond(packet)
class WriteOverExternalSoftwareBreakpoint(TestBase, TestCases):
pass