[llvm-branch-commits] [lldb] [lldb] Backport to 23 the fixes for problems exposed by the delayed breakpoints feature (PR #221244)

David Spickett via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Fri Sep 4 08:02:20 PDT 2026


https://github.com/DavidSpickett created https://github.com/llvm/llvm-project/pull/221244

https://github.com/llvm/llvm-project/pull/192971 added a delayed breakpoints feature, which delays sending the breakpoint management packets until we actually continue the process. The feature itself is working, but it has made it easier to hit a bunch of pre-existing issues handling reads and writes around breakpoints.

https://github.com/llvm/llvm-project/issues/217359, https://github.com/llvm/llvm-project/issues/217840, https://github.com/llvm/llvm-project/issues/217359, https://github.com/llvm/llvm-project/issues/205120 and https://github.com/llvm/llvm-project/issues/202672 (reported by a downstream project as https://github.com/llvm/llvm-project/issues/216000).

This PR backports to 23 a set of fixes for those issues. It is quite large, but ~560 of the ~660 new lines are tests.

89ad6f51f475 [lldb][test] Fix skip in TestWriteMemoryWithHWBreakpoint.py (#216723)
b955ea912ac1 [lldb][debugserver] Fix a bug in FindBreakpointsThatOverlapRange (#217009)
a132493da2f6 [LLDB][lldb-server] Handle writes over software breakpoint sites (#217348)
76d27c889614 [lldb][lldb-server] Fix compilation on 32-bit (#217934)
938e6446b039 [lldb][debugserver] Handle breakpoint prior to addr in RemoveTrapsFromBuffer (#217851)
d8ec70dc8db9 [lldb][debugserver] Fix bugs in FindBreakpointsThatOverlapRange (#217837)
9cda4668bc97 [lldb] Handle 0 size sites in StopPointSiteList::FindInRange (#217919)
0b98a7cd6353 [lldb][test] Enable TestWriteOverSoftwareBreakpoint for debugserver (#218362)
7ea6d83f49d9 [lldb][test] Disable two breakpoint tests for debugserver (#218636)
dfa87f357528 Reland "[lldb][test] Disable two breakpoint tests for debugserver" (#220845)
bf1874e420a3 [lldb] Update decorator in write-over-breakpoint tests (#220919)

>From 2416b034eb8c3949ccaf0a9c3f51e46ab8a2fae5 Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Mon, 17 Aug 2026 14:13:17 +0100
Subject: [PATCH 01/11] [lldb][test] Fix skip in
 TestWriteMemoryWithHWBreakpoint.py (#216723)

In a87b27fd5161ec43527fc3356852046a321ea82c, the opposite
skip was put in. It should skip if hardware breakpoints are
*not* supported.

Also that commit added a stray "skip". I have removed that and
fixed the incorrect variable name.
---
 .../TestWriteMemoryWithHWBreakpoint.py                       | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
index c82ae24a6d9ab..c7c4c09454ef0 100644
--- a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
@@ -13,8 +13,7 @@
 
 class WriteMemoryWithHWBreakpoint(HardwareBreakpointTestBase):
 
-    @skipTestIfFn(HardwareBreakpointTestBase.supports_hw_breakpoints)
-    @skip
+    @skipTestIfFn(HardwareBreakpointTestBase.hw_breakpoints_unsupported)
     def test_copy_memory_with_hw_break(self):
         self.build()
         exe = self.getBuildArtifact("a.out")
@@ -46,4 +45,4 @@ def test_copy_memory_with_hw_break(self):
         error = lldb.SBError()
 
         result = process.WriteMemory(address, data, error)
-        self.assertTrue(error.Success() and result == len(bytes))
+        self.assertTrue(error.Success() and result == len(data))

>From 2aaef556b4569d843516f3c4b94009fafc98b946 Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Thu, 20 Aug 2026 08:57:44 +0100
Subject: [PATCH 02/11] [lldb][debugserver] Fix a bug in
 FindBreakpointsThatOverlapRange (#217009)

This reverts commit a7ef89ad9796e6de3a085ec75a13f0dfec5a8059 to reland
with a fix for the reported failure on MacOS. I think this was due to a
bug in
`DNBBreakpointList::FindBreakpointsThatOverlapRange`.

In the second loop, `pos` is only incremented if `IntersectsRange`
returns true. It will
never return true for a hardware breakpoint because hardware breakpoints
are not
placed into actual memory.

This means that it gets stuck in that final while. I think if I had
access to the logs, I
would see lldb timing out the write memory request.

I made some small changes to the test case:
* Split the final assertion so we can tell if it failed entirely or
partially.
* Renamed the test case, since it's not "copying" anything.
---
 .../TestWriteMemoryWithHWBreakpoint.py                      | 5 +++--
 lldb/tools/debugserver/source/DNBBreakpoint.cpp             | 6 +++---
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
index c7c4c09454ef0..341ef444c66e0 100644
--- a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
@@ -14,7 +14,7 @@
 class WriteMemoryWithHWBreakpoint(HardwareBreakpointTestBase):
 
     @skipTestIfFn(HardwareBreakpointTestBase.hw_breakpoints_unsupported)
-    def test_copy_memory_with_hw_break(self):
+    def test_write_memory_with_hw_break(self):
         self.build()
         exe = self.getBuildArtifact("a.out")
 
@@ -45,4 +45,5 @@ def test_copy_memory_with_hw_break(self):
         error = lldb.SBError()
 
         result = process.WriteMemory(address, data, error)
-        self.assertTrue(error.Success() and result == len(data))
+        self.assertTrue(error.Success())
+        self.assertEqual(result, len(data))
diff --git a/lldb/tools/debugserver/source/DNBBreakpoint.cpp b/lldb/tools/debugserver/source/DNBBreakpoint.cpp
index e41bf9b4fd905..74f0fb17129f3 100644
--- a/lldb/tools/debugserver/source/DNBBreakpoint.cpp
+++ b/lldb/tools/debugserver/source/DNBBreakpoint.cpp
@@ -147,10 +147,10 @@ size_t DNBBreakpointList::FindBreakpointsThatOverlapRange(
         break;
 
       // Check if this breakpoint overlaps, and if it does, add it to the list
-      if (pos->second.IntersectsRange(addr, size, NULL, NULL, NULL)) {
+      if (pos->second.IntersectsRange(addr, size, NULL, NULL, NULL))
         bps.push_back(&pos->second);
-        ++pos;
-      }
+
+      ++pos;
     }
   }
   return bps.size();

>From 5173a907a2a5532103e29adbf6a0f48aadb76478 Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Fri, 21 Aug 2026 13:23:58 +0100
Subject: [PATCH 03/11] [LLDB][lldb-server] Handle writes over software
 breakpoint sites (#217348)

Fixes #202672

lldb-server was not handling them at all, and I found bugs in
debugserver's implementation. This change only fixes lldb-server, the
debugserver issue is tracked by #217359 and #217840 (though the added API tests can
be run for debugserver once that's done).

When data is written over a software breakpoint location, it should not
be written to memory but instead to the saved copy of the original data
at that location. If this is not done, writes corrupt the software break
instruction and it's no longer a valid breakpoint.

This is how it should work:
```
      Memory contents: ----abcd----
   Place a breakpoint: ----bkpt---- (saved bytes: abcd)
Then we write over it: --abcdefgh--
           The result: --abbkptgh-- (saved bytes: cdef)
Remove the breakpoint: --abcdefgh--
```
When the breakpoint is removed, the saved bytes are written to memory
and it looks just like you had written it all in the first place.

This is what was happening with lldb-server:
```
      Memory contents: ----abcd----
   Place a breakpoint: ----bkpt---- (saved bytes: abcd)
Then we write over it: --abcdefgh--
           The result: --abcdefgh-- (saved bytes: bkpt)
                                    (break no longer valid)
Remove the breakpoint: Errors because it doesn't see the
                       break instruction in memory.
```
debugserver already has an algorithm to work out how to do this. It's in
MachProcess::WriteMemory and what I've added to NativeProcessProtocol
follows the same basic logic.

* Assume breakpoints are sorted in ascending order of address (hence the
change from std::unordered_map to std::map).
* Skip forward to the first breakpoint that starts > the write address.
* If that breakpoint is not the first, go back one breakpoint. This
catches the case where a breakpoint starts earlier than the write but
still over laps it, or starts exactly when the write does.
* If the write address is before the first breakpoint, write up to that
breakpoint.
* If the write address is at or in a breakpoint, update that
breakpoint's saved bytes until you reach the end of it, or the write is
complete.
* If there's write data left, move the write address forward to just
beyond the breakpoint.
* Repeat for all breakpoints from that point on in the list, or until
the write is complete.
* Finally, check for any remaining bytes to be written after the last
breakpoint.

This means a single write is split into multiple sometimes. The
alternative is to make a copy of the write data, and patch the
breakpoint instructions into that to create 1 write request.

I decided not to do that because:
* debugserver does not do this, and we may need to compare them later.
* It is harder to reason about recovery if the single write fails.

NativeProcessProtocol ReadMemory is no longer abstract, instead there is
a lower level DoWriteMemory which sub-classes implement. DoWriteMemory
is what WriteMemory used to be, so some places call it directly because
they want to ignore breakpoint sites. I tried to avoid this extra
abstraction layer, but I do not see a good alternative.

Tests have been added at the NativeProccess level for different layouts.
The expected number of write calls is checked and breakpoint insertion
is tested in 2 orders if there is > 1.

There is an API test based on the steps reported in #216000. This test
should run on all architectures. For now it won't run for debugserver.

I have had to take a small risk to get some dead code immediately before
the breakpoint site. Short of using target specific assembly, I think
what I've got is as safe as we can be. Limiting this test to AArch64
would not be a big problem, as the same write splitting algorithm is
used on all architectures. However just on principle I would like it to
be as generic as it can be.

I did have to skip Windows, because I could not find a way to make the
breakpoint location label visible to lldb. The code being tested should
work on Windows, if someone can figure out the symbol part. The loss of
coverage is minimal because the algorithm under test is not OS specific.
---
 .../lldb/Host/common/NativeProcessProtocol.h  |  19 +-
 .../Host/common/NativeProcessProtocol.cpp     |  95 ++++++-
 .../Plugins/Process/AIX/NativeProcessAIX.cpp  |   4 +-
 .../Plugins/Process/AIX/NativeProcessAIX.h    |   4 +-
 .../Process/FreeBSD/NativeProcessFreeBSD.cpp  |   4 +-
 .../Process/FreeBSD/NativeProcessFreeBSD.h    |   4 +-
 .../Process/Linux/NativeProcessLinux.cpp      |  15 +-
 .../Process/Linux/NativeProcessLinux.h        |   4 +-
 .../Process/NetBSD/NativeProcessNetBSD.cpp    |   4 +-
 .../Process/NetBSD/NativeProcessNetBSD.h      |   4 +-
 .../Windows/Common/NativeProcessWindows.cpp   |   4 +-
 .../Windows/Common/NativeProcessWindows.h     |   4 +-
 .../write_over_software_breakpoint/Makefile   |   3 +
 .../TestWriteOverSoftwareBreakpoint.py        | 161 +++++++++++
 .../write_over_software_breakpoint/main.c     |  34 +++
 .../Mock/ProcessMockAccelerator.cpp           |   5 +-
 .../Accelerator/Mock/ProcessMockAccelerator.h |   4 +-
 .../Host/NativeProcessProtocolTest.cpp        | 264 ++++++++++++++++++
 .../Host/NativeProcessTestUtils.h             |  22 +-
 19 files changed, 610 insertions(+), 48 deletions(-)
 create mode 100644 lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/Makefile
 create mode 100644 lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
 create mode 100644 lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/main.c

diff --git a/lldb/include/lldb/Host/common/NativeProcessProtocol.h b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
index 435185a38f3f9..c70bd4d011383 100644
--- a/lldb/include/lldb/Host/common/NativeProcessProtocol.h
+++ b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
@@ -26,9 +26,9 @@
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/MemoryBuffer.h"
+#include <map>
 #include <mutex>
 #include <optional>
-#include <unordered_map>
 #include <vector>
 
 namespace lldb_private {
@@ -133,8 +133,10 @@ class NativeProcessProtocol {
   ReadCStringFromMemory(lldb::addr_t addr, char *buffer, size_t max_size,
                         size_t &total_bytes_read);
 
-  virtual Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
-                             size_t &bytes_written) = 0;
+  /// Write memory while not overwriting breakpoints in memory. The breakpoints'
+  /// saved bytes are updated with what would have been written.
+  Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                     size_t &bytes_written);
 
   virtual llvm::Expected<lldb::addr_t> AllocateMemory(size_t size,
                                                       uint32_t permissions) {
@@ -456,7 +458,9 @@ class NativeProcessProtocol {
     llvm::ArrayRef<uint8_t> breakpoint_opcodes;
   };
 
-  std::unordered_map<lldb::addr_t, SoftwareBreakpoint> m_software_breakpoints;
+  // Using std::map so that breakpoints are sorted in ascending address order.
+  // WriteMemory relies on this.
+  std::map<lldb::addr_t, SoftwareBreakpoint> m_software_breakpoints;
   lldb::pid_t m_pid;
 
   std::vector<std::unique_ptr<NativeThreadProtocol>> m_threads;
@@ -481,6 +485,13 @@ class NativeProcessProtocol {
   // Extensions enabled per the last SetEnabledExtensions() call.
   Extension m_enabled_extensions;
 
+  // Write to memory, with no awareness of software breakpoint sites. Used to
+  // implement WriteMemory. May be called directly for use cases that should
+  // ignore software breakpoint sites, for example adding or removing those
+  // breakpoints.
+  virtual Status DoWriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                               size_t &bytes_written) = 0;
+
   // lldb_private::Host calls should be used to launch a process for debugging,
   // and then the process should be attached to. When attaching to a process
   // lldb_private::Host calls should be used to locate the process to attach
diff --git a/lldb/source/Host/common/NativeProcessProtocol.cpp b/lldb/source/Host/common/NativeProcessProtocol.cpp
index dbffdc619ef42..b996424541907 100644
--- a/lldb/source/Host/common/NativeProcessProtocol.cpp
+++ b/lldb/source/Host/common/NativeProcessProtocol.cpp
@@ -18,6 +18,7 @@
 #include "lldb/lldb-enumerations.h"
 
 #include "llvm/Support/Process.h"
+#include <iterator>
 #include <optional>
 
 using namespace lldb;
@@ -396,7 +397,7 @@ Status NativeProcessProtocol::RemoveSoftwareBreakpoint(lldb::addr_t addr) {
     // We found a valid breakpoint opcode at this address, now restore the
     // saved opcode.
     size_t bytes_written = 0;
-    error = WriteMemory(addr, saved.data(), saved.size(), bytes_written);
+    error = DoWriteMemory(addr, saved.data(), saved.size(), bytes_written);
     if (error.Fail() || bytes_written < saved.size()) {
       return Status::FromErrorStringWithFormat(
           "addr=0x%" PRIx64 ": tried to write %zu bytes but only wrote %zu",
@@ -454,8 +455,8 @@ NativeProcessProtocol::EnableSoftwareBreakpoint(lldb::addr_t addr,
 
   // Write a software breakpoint in place of the original opcode.
   size_t bytes_written = 0;
-  error = WriteMemory(addr, expected_trap->data(), expected_trap->size(),
-                      bytes_written);
+  error = DoWriteMemory(addr, expected_trap->data(), expected_trap->size(),
+                        bytes_written);
   if (error.Fail())
     return error.ToError();
 
@@ -649,9 +650,91 @@ Status NativeProcessProtocol::RemoveBreakpoint(lldb::addr_t addr,
     return RemoveSoftwareBreakpoint(addr);
 }
 
-Status NativeProcessProtocol::ReadMemoryWithoutTrap(lldb::addr_t addr,
-                                                    void *buf, size_t size,
-                                                    size_t &bytes_read) {
+Status NativeProcessProtocol::WriteMemory(lldb::addr_t addr, const void *buf,
+                                          size_t size, size_t &bytes_written) {
+  Status error;
+  bytes_written = 0;
+
+  if (!size)
+    return error;
+
+  if (m_software_breakpoints.empty())
+    return DoWriteMemory(addr, buf, size, bytes_written);
+
+  // Find first breakpoint that starts >= addr.
+  std::map<lldb::addr_t, SoftwareBreakpoint>::iterator bkpt =
+      m_software_breakpoints.lower_bound(addr);
+
+  // it points to the first breakpoint starting at >= addr, but the one
+  // immediately before it may extend over addr, or begin exactly at addr.
+  if (bkpt != m_software_breakpoints.begin())
+    bkpt = std::prev(bkpt);
+
+  const uint8_t *byte_buf = static_cast<const uint8_t *>(buf);
+  for (; bkpt != m_software_breakpoints.end(); ++bkpt) {
+    auto &[sbp_addr, sbp_data] = *bkpt;
+    // If the address is before a breakpoint site, write up to the site, or to
+    // the end of the write. Whichever comes first.
+    if (addr < sbp_addr) {
+      const size_t to_write = std::min(size, size_t{sbp_addr - addr});
+      size_t part_bytes_written = 0;
+      error = DoWriteMemory(addr, byte_buf, to_write, part_bytes_written);
+      bytes_written += part_bytes_written;
+
+      if (error.Fail() || part_bytes_written < to_write) {
+        return Status::FromErrorStringWithFormat(
+            "addr=0x%" PRIx64 ": tried to write %zu bytes but only wrote %zu",
+            addr, to_write, part_bytes_written);
+      }
+
+      byte_buf += to_write;
+      addr += to_write;
+      size -= to_write;
+
+      if (!size)
+        break;
+    }
+
+    // If the address is within a breakpoint site, update the saved opcodes
+    // for that site.
+    if ((addr >= sbp_addr) &&
+        (addr < (sbp_addr + sbp_data.saved_opcodes.size()))) {
+      // Instead of writing this chunk, update the saved bytes in the
+      // breakpoint.
+      const size_t idx = addr - sbp_addr;
+      const size_t to_write =
+          std::min(size, sbp_data.saved_opcodes.size() - idx);
+      for (size_t copied = 0; copied < to_write;
+           ++bytes_written, ++byte_buf, ++addr, --size, ++copied)
+        sbp_data.saved_opcodes[idx + copied] = *byte_buf;
+    }
+
+    if (!size)
+      break;
+  }
+
+  // If the write range extends beyond the last breakpoint site, write the
+  // remaining data.
+  if (size) {
+    // Write the remaining part after the last breakpoint, or the whole range
+    // in the case that there were no breakpoints.
+    size_t part_bytes_written = 0;
+    error = DoWriteMemory(addr, byte_buf, size, part_bytes_written);
+    bytes_written += part_bytes_written;
+    if (error.Fail() || part_bytes_written < size) {
+      return Status::FromErrorStringWithFormat(
+          "addr=0x%" PRIx64 ": tried to write %zu bytes but only wrote %zu",
+          addr, size, part_bytes_written);
+    }
+  }
+
+  return Status();
+}
+
+Status
+NativeProcessProtocol::ReadMemoryWithoutTrap(const lldb::addr_t addr,
+                                             void *buf, size_t size,
+                                             size_t &bytes_read) {
   Status error = ReadMemory(addr, buf, size, bytes_read);
   if (error.Fail())
     return error;
diff --git a/lldb/source/Plugins/Process/AIX/NativeProcessAIX.cpp b/lldb/source/Plugins/Process/AIX/NativeProcessAIX.cpp
index 9c7e66cb79028..9792e217cdfa4 100644
--- a/lldb/source/Plugins/Process/AIX/NativeProcessAIX.cpp
+++ b/lldb/source/Plugins/Process/AIX/NativeProcessAIX.cpp
@@ -243,8 +243,8 @@ Status NativeProcessAIX::ReadMemory(lldb::addr_t addr, void *buf, size_t size,
   return Status("unsupported");
 }
 
-Status NativeProcessAIX::WriteMemory(lldb::addr_t addr, const void *buf,
-                                     size_t size, size_t &bytes_written) {
+Status NativeProcessAIX::DoWriteMemory(lldb::addr_t addr, const void *buf,
+                                       size_t size, size_t &bytes_written) {
   return Status("unsupported");
 }
 
diff --git a/lldb/source/Plugins/Process/AIX/NativeProcessAIX.h b/lldb/source/Plugins/Process/AIX/NativeProcessAIX.h
index bc44f2b02af98..4de7cb02f1426 100644
--- a/lldb/source/Plugins/Process/AIX/NativeProcessAIX.h
+++ b/lldb/source/Plugins/Process/AIX/NativeProcessAIX.h
@@ -80,8 +80,8 @@ class NativeProcessAIX : public NativeProcessProtocol {
   Status ReadMemory(lldb::addr_t addr, void *buf, size_t size,
                     size_t &bytes_read) override;
 
-  Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
-                     size_t &bytes_written) override;
+  Status DoWriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                       size_t &bytes_written) override;
 
   size_t UpdateThreads() override;
 
diff --git a/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.cpp b/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.cpp
index 4853ab2827d9e..82ac352e85cd5 100644
--- a/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.cpp
+++ b/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.cpp
@@ -898,8 +898,8 @@ Status NativeProcessFreeBSD::ReadMemory(lldb::addr_t addr, void *buf,
   return Status();
 }
 
-Status NativeProcessFreeBSD::WriteMemory(lldb::addr_t addr, const void *buf,
-                                         size_t size, size_t &bytes_written) {
+Status NativeProcessFreeBSD::DoWriteMemory(lldb::addr_t addr, const void *buf,
+                                           size_t size, size_t &bytes_written) {
   const unsigned char *src = static_cast<const unsigned char *>(buf);
   Status error;
   struct ptrace_io_desc io;
diff --git a/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.h b/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.h
index 7e8bdc527f420..90c830b8e5f66 100644
--- a/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.h
+++ b/lldb/source/Plugins/Process/FreeBSD/NativeProcessFreeBSD.h
@@ -62,8 +62,8 @@ class NativeProcessFreeBSD : public NativeProcessELF {
   Status ReadMemory(lldb::addr_t addr, void *buf, size_t size,
                     size_t &bytes_read) override;
 
-  Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
-                     size_t &bytes_written) override;
+  Status DoWriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                       size_t &bytes_written) override;
 
   size_t UpdateThreads() override;
 
diff --git a/lldb/source/Plugins/Process/Linux/NativeProcessLinux.cpp b/lldb/source/Plugins/Process/Linux/NativeProcessLinux.cpp
index 80f1b5662ba61..9b309fee41dde 100644
--- a/lldb/source/Plugins/Process/Linux/NativeProcessLinux.cpp
+++ b/lldb/source/Plugins/Process/Linux/NativeProcessLinux.cpp
@@ -1347,8 +1347,9 @@ NativeProcessLinux::Syscall(llvm::ArrayRef<uint64_t> args) {
     return std::move(Err);
   }
 
-  llvm::scope_exit restore_mem(
-      [&] { WriteMemory(exe_addr, memory.data(), memory.size(), bytes_read); });
+  llvm::scope_exit restore_mem([&] {
+    DoWriteMemory(exe_addr, memory.data(), memory.size(), bytes_read);
+  });
 
   if (llvm::Error Err = reg_ctx.SetPC(exe_addr).ToError())
     return std::move(Err);
@@ -1361,8 +1362,8 @@ NativeProcessLinux::Syscall(llvm::ArrayRef<uint64_t> args) {
       return std::move(Err);
     }
   }
-  if (llvm::Error Err = WriteMemory(exe_addr, syscall_data.Insn.data(),
-                                    syscall_data.Insn.size(), bytes_read)
+  if (llvm::Error Err = DoWriteMemory(exe_addr, syscall_data.Insn.data(),
+                                      syscall_data.Insn.size(), bytes_read)
                             .ToError())
     return std::move(Err);
 
@@ -1665,8 +1666,8 @@ Status NativeProcessLinux::ReadMemory(lldb::addr_t addr, void *buf, size_t size,
   return Status();
 }
 
-Status NativeProcessLinux::WriteMemory(lldb::addr_t addr, const void *buf,
-                                       size_t size, size_t &bytes_written) {
+Status NativeProcessLinux::DoWriteMemory(lldb::addr_t addr, const void *buf,
+                                         size_t size, size_t &bytes_written) {
   const unsigned char *src = static_cast<const unsigned char *>(buf);
   size_t remainder;
   Status error;
@@ -1697,7 +1698,7 @@ Status NativeProcessLinux::WriteMemory(lldb::addr_t addr, const void *buf,
       memcpy(buff, src, remainder);
 
       size_t bytes_written_rec;
-      error = WriteMemory(addr, buff, k_ptrace_word_size, bytes_written_rec);
+      error = DoWriteMemory(addr, buff, k_ptrace_word_size, bytes_written_rec);
       if (error.Fail())
         return error;
 
diff --git a/lldb/source/Plugins/Process/Linux/NativeProcessLinux.h b/lldb/source/Plugins/Process/Linux/NativeProcessLinux.h
index 936d690e42ae7..d255c1e0df18b 100644
--- a/lldb/source/Plugins/Process/Linux/NativeProcessLinux.h
+++ b/lldb/source/Plugins/Process/Linux/NativeProcessLinux.h
@@ -99,8 +99,8 @@ class NativeProcessLinux : public NativeProcessELF,
   Status ReadMemory(lldb::addr_t addr, void *buf, size_t size,
                     size_t &bytes_read) override;
 
-  Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
-                     size_t &bytes_written) override;
+  Status DoWriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                       size_t &bytes_written) override;
 
   llvm::Expected<lldb::addr_t> AllocateMemory(size_t size,
                                               uint32_t permissions) override;
diff --git a/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.cpp b/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.cpp
index 3fd14c4c43071..702b2bbca4440 100644
--- a/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.cpp
+++ b/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.cpp
@@ -926,8 +926,8 @@ Status NativeProcessNetBSD::ReadMemory(lldb::addr_t addr, void *buf,
   return Status();
 }
 
-Status NativeProcessNetBSD::WriteMemory(lldb::addr_t addr, const void *buf,
-                                        size_t size, size_t &bytes_written) {
+Status NativeProcessNetBSD::DoWriteMemory(lldb::addr_t addr, const void *buf,
+                                          size_t size, size_t &bytes_written) {
   const unsigned char *src = static_cast<const unsigned char *>(buf);
   Status error;
   struct ptrace_io_desc io;
diff --git a/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.h b/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.h
index 976d48e74854e..d27042c1a6a79 100644
--- a/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.h
+++ b/lldb/source/Plugins/Process/NetBSD/NativeProcessNetBSD.h
@@ -60,8 +60,8 @@ class NativeProcessNetBSD : public NativeProcessELF {
   Status ReadMemory(lldb::addr_t addr, void *buf, size_t size,
                     size_t &bytes_read) override;
 
-  Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
-                     size_t &bytes_written) override;
+  Status DoWriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                       size_t &bytes_written) override;
 
   lldb::addr_t GetSharedLibraryInfoAddress() override;
 
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index f87fd23f5a047..1601d3f2f0f1f 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -237,8 +237,8 @@ Status NativeProcessWindows::ReadMemory(lldb::addr_t addr, void *buf,
   return ProcessDebugger::ReadMemory(addr, buf, size, bytes_read);
 }
 
-Status NativeProcessWindows::WriteMemory(lldb::addr_t addr, const void *buf,
-                                         size_t size, size_t &bytes_written) {
+Status NativeProcessWindows::DoWriteMemory(lldb::addr_t addr, const void *buf,
+                                           size_t size, size_t &bytes_written) {
   return ProcessDebugger::WriteMemory(addr, buf, size, bytes_written);
 }
 
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index 17469f18fbc73..82b82d109b1e4 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -71,8 +71,8 @@ class NativeProcessWindows : public NativeProcessProtocol,
   Status ReadMemory(lldb::addr_t addr, void *buf, size_t size,
                     size_t &bytes_read) override;
 
-  Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
-                     size_t &bytes_written) override;
+  Status DoWriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                       size_t &bytes_written) override;
 
   llvm::Expected<lldb::addr_t> AllocateMemory(size_t size,
                                               uint32_t permissions) override;
diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/Makefile b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
new file mode 100644
index 0000000000000..4d33382fe3bd9
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
@@ -0,0 +1,161 @@
+"""
+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 *
+
+
+class WriteOverSoftwareBreakpoint(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    # Could not find a way to make place_break_here visible to lldb on Windows.
+    @skipIfWindows
+    # debugserver needs fixing, see:
+    # https://github.com/llvm/llvm-project/issues/217359
+    # https://github.com/llvm/llvm-project/issues/217840
+    @llgs_test
+    def test_write_over_breakpoint(self):
+        TestBase.setUp(self)
+        self.line = line_number("main.c", "// break here")
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
+
+        lldbutil.run_break_set_by_file_and_line(
+            self, "main.c", self.line, num_expected_locations=1, loc_exact=True
+        )
+        self.runCmd("run", RUN_SUCCEEDED)
+        self.expect(
+            "thread list",
+            STOPPED_DUE_TO_BREAKPOINT,
+            substrs=["stopped", "stop reason = breakpoint"],
+        )
+
+        target = self.dbg.GetSelectedTarget()
+        process = target.GetProcess()
+
+        loop_start_breakpoint_addr = (
+            target.breakpoints[0].GetLocationAtIndex(0).GetLoadAddress()
+        )
+
+        # 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())
diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/main.c b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/main.c
new file mode 100644
index 0000000000000..4c9100dda604a
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/main.c
@@ -0,0 +1,34 @@
+volatile int always_false;
+
+int foo() {
+  // This nop is deliberately dead code so that we have an instruction
+  // immediately prior to the breakpoint that we are allowed to corrupt.
+  if (always_false) {
+    // This dead code must be at least 4 bytes even on an architecture where
+    // nop is 1 byte.
+    asm volatile("nop\n"
+                 "nop\n"
+                 "nop\n"
+                 "nop\n");
+  }
+  // We are assuming that there are no instructions placed between the dead code
+  // above and the assembly below. This is not guaranteed but it's safer than
+  // assuming that this function will have no prologue instructions or breaking
+  // in the very first instruction of foo and hoping whatever comes before is
+  // not important.
+  asm volatile(".globl place_break_here\n"
+               "place_break_here:\n"
+               // The test will repeatedly add and remove a breakpoint here.
+               "nop");
+
+  return 0;
+}
+
+int main() {
+  volatile int sum = 0;
+  // Pick a number of loops >= the number of write patterns being tested.
+  for (unsigned i = 0; i < 10; ++i)
+    sum += foo(); // break here
+
+  return 0;
+}
diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp
index 174ab1a0143d4..e6ed3b695452c 100644
--- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp
+++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp
@@ -65,8 +65,9 @@ Status ProcessMockAccelerator::ReadMemory(lldb::addr_t addr, void *buf,
   return Status::FromErrorString("unimplemented");
 }
 
-Status ProcessMockAccelerator::WriteMemory(lldb::addr_t addr, const void *buf,
-                                           size_t size, size_t &bytes_written) {
+Status ProcessMockAccelerator::DoWriteMemory(lldb::addr_t addr, const void *buf,
+                                             size_t size,
+                                             size_t &bytes_written) {
   bytes_written = 0;
   return Status::FromErrorString("unimplemented");
 }
diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h
index 6fc07b5ac011a..d475e0d983775 100644
--- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h
+++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h
@@ -42,8 +42,8 @@ class ProcessMockAccelerator : public NativeProcessProtocol {
 
   Status ReadMemory(lldb::addr_t addr, void *buf, size_t size,
                     size_t &bytes_read) override;
-  Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size,
-                     size_t &bytes_written) override;
+  Status DoWriteMemory(lldb::addr_t addr, const void *buf, size_t size,
+                       size_t &bytes_written) override;
 
   lldb::addr_t GetSharedLibraryInfoAddress() override;
   size_t UpdateThreads() override;
diff --git a/lldb/unittests/Host/NativeProcessProtocolTest.cpp b/lldb/unittests/Host/NativeProcessProtocolTest.cpp
index 91c4fd69d6e54..e5756d1db9f10 100644
--- a/lldb/unittests/Host/NativeProcessProtocolTest.cpp
+++ b/lldb/unittests/Host/NativeProcessProtocolTest.cpp
@@ -238,3 +238,267 @@ TEST(NativeProcessProtocolTest, ReadCStringFromMemory_CrossPageBoundary) {
                        llvm::HasValue(llvm::StringRef("hello")));
   EXPECT_EQ(bytes_read, 6UL);
 }
+
+void DoTestWriteMemoryPreservingTrap(
+    const std::vector<lldb::addr_t> &bp_addrs,
+    std::optional<lldb::addr_t> write_addr,
+    const std::vector<uint8_t> &write_data, uint32_t expected_number_of_writes,
+    const std::vector<uint8_t> expected_after_write_read_memory,
+    const std::vector<uint8_t> expected_after_write_read_memory_without_trap) {
+  NiceMock<MockDelegate> DummyDelegate;
+  MockProcess<NativeProcessProtocol> Process(DummyDelegate,
+                                             ArchSpec("aarch64-pc-linux"));
+  const std::vector<uint8_t> fake_memory{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+  FakeMemory M{fake_memory};
+  ON_CALL(Process, ReadMemory(_, _))
+      .WillByDefault(Invoke(&M, &FakeMemory::Read));
+  ON_CALL(Process, WriteMemory(_, _))
+      .WillByDefault(Invoke(&M, &FakeMemory::Write));
+
+  for (auto bp_addr : bp_addrs)
+    EXPECT_THAT_ERROR(
+        Process.SetBreakpoint(bp_addr, 0, /*hardware=*/false).ToError(),
+        llvm::Succeeded());
+
+  if (expected_number_of_writes)
+    EXPECT_CALL(Process, WriteMemory(_, _))
+        .Times(expected_number_of_writes)
+        .WillRepeatedly(DoDefault());
+  else
+    EXPECT_CALL(Process, WriteMemory(_, _)).Times(0);
+
+  if (write_addr) {
+    size_t bytes_written = 0;
+    Status err = Process.WriteMemory(*write_addr, write_data.data(),
+                                     write_data.size(), bytes_written);
+    EXPECT_THAT_ERROR(err.ToError(), llvm::Succeeded());
+    EXPECT_EQ(bytes_written, write_data.size());
+  }
+
+  Mock::VerifyAndClearExpectations(&Process);
+
+  // Anything written over a breakpoint should go into the saved bytes instead
+  // of into memory.
+
+  // ReadMemory should show that the breakpoint instructions are unchanged.
+  auto memory_or_err = Process.ReadMemory(0, fake_memory.size());
+  EXPECT_THAT_EXPECTED(memory_or_err, llvm::Succeeded());
+  EXPECT_EQ(*memory_or_err, expected_after_write_read_memory);
+
+  // ReadMemoryWithoutTrap should show that writes to the breakpoints have
+  // updated the saved data in the breakpoint.
+  memory_or_err = Process.ReadMemoryWithoutTrap(0, fake_memory.size());
+  EXPECT_THAT_EXPECTED(memory_or_err, llvm::Succeeded());
+  EXPECT_EQ(*memory_or_err, expected_after_write_read_memory_without_trap);
+
+  // When the breakpoint is removed, the saved bytes are actually written
+  // to memory.
+  for (auto bp_addr : bp_addrs)
+    EXPECT_THAT_ERROR(Process.RemoveBreakpoint(bp_addr, false).ToError(),
+                      llvm::Succeeded());
+
+  // The memory should contain the written data now.
+  memory_or_err = Process.ReadMemory(0, fake_memory.size());
+  EXPECT_THAT_EXPECTED(memory_or_err, llvm::Succeeded());
+  EXPECT_EQ(*memory_or_err, expected_after_write_read_memory_without_trap);
+
+  // As there are no breakpoints, the result of ReadMemoryWithoutTrap should
+  // be the same.
+  memory_or_err = Process.ReadMemoryWithoutTrap(0, fake_memory.size());
+  EXPECT_THAT_EXPECTED(memory_or_err, llvm::Succeeded());
+  EXPECT_EQ(*memory_or_err, expected_after_write_read_memory_without_trap);
+}
+
+void TestWriteMemoryPreservingTrap(
+    const std::vector<lldb::addr_t> &bp_addrs,
+    std::optional<lldb::addr_t> write_addr,
+    const std::vector<uint8_t> &write_data, uint32_t expected_number_of_writes,
+    const std::vector<uint8_t> expected_after_write_read_memory,
+    const std::vector<uint8_t> expected_after_write_read_memory_without_trap) {
+  auto bp_addrs_in_order = bp_addrs;
+  DoTestWriteMemoryPreservingTrap(
+      bp_addrs_in_order, write_addr, write_data, expected_number_of_writes,
+      expected_after_write_read_memory,
+      expected_after_write_read_memory_without_trap);
+
+  // WriteMemoryPreservingTrap should not care in what order the breakpoints
+  // were inserted.
+  if (bp_addrs_in_order.size()) {
+    std::reverse(bp_addrs_in_order.begin(), bp_addrs_in_order.end());
+    DoTestWriteMemoryPreservingTrap(
+        bp_addrs_in_order, write_addr, write_data, expected_number_of_writes,
+        expected_after_write_read_memory,
+        expected_after_write_read_memory_without_trap);
+  }
+}
+
+TEST(NativeProcessProtocolTest, WriteMemoryPreservingTrap) {
+// Software breakpoint instruction encoding for AArch64.
+#define S__W__B__P 0x0, 0x0, 0x20, 0xd4
+
+  // In these tests, numbers are used for the initial memory contents and
+  // letters for the data being written. These variables are used for letters
+  // so that the inputs can be vertically aligned.
+  uint8_t a = 'a';
+  uint8_t b = 'b';
+  uint8_t c = 'c';
+  uint8_t d = 'd';
+  uint8_t e = 'e';
+  uint8_t f = 'f';
+  uint8_t g = 'g';
+  uint8_t h = 'h';
+  uint8_t i = 'i';
+  uint8_t j = 'j';
+  uint8_t k = 'k';
+
+  // Write nothing, set no breakpoints, nothing changes.
+  TestWriteMemoryPreservingTrap({}, std::nullopt, {}, 0,
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+  // Write nothing, set a breakpoint. Breakpoint encoding should be visible in
+  // memory.
+  TestWriteMemoryPreservingTrap({0}, std::nullopt, {}, 0,
+                                {S__W__B__P, 4, 5, 6, 7, 8, 9, 10},
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+  // 0 size write with no breakpoints set.
+  TestWriteMemoryPreservingTrap({}, 0, {}, 0,
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+  // 0 size write not within a breakpoint.
+  TestWriteMemoryPreservingTrap({4}, 0, {}, 0,
+                                {0, 1, 2, 3, S__W__B__P, 8, 9, 10},
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+  // 0 size write at start of a breakpoint.
+  TestWriteMemoryPreservingTrap({4}, 4, {}, 0,
+                                {0, 1, 2, 3, S__W__B__P, 8, 9, 10},
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+  // 0 size write at end of a breakpoint.
+  TestWriteMemoryPreservingTrap({4}, 7, {}, 0,
+                                {0, 1, 2, 3, S__W__B__P, 8, 9, 10},
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+  // 0 size write one beyond the end of a breakpoint.
+  TestWriteMemoryPreservingTrap({4}, 8, {}, 0,
+                                {0, 1, 2, 3, S__W__B__P, 8, 9, 10},
+                                {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+  // Write something but set no breakpoints. Data is written directly to memory.
+  TestWriteMemoryPreservingTrap({}, 2, {a, b}, 1,
+                                {0, 1, a, b, 4, 5, 6, 7, 8, 9, 10},
+                                {0, 1, a, b, 4, 5, 6, 7, 8, 9, 10});
+
+  // Write before a breakpoint and do not overlap it.
+  TestWriteMemoryPreservingTrap({4}, 1, {a, b, c}, 1,
+                                {0, a, b, c, S__W__B__P, 8, 9, 10},
+                                {0, a, b, c, 4, 5, 6, 7, 8, 9, 10});
+
+  // Write within a breakpoint, without writing outside of it. Data kept in
+  // saved bytes until removal.
+  TestWriteMemoryPreservingTrap({4}, 4, {a, b, c, d}, 0,
+                                {0, 1, 2, 3, S__W__B__P, 8, 9, 10},
+                                {0, 1, 2, 3, a, b, c, d, 8, 9, 10});
+
+  // Write immediately after a breakpoint. Data is written directly to memory.
+  TestWriteMemoryPreservingTrap({2}, 6, {a, b, c}, 1,
+                                {0, 1, S__W__B__P, a, b, c, 9, 10},
+                                {0, 1, 2, 3, 4, 5, a, b, c, 9, 10});
+
+  // Write a range overlapping the beginning of a breakpoint. First part will
+  // go direct to memory, the other to the saved bytes.
+  TestWriteMemoryPreservingTrap({2}, 0, {a, b, c, d}, 1,
+                                {a, b, S__W__B__P, 6, 7, 8, 9, 10},
+                                {a, b, c, d, 4, 5, 6, 7, 8, 9, 10});
+
+  // Write overlapping end of breakpoint. First part goes to saved bytes,
+  // second part direct to memory.
+  TestWriteMemoryPreservingTrap({3}, 5, {a, b, c, d}, 1,
+                                {0, 1, 2, S__W__B__P, c, d, 9, 10},
+                                {0, 1, 2, 3, 4, a, b, c, d, 9, 10});
+
+  // Write from before to after break. Ends go to memory, middle to saved bytes.
+  TestWriteMemoryPreservingTrap({2}, 0, {a, b, c, d, e, f, g, h}, 2,
+                                {a, b, S__W__B__P, g, h, 8, 9, 10},
+                                {a, b, c, d, e, f, g, h, 8, 9, 10});
+
+  // Overlap a breakpoint at the very start of memory.
+  TestWriteMemoryPreservingTrap({0}, 0, {a, b, c, d, e, f}, 1,
+                                {S__W__B__P, e, f, 6, 7, 8, 9, 10},
+                                {a, b, c, d, e, f, 6, 7, 8, 9, 10});
+
+  // Overlap a breakpoint at the very end of memory.
+  TestWriteMemoryPreservingTrap({7}, 5, {a, b, c, d, e, f}, 1,
+                                {0, 1, 2, 3, 4, a, b, S__W__B__P},
+                                {0, 1, 2, 3, 4, a, b, c, d, e, f});
+
+  // Write up to a breakpoint.
+  TestWriteMemoryPreservingTrap({5}, 1, {a, b, c, d}, 1,
+                                {0, a, b, c, d, S__W__B__P, 9, 10},
+                                {0, a, b, c, d, 5, 6, 7, 8, 9, 10});
+
+  // Write starting immediately after a breakpoint.
+  TestWriteMemoryPreservingTrap({2}, 6, {a, b, c, d}, 1,
+                                {0, 1, S__W__B__P, a, b, c, d, 10},
+                                {0, 1, 2, 3, 4, 5, a, b, c, d, 10});
+
+  // Overlap 2 breakpoints, write extends before and after them.
+  TestWriteMemoryPreservingTrap({1, 6}, 0, {a, b, c, d, e, f, g, h, i, j, k}, 3,
+                                {a, S__W__B__P, f, S__W__B__P, k},
+                                {a, b, c, d, e, f, g, h, i, j, k});
+
+  // Write starts within the first one, and ends after the second one.
+  TestWriteMemoryPreservingTrap({1, 6}, 2, {a, b, c, d, e, f, g, h, i}, 2,
+                                {0, S__W__B__P, d, S__W__B__P, i},
+                                {0, 1, a, b, c, d, e, f, g, h, i});
+
+  // Write range from before first breakpoint to within second breakpoint.
+  TestWriteMemoryPreservingTrap({1, 6}, 0, {a, b, c, d, e, f, g, h, i}, 2,
+                                {a, S__W__B__P, f, S__W__B__P, 10},
+                                {a, b, c, d, e, f, g, h, i, 9, 10});
+
+  // Write range from within first to beyond second.
+  TestWriteMemoryPreservingTrap({1, 6}, 2, {a, b, c, d, e, f, g, h, i}, 2,
+                                {0, S__W__B__P, d, S__W__B__P, i},
+                                {0, 1, a, b, c, d, e, f, g, h, i});
+
+  // Write range from within first to within second.
+  TestWriteMemoryPreservingTrap({1, 6}, 2, {a, b, c, d, e, f, g}, 1,
+                                {0, S__W__B__P, d, S__W__B__P, 10},
+                                {0, 1, a, b, c, d, e, f, g, 9, 10});
+
+  // Write in range between 2 breakpoints.
+  TestWriteMemoryPreservingTrap({0, 7}, 4, {a, b, c}, 1,
+                                {S__W__B__P, a, b, c, S__W__B__P},
+                                {0, 1, 2, 3, a, b, c, 7, 8, 9, 10});
+
+  // 2 breakpoints with no gap between them, write overlaps the first one.
+  TestWriteMemoryPreservingTrap({2, 6}, 0, {a, b, c, d}, 1,
+                                {a, b, S__W__B__P, S__W__B__P, 10},
+                                {a, b, c, d, 4, 5, 6, 7, 8, 9, 10});
+
+  // 2 breakpoints with no gap between them, write is within the first one.
+  TestWriteMemoryPreservingTrap({2, 6}, 3, {a, b}, 0,
+                                {0, 1, S__W__B__P, S__W__B__P, 10},
+                                {0, 1, 2, a, b, 5, 6, 7, 8, 9, 10});
+
+  // 2 breakpoints with no gap between them, write is across both.
+  TestWriteMemoryPreservingTrap({2, 6}, 4, {a, b, c, d}, 0,
+                                {0, 1, S__W__B__P, S__W__B__P, 10},
+                                {0, 1, 2, 3, a, b, c, d, 8, 9, 10});
+
+  // 2 breakpoints with no gap between them, write is within second one.
+  TestWriteMemoryPreservingTrap({2, 6}, 7, {a, b}, 0,
+                                {0, 1, S__W__B__P, S__W__B__P, 10},
+                                {0, 1, 2, 3, 4, 5, 6, a, b, 9, 10});
+
+  // 2 breakpoints with no gap between them, write overlaps second one.
+  TestWriteMemoryPreservingTrap({2, 6}, 8, {a, b, c}, 1,
+                                {0, 1, S__W__B__P, S__W__B__P, c},
+                                {0, 1, 2, 3, 4, 5, 6, 7, a, b, c});
+
+#undef S__W__B__P
+}
diff --git a/lldb/unittests/TestingSupport/Host/NativeProcessTestUtils.h b/lldb/unittests/TestingSupport/Host/NativeProcessTestUtils.h
index 1a017122411a8..005d199febaf1 100644
--- a/lldb/unittests/TestingSupport/Host/NativeProcessTestUtils.h
+++ b/lldb/unittests/TestingSupport/Host/NativeProcessTestUtils.h
@@ -83,10 +83,20 @@ template <typename T> class MockProcess : public T {
     return Status();
   }
 
-  Status WriteMemory(addr_t Addr, const void *Buf, size_t Size,
-                     size_t &BytesWritten) /*override*/ {
+  using T::WriteMemory;
+
+  MOCK_METHOD2(ReadMemory,
+               llvm::Expected<std::vector<uint8_t>>(addr_t Addr, size_t Size));
+  MOCK_METHOD2(WriteMemory,
+               llvm::Expected<size_t>(addr_t Addr,
+                                      llvm::ArrayRef<uint8_t> Data));
+
+  Status DoWriteMemory(addr_t Addr, const void *Buf, size_t Size,
+                       size_t &BytesWritten) /*override*/ {
     auto ExpectedBytes = this->WriteMemory(
-        Addr, llvm::ArrayRef(static_cast<const uint8_t *>(Buf), Size));
+        Addr,
+        llvm::ArrayRef(const_cast<uint8_t *>(static_cast<const uint8_t *>(Buf)),
+                       Size));
     if (!ExpectedBytes) {
       BytesWritten = 0;
       return Status::FromError(ExpectedBytes.takeError());
@@ -95,12 +105,6 @@ template <typename T> class MockProcess : public T {
     return Status();
   }
 
-  MOCK_METHOD2(ReadMemory,
-               llvm::Expected<std::vector<uint8_t>>(addr_t Addr, size_t Size));
-  MOCK_METHOD2(WriteMemory,
-               llvm::Expected<size_t>(addr_t Addr,
-                                      llvm::ArrayRef<uint8_t> Data));
-
   using T::GetSoftwareBreakpointTrapOpcode;
   llvm::Expected<std::vector<uint8_t>> ReadMemoryWithoutTrap(addr_t Addr,
                                                              size_t Size) {

>From a8f070829d97fc1095b3298a3111c09ce68342fa Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Fri, 21 Aug 2026 16:09:46 +0100
Subject: [PATCH 04/11] [lldb][lldb-server] Fix compilation on 32-bit (#217934)

Fixes #217348 / a132493da2f60a08cc66c75fa0dd12ccb3fe9c26

NativeProcessProtocol.cpp:679:53:
error: non-constant-expression cannot be narrowed from type 'unsigned
long long' to 'size_t' (aka 'unsigned int') in initializer list
[-Wc++11-narrowing]
679 | const size_t to_write = std::min(size, size_t{sbp_addr - addr});
| ^~~~~~~~~~~~~~~

On 32-bit, size_t is 32-bit but addr_t is still a 64-bit type.

sbp_addr and addr are addr_t, so it makes more sense to cast size to
that. Which is a widening on 32-bit and a nop on 64-bit.
---
 lldb/source/Host/common/NativeProcessProtocol.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lldb/source/Host/common/NativeProcessProtocol.cpp b/lldb/source/Host/common/NativeProcessProtocol.cpp
index b996424541907..2d905d0539801 100644
--- a/lldb/source/Host/common/NativeProcessProtocol.cpp
+++ b/lldb/source/Host/common/NativeProcessProtocol.cpp
@@ -676,7 +676,8 @@ Status NativeProcessProtocol::WriteMemory(lldb::addr_t addr, const void *buf,
     // If the address is before a breakpoint site, write up to the site, or to
     // the end of the write. Whichever comes first.
     if (addr < sbp_addr) {
-      const size_t to_write = std::min(size, size_t{sbp_addr - addr});
+      const size_t to_write =
+          std::min(static_cast<addr_t>(size), sbp_addr - addr);
       size_t part_bytes_written = 0;
       error = DoWriteMemory(addr, byte_buf, to_write, part_bytes_written);
       bytes_written += part_bytes_written;

>From ce9e5c551cb97dc7e3f68ab0da6ae7b206b274be Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Mon, 24 Aug 2026 09:25:58 +0100
Subject: [PATCH 05/11] [lldb][debugserver] Handle breakpoint prior to addr in
 RemoveTrapsFromBuffer (#217851)

Fixes #217840

lower_bound finds the first breakpoint that starts >= addr. There might
be a breakpoint before that which starts before addr but extends past
addr. Therefore it might need to be patched out of the buffer.

This fix is intentionally minimal as I don't have a Mac to test it in.

A proper fix would reuse FindBreakpointsThatOverlapRange, which also has
this bug but is getting fixed. The problem with that is that
RemoveTrapsFromBuffer is const, and FindBreakpointsThatOverlapRange
returns non const pointers to the breakpoints.

Not super complex to fix but more than I want to do at a distance.
---
 lldb/tools/debugserver/source/DNBBreakpoint.cpp | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/lldb/tools/debugserver/source/DNBBreakpoint.cpp b/lldb/tools/debugserver/source/DNBBreakpoint.cpp
index 74f0fb17129f3..531a049bfd803 100644
--- a/lldb/tools/debugserver/source/DNBBreakpoint.cpp
+++ b/lldb/tools/debugserver/source/DNBBreakpoint.cpp
@@ -171,9 +171,18 @@ void DNBBreakpointList::DisableAll() {
 
 void DNBBreakpointList::RemoveTrapsFromBuffer(nub_addr_t addr, nub_size_t size,
                                               void *p) const {
+  if (m_breakpoints.empty())
+    return;
+
   uint8_t *buf = (uint8_t *)p;
   const_iterator end = m_breakpoints.end();
   const_iterator pos = m_breakpoints.lower_bound(addr);
+
+  // lower_bound finds a breakpoint starting >= addr. The breakpoint prior to
+  // that may start before addr but extend beyond it, so it must be checked too.
+  if (pos != m_breakpoints.begin())
+    --pos;
+
   while (pos != end && (pos->first < (addr + size))) {
     nub_addr_t intersect_addr;
     nub_size_t intersect_size;

>From f36d2d4e31768c35ca132bef3221197c9b92e880 Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Mon, 24 Aug 2026 09:26:21 +0100
Subject: [PATCH 06/11] [lldb][debugserver] Fix bugs in
 FindBreakpointsThatOverlapRange (#217837)

Fixes #217359

The first issue is obvious, when checking the previous breakpoint we
should push back prev_pos->second, instead of pos->second.

The second problem is what happens when lower_bound returns end().
Before, `if (pos != end)` would stop us checking the previous
breakpoint.

If lower_bound returned end() but there were breakpoints, the last
breakpoint may start before addr and extend past it. This was being
missed.

To fix that:
* Remove the `if (pos != end)` check.
* Return early if there are no breakpoints.
* If lower_bound does not find the first one, look at the previous one.
* Look at the rest from pos onwards.

The earlly return means we know that
m_breakpoints.begin() != m_breakpoints.end() and so even if lower_bound
returns end(), we are safe to decrement that iterator.

I think this will fix the API tests in #217348, when those are enabled
from debugserver.
---
 .../debugserver/source/DNBBreakpoint.cpp      | 46 ++++++++++---------
 1 file changed, 25 insertions(+), 21 deletions(-)

diff --git a/lldb/tools/debugserver/source/DNBBreakpoint.cpp b/lldb/tools/debugserver/source/DNBBreakpoint.cpp
index 531a049bfd803..ce390a85f54e0 100644
--- a/lldb/tools/debugserver/source/DNBBreakpoint.cpp
+++ b/lldb/tools/debugserver/source/DNBBreakpoint.cpp
@@ -125,34 +125,38 @@ DNBBreakpointList::FindNearestWatchpoint(nub_addr_t addr) const {
 size_t DNBBreakpointList::FindBreakpointsThatOverlapRange(
     nub_addr_t addr, nub_addr_t size, std::vector<DNBBreakpoint *> &bps) {
   bps.clear();
+
+  if (m_breakpoints.empty())
+    return bps.size();
+
   iterator end = m_breakpoints.end();
   // Find the first breakpoint with an address >= to "addr"
   iterator pos = m_breakpoints.lower_bound(addr);
-  if (pos != end) {
-    if (pos != m_breakpoints.begin()) {
-      // Watch out for a breakpoint at an address less than "addr" that might
-      // still overlap
-      iterator prev_pos = pos;
-      --prev_pos;
-      if (prev_pos->second.IntersectsRange(addr, size, NULL, NULL, NULL))
-        bps.push_back(&pos->second);
-    }
 
-    while (pos != end) {
-      // When we hit a breakpoint whose start address is greater than "addr +
-      // size" we are done.
-      // Do the math in a way that doesn't risk unsigned overflow with bad
-      // input.
-      if ((pos->second.Address() - addr) >= size)
-        break;
+  if (pos != m_breakpoints.begin()) {
+    // Watch out for a breakpoint at an address less than "addr" that might
+    // still overlap
+    iterator prev_pos = pos;
+    --prev_pos;
+    if (prev_pos->second.IntersectsRange(addr, size, NULL, NULL, NULL))
+      bps.push_back(&prev_pos->second);
+  }
+
+  while (pos != end) {
+    // When we hit a breakpoint whose start address is greater than "addr +
+    // size" we are done.
+    // Do the math in a way that doesn't risk unsigned overflow with bad
+    // input.
+    if ((pos->second.Address() - addr) >= size)
+      break;
 
-      // Check if this breakpoint overlaps, and if it does, add it to the list
-      if (pos->second.IntersectsRange(addr, size, NULL, NULL, NULL))
-        bps.push_back(&pos->second);
+    // Check if this breakpoint overlaps, and if it does, add it to the list
+    if (pos->second.IntersectsRange(addr, size, NULL, NULL, NULL))
+      bps.push_back(&pos->second);
 
-      ++pos;
-    }
+    ++pos;
   }
+
   return bps.size();
 }
 

>From a5f852ee5054148746662ab276fe356dc042e61d Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Mon, 24 Aug 2026 10:15:56 +0100
Subject: [PATCH 07/11] [lldb] Handle 0 size sites in
 StopPointSiteList::FindInRange (#217919)

Fixes #205120

In which due to delayed breakpoints, a breakpoint that would become an
external breakpoint later (meaning managed by the debug server) was
temporarily stored as a software breakpoint (which is managed by lldb)
with a zero size breakpoint site. That zero site site tripped an
assertion when you tried to write over the site.

To fix this, I've explicitly ignored zero size sites in FindInRange by
defining them as never overlapping. FindInRange is only used for
patching reads and writes, so I think this is safe to do. I have
documented this in the docstring.

I considered adding a breakpoint type "uncommitted", but software
breakpoints are actually handled in the most conservative manner (reads
and writes are always patched). So I think as a default it's fine (also
I don't want to go and audit all the places that use that enum and end
up with a way larger fix for this issue).

Though I have a few debugserver fixes in flight, since this is a fix in
lldb, I think the added API test will work for lldb-server and
debugserver.
---
 .../lldb/Breakpoint/StopPointSiteList.h       | 11 ++-
 .../TestWriteOverSoftwareBreakpoint.py        | 88 +++++++++++++++++++
 2 files changed, 97 insertions(+), 2 deletions(-)

diff --git a/lldb/include/lldb/Breakpoint/StopPointSiteList.h b/lldb/include/lldb/Breakpoint/StopPointSiteList.h
index 101eccda4616b..088c2b3c1ee1d 100644
--- a/lldb/include/lldb/Breakpoint/StopPointSiteList.h
+++ b/lldb/include/lldb/Breakpoint/StopPointSiteList.h
@@ -182,6 +182,9 @@ template <typename StopPointSite> class StopPointSiteList {
     return false;
   }
 
+  /// Find breakpoint sites that in any way overlap the range starting at
+  /// \a lower_bound and ending at \a upper_bound (but not including it).
+  /// Zero sized sites are treated as never overlapping.
   bool FindInRange(lldb::addr_t lower_bound, lldb::addr_t upper_bound,
                    StopPointSiteList &bp_site_list) const {
     if (lower_bound > upper_bound)
@@ -200,14 +203,18 @@ template <typename StopPointSite> class StopPointSiteList {
       typename collection::const_iterator prev_pos = lower;
       prev_pos--;
       const StopPointSiteSP &prev_site = (*prev_pos).second;
-      if (prev_site->GetLoadAddress() + prev_site->GetByteSize() > lower_bound)
+      if (prev_site->GetByteSize() != 0 &&
+          (prev_site->GetLoadAddress() + prev_site->GetByteSize() >
+           lower_bound))
         bp_site_list.Add(prev_site);
     }
 
     upper = m_site_list.upper_bound(upper_bound);
 
     for (pos = lower; pos != upper; pos++)
-      bp_site_list.Add((*pos).second);
+      if (pos->second->GetByteSize() != 0)
+        bp_site_list.Add(pos->second);
+
     return true;
   }
 
diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
index 4d33382fe3bd9..6b9f599ef2bc2 100644
--- a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
@@ -159,3 +159,91 @@ def test_write_over_breakpoint(self):
             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):
+        TestBase.setUp(self)
+        self.line = line_number("main.c", "// break here")
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
+
+        self.runCmd("settings set target.process.use-delayed-breakpoints true")
+
+        lldbutil.run_break_set_by_file_and_line(
+            self, "main.c", self.line, num_expected_locations=1, loc_exact=True
+        )
+        self.runCmd("run", RUN_SUCCEEDED)
+        self.expect(
+            "thread list",
+            STOPPED_DUE_TO_BREAKPOINT,
+            substrs=["stopped", "stop reason = breakpoint"],
+        )
+
+        target = self.dbg.GetSelectedTarget()
+        process = target.GetProcess()
+
+        loop_start_breakpoint_addr = (
+            target.breakpoints[0].GetLocationAtIndex(0).GetLoadAddress()
+        )
+
+        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()

>From 1859194e790c9865d5402fae366b50ca61bb1c75 Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Mon, 24 Aug 2026 10:50:30 +0100
Subject: [PATCH 08/11] [lldb][test] Enable TestWriteOverSoftwareBreakpoint for
 debugserver (#218362)

Both the mentioned issues have been fixed.
---
 .../TestWriteOverSoftwareBreakpoint.py                        | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
index 6b9f599ef2bc2..2abae29c837ea 100644
--- a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
@@ -15,10 +15,6 @@ class WriteOverSoftwareBreakpoint(TestBase):
 
     # Could not find a way to make place_break_here visible to lldb on Windows.
     @skipIfWindows
-    # debugserver needs fixing, see:
-    # https://github.com/llvm/llvm-project/issues/217359
-    # https://github.com/llvm/llvm-project/issues/217840
-    @llgs_test
     def test_write_over_breakpoint(self):
         TestBase.setUp(self)
         self.line = line_number("main.c", "// break here")

>From cb012d986d8aa58dbc99c845ec55d7d1031f24f5 Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Tue, 25 Aug 2026 09:30:31 +0100
Subject: [PATCH 09/11] [lldb][test] Disable two breakpoint tests for
 debugserver (#218636)

These are failing in Green Dragon's LLDB:Sanitized build.
---
 .../TestWriteMemoryWithHWBreakpoint.py                          | 2 ++
 .../TestWriteOverSoftwareBreakpoint.py                          | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
index 341ef444c66e0..1fb9060902b16 100644
--- a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
@@ -14,6 +14,8 @@
 class WriteMemoryWithHWBreakpoint(HardwareBreakpointTestBase):
 
     @skipTestIfFn(HardwareBreakpointTestBase.hw_breakpoints_unsupported)
+    # Fails with a sanitized build of debugserver.
+    @llgs_test
     def test_write_memory_with_hw_break(self):
         self.build()
         exe = self.getBuildArtifact("a.out")
diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
index 2abae29c837ea..471684f61e152 100644
--- a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
@@ -15,6 +15,8 @@ class WriteOverSoftwareBreakpoint(TestBase):
 
     # Could not find a way to make place_break_here visible to lldb on Windows.
     @skipIfWindows
+    # Fails with a sanitized build of debugserver.
+    @llgs_test
     def test_write_over_breakpoint(self):
         TestBase.setUp(self)
         self.line = line_number("main.c", "// break here")

>From abeabec51b8c2902bf365d927cecec6656b6b2ec Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at arm.com>
Date: Thu, 3 Sep 2026 09:37:49 +0100
Subject: [PATCH 10/11] Reland "[lldb][test] Disable two breakpoint tests for
 debugserver" (#220845)

Reverts llvm/llvm-project#220615

These tests are still failing in Swift CI despite apparently working at
desk:
https://ci.swift.org/view/all/job/llvm.org/job/lldb-cmake-sanitized/1529/

Tracking this in https://github.com/llvm/llvm-project/issues/220844.
---
 .../TestWriteMemoryWithHWBreakpoint.py                          | 2 +-
 .../TestWriteOverSoftwareBreakpoint.py                          | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
index 1fb9060902b16..de90f8f65e218 100644
--- a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
@@ -14,7 +14,7 @@
 class WriteMemoryWithHWBreakpoint(HardwareBreakpointTestBase):
 
     @skipTestIfFn(HardwareBreakpointTestBase.hw_breakpoints_unsupported)
-    # Fails with a sanitized build of debugserver.
+    # Fails with a sanitized build of debugserver. https://github.com/llvm/llvm-project/issues/220844
     @llgs_test
     def test_write_memory_with_hw_break(self):
         self.build()
diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
index 471684f61e152..d023624454443 100644
--- a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
@@ -15,7 +15,7 @@ class WriteOverSoftwareBreakpoint(TestBase):
 
     # Could not find a way to make place_break_here visible to lldb on Windows.
     @skipIfWindows
-    # Fails with a sanitized build of debugserver.
+    # Fails with a sanitized build of debugserver. https://github.com/llvm/llvm-project/issues/220844
     @llgs_test
     def test_write_over_breakpoint(self):
         TestBase.setUp(self)

>From 8c3d36216b17e73b2f46e8102ce20c4959720781 Mon Sep 17 00:00:00 2001
From: Felipe de Azevedo Piovezan <fpiovezan at apple.com>
Date: Thu, 3 Sep 2026 08:52:30 -0400
Subject: [PATCH 11/11] [lldb] Update decorator in write-over-breakpoint tests
 (#220919)

sanitized bots don't use the just-built debugserver, so any tests
exercising new debugserver features won't work on them.
---
 .../TestWriteMemoryWithHWBreakpoint.py                         | 3 +--
 .../TestWriteOverSoftwareBreakpoint.py                         | 3 +--
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
index de90f8f65e218..402c152c05224 100644
--- a/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/hardware_breakpoints/write_memory_with_hw_breakpoint/TestWriteMemoryWithHWBreakpoint.py
@@ -14,8 +14,7 @@
 class WriteMemoryWithHWBreakpoint(HardwareBreakpointTestBase):
 
     @skipTestIfFn(HardwareBreakpointTestBase.hw_breakpoints_unsupported)
-    # Fails with a sanitized build of debugserver. https://github.com/llvm/llvm-project/issues/220844
-    @llgs_test
+    @skipIfOutOfTreeDebugserver
     def test_write_memory_with_hw_break(self):
         self.build()
         exe = self.getBuildArtifact("a.out")
diff --git a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
index d023624454443..1821e0fbfe7f3 100644
--- a/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
+++ b/lldb/test/API/functionalities/breakpoint/write_over_software_breakpoint/TestWriteOverSoftwareBreakpoint.py
@@ -15,8 +15,7 @@ class WriteOverSoftwareBreakpoint(TestBase):
 
     # Could not find a way to make place_break_here visible to lldb on Windows.
     @skipIfWindows
-    # Fails with a sanitized build of debugserver. https://github.com/llvm/llvm-project/issues/220844
-    @llgs_test
+    @skipIfOutOfTreeDebugserver
     def test_write_over_breakpoint(self):
         TestBase.setUp(self)
         self.line = line_number("main.c", "// break here")



More information about the llvm-branch-commits mailing list