llvm-project/lldb/unittests/Host/NativeProcessProtocolTest.cpp
royitaqi a13712ed88
[lldb] Fix a crash in lldb-server during RemoveSoftwareBreakpoint() (#148738)
# Lldb-server crash

We have seen stacks like the following in lldb-server core dumps:
```
[
  "__GI___pthread_kill at pthread_kill.c:46",
  "__GI_raise at raise.c:26",
  "__GI_abort at abort.c:100",
  "__assert_fail_base at assert.c:92",
  "__GI___assert_fail at assert.c:101",
  "lldb_private::NativeProcessProtocol::RemoveSoftwareBreakpoint(unsigned long) at /redacted/lldb-server:0"
]
```

# Hypothesis of root cause

In `NativeProcessProtocol::RemoveSoftwareBreakpoint()`
([code](19b2dd9d79/lldb/source/Host/common/NativeProcessProtocol.cpp (L359-L423))),
a `ref_count` is asserted and reduced. If it becomes zero, the code
first go through a series of memory reads and writes to remove the
breakpoint trap opcode and to restore the original process code, then,
if everything goes fine, removes the entry from the map
`m_software_breakpoints` at the end of the function.

However, if any of the validations for the above reads and writes goes
wrong, the code returns an error early, skipping the removal of the
entry. This leaves the entry behind with a `ref_count` of zero.

The next call to `NativeProcessProtocol::RemoveSoftwareBreakpoint()` for
the same breakpoint[*] would violate the assertion about `ref_count > 0`
([here](19b2dd9d79/lldb/source/Host/common/NativeProcessProtocol.cpp (L365))),
which would cause a crash.

[*] We haven't found a *regular* way to repro such a next call in lldb
or lldb-dap. This is because both of them remove the breakpoint from
their internal list when they get any response from the lldb-server (OK
or error). Asking the client to delete the breakpoint a second time
doesn't trigger the client to send the `$z` gdb packet to lldb-server.
We are able to trigger the crash by sending the `$z` packet directly,
see "Manual test" below.

# Fix

Lift the removal of the map entry to be immediately after the decrement
of `ref_count`, before the early returns. This ensures that the asserted
case will never happen. The validation errors can still happen, and
whether they happen or not, the breakpoint has been removed from the
perspective of the lldb-server (same as that of lldb and lldb-dap).


# Manual test & unit test

See PR.
2025-07-16 07:57:13 -07:00

241 lines
10 KiB
C++

//===-- NativeProcessProtocolTest.cpp -------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "TestingSupport/Host/NativeProcessTestUtils.h"
#include "lldb/Host/common/NativeProcessProtocol.h"
#include "llvm/Support/Process.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
using namespace lldb_private;
using namespace lldb;
using namespace testing;
TEST(NativeProcessProtocolTest, SetBreakpoint) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("x86_64-pc-linux"));
auto Trap = cantFail(Process.GetSoftwareBreakpointTrapOpcode(1));
InSequence S;
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(std::vector<uint8_t>{0xbb})));
EXPECT_CALL(Process, WriteMemory(0x47, Trap)).WillOnce(Return(ByMove(1)));
EXPECT_CALL(Process, ReadMemory(0x47, 1)).WillOnce(Return(ByMove(Trap)));
EXPECT_THAT_ERROR(Process.SetBreakpoint(0x47, 0, false).ToError(),
llvm::Succeeded());
}
TEST(NativeProcessProtocolTest, SetBreakpointFailRead) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("x86_64-pc-linux"));
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(
llvm::createStringError(llvm::inconvertibleErrorCode(), "Foo"))));
EXPECT_THAT_ERROR(Process.SetBreakpoint(0x47, 0, false).ToError(),
llvm::Failed());
}
TEST(NativeProcessProtocolTest, SetBreakpointFailWrite) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("x86_64-pc-linux"));
auto Trap = cantFail(Process.GetSoftwareBreakpointTrapOpcode(1));
InSequence S;
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(std::vector<uint8_t>{0xbb})));
EXPECT_CALL(Process, WriteMemory(0x47, Trap))
.WillOnce(Return(ByMove(
llvm::createStringError(llvm::inconvertibleErrorCode(), "Foo"))));
EXPECT_THAT_ERROR(Process.SetBreakpoint(0x47, 0, false).ToError(),
llvm::Failed());
}
TEST(NativeProcessProtocolTest, SetBreakpointFailVerify) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("x86_64-pc-linux"));
auto Trap = cantFail(Process.GetSoftwareBreakpointTrapOpcode(1));
InSequence S;
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(std::vector<uint8_t>{0xbb})));
EXPECT_CALL(Process, WriteMemory(0x47, Trap)).WillOnce(Return(ByMove(1)));
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(
llvm::createStringError(llvm::inconvertibleErrorCode(), "Foo"))));
EXPECT_THAT_ERROR(Process.SetBreakpoint(0x47, 0, false).ToError(),
llvm::Failed());
}
TEST(NativeProcessProtocolTest, RemoveSoftwareBreakpoint) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("x86_64-pc-linux"));
auto Trap = cantFail(Process.GetSoftwareBreakpointTrapOpcode(1));
auto Original = std::vector<uint8_t>{0xbb};
// Set up a breakpoint.
{
InSequence S;
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(Original)));
EXPECT_CALL(Process, WriteMemory(0x47, Trap)).WillOnce(Return(ByMove(1)));
EXPECT_CALL(Process, ReadMemory(0x47, 1)).WillOnce(Return(ByMove(Trap)));
EXPECT_THAT_ERROR(Process.SetBreakpoint(0x47, 0, false).ToError(),
llvm::Succeeded());
}
// Remove the breakpoint for the first time. This should remove the breakpoint
// from m_software_breakpoints.
//
// Should succeed.
{
InSequence S;
EXPECT_CALL(Process, ReadMemory(0x47, 1)).WillOnce(Return(ByMove(Trap)));
EXPECT_CALL(Process, WriteMemory(0x47, llvm::ArrayRef(Original)))
.WillOnce(Return(ByMove(1)));
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(Original)));
EXPECT_THAT_ERROR(Process.RemoveBreakpoint(0x47, false).ToError(),
llvm::Succeeded());
}
// Remove the breakpoint for the second time.
//
// Should fail. None of the ReadMemory() or WriteMemory() should be called,
// because the function should early return when seeing that the breakpoint
// isn't in m_software_breakpoints.
{
EXPECT_CALL(Process, ReadMemory(_, _)).Times(0);
EXPECT_CALL(Process, WriteMemory(_, _)).Times(0);
EXPECT_THAT_ERROR(Process.RemoveBreakpoint(0x47, false).ToError(),
llvm::Failed());
}
}
TEST(NativeProcessProtocolTest, RemoveSoftwareBreakpointMemoryError) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("x86_64-pc-linux"));
auto Trap = cantFail(Process.GetSoftwareBreakpointTrapOpcode(1));
auto Original = std::vector<uint8_t>{0xbb};
auto SomethingElse = std::vector<uint8_t>{0xaa};
// Set up a breakpoint.
{
InSequence S;
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(Original)));
EXPECT_CALL(Process, WriteMemory(0x47, Trap)).WillOnce(Return(ByMove(1)));
EXPECT_CALL(Process, ReadMemory(0x47, 1)).WillOnce(Return(ByMove(Trap)));
EXPECT_THAT_ERROR(Process.SetBreakpoint(0x47, 0, false).ToError(),
llvm::Succeeded());
}
// Remove the breakpoint for the first time, with an unexpected value read by
// the first ReadMemory(). This should cause an early return, with the
// breakpoint removed from m_software_breakpoints.
//
// Should fail.
{
InSequence S;
EXPECT_CALL(Process, ReadMemory(0x47, 1))
.WillOnce(Return(ByMove(SomethingElse)));
EXPECT_THAT_ERROR(Process.RemoveBreakpoint(0x47, false).ToError(),
llvm::Failed());
}
// Remove the breakpoint for the second time.
//
// Should fail. None of the ReadMemory() or WriteMemory() should be called,
// because the function should early return when seeing that the breakpoint
// isn't in m_software_breakpoints.
{
EXPECT_CALL(Process, ReadMemory(_, _)).Times(0);
EXPECT_CALL(Process, WriteMemory(_, _)).Times(0);
EXPECT_THAT_ERROR(Process.RemoveBreakpoint(0x47, false).ToError(),
llvm::Failed());
}
}
TEST(NativeProcessProtocolTest, ReadMemoryWithoutTrap) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("aarch64-pc-linux"));
FakeMemory M{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}};
EXPECT_CALL(Process, ReadMemory(_, _))
.WillRepeatedly(Invoke(&M, &FakeMemory::Read));
EXPECT_CALL(Process, WriteMemory(_, _))
.WillRepeatedly(Invoke(&M, &FakeMemory::Write));
EXPECT_THAT_ERROR(Process.SetBreakpoint(0x4, 0, false).ToError(),
llvm::Succeeded());
EXPECT_THAT_EXPECTED(
Process.ReadMemoryWithoutTrap(0, 10),
llvm::HasValue(std::vector<uint8_t>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}));
EXPECT_THAT_EXPECTED(Process.ReadMemoryWithoutTrap(0, 6),
llvm::HasValue(std::vector<uint8_t>{0, 1, 2, 3, 4, 5}));
EXPECT_THAT_EXPECTED(Process.ReadMemoryWithoutTrap(6, 4),
llvm::HasValue(std::vector<uint8_t>{6, 7, 8, 9}));
EXPECT_THAT_EXPECTED(Process.ReadMemoryWithoutTrap(6, 2),
llvm::HasValue(std::vector<uint8_t>{6, 7}));
EXPECT_THAT_EXPECTED(Process.ReadMemoryWithoutTrap(4, 2),
llvm::HasValue(std::vector<uint8_t>{4, 5}));
}
TEST(NativeProcessProtocolTest, ReadCStringFromMemory) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("aarch64-pc-linux"));
FakeMemory M({'h', 'e', 'l', 'l', 'o', 0, 'w', 'o'});
EXPECT_CALL(Process, ReadMemory(_, _))
.WillRepeatedly(Invoke(&M, &FakeMemory::Read));
char string[1024];
size_t bytes_read;
EXPECT_THAT_EXPECTED(Process.ReadCStringFromMemory(
0x0, &string[0], sizeof(string), bytes_read),
llvm::HasValue(llvm::StringRef("hello")));
EXPECT_EQ(bytes_read, 6UL);
}
TEST(NativeProcessProtocolTest, ReadCStringFromMemory_MaxSize) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("aarch64-pc-linux"));
FakeMemory M({'h', 'e', 'l', 'l', 'o', 0, 'w', 'o'});
EXPECT_CALL(Process, ReadMemory(_, _))
.WillRepeatedly(Invoke(&M, &FakeMemory::Read));
char string[4];
size_t bytes_read;
EXPECT_THAT_EXPECTED(Process.ReadCStringFromMemory(
0x0, &string[0], sizeof(string), bytes_read),
llvm::HasValue(llvm::StringRef("hel")));
EXPECT_EQ(bytes_read, 3UL);
}
TEST(NativeProcessProtocolTest, ReadCStringFromMemory_CrossPageBoundary) {
NiceMock<MockDelegate> DummyDelegate;
MockProcess<NativeProcessProtocol> Process(DummyDelegate,
ArchSpec("aarch64-pc-linux"));
unsigned string_start = llvm::sys::Process::getPageSizeEstimate() - 3;
FakeMemory M({'h', 'e', 'l', 'l', 'o', 0, 'w', 'o'}, string_start);
EXPECT_CALL(Process, ReadMemory(_, _))
.WillRepeatedly(Invoke(&M, &FakeMemory::Read));
char string[1024];
size_t bytes_read;
EXPECT_THAT_EXPECTED(Process.ReadCStringFromMemory(string_start, &string[0],
sizeof(string),
bytes_read),
llvm::HasValue(llvm::StringRef("hello")));
EXPECT_EQ(bytes_read, 6UL);
}