diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index d227a66a703c1..68f58bf1349a7 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -831,6 +831,20 @@ def request_readMemory(self, memoryReference, offset, count): } return self.send_recv(command_dict) + def request_writeMemory(self, memoryReference, data, offset=0, allowPartial=True): + args_dict = { + "memoryReference": memoryReference, + "offset": offset, + "allowPartial": allowPartial, + "data": data, + } + command_dict = { + "command": "writeMemory", + "type": "request", + "arguments": args_dict, + } + return self.send_recv(command_dict) + def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None): stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) if stackFrame is None: diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py index 6299caf7631af..00b01d4bdb1c5 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py @@ -8,6 +8,7 @@ from lldbsuite.test.lldbtest import * from lldbsuite.test import lldbplatformutil import lldbgdbserverutils +import base64 class DAPTestCaseBase(TestBase): @@ -506,3 +507,20 @@ def getBuiltinDebugServerTool(self): self.dap_server.request_disconnect(terminateDebuggee=True) self.assertIsNotNone(server_tool, "debugserver not found.") return server_tool + + def writeMemory(self, memoryReference, data=None, offset=None, allowPartial=None): + # This function accepts data in decimal and hexadecimal format, + # converts it to a Base64 string, and send it to the DAP, + # which expects Base64 encoded data. + encodedData = ( + "" + if data is None + else base64.b64encode( + # (bit_length + 7 (rounding up to nearest byte) ) //8 = converts to bytes. + data.to_bytes((data.bit_length() + 7) // 8, "little") + ).decode() + ) + response = self.dap_server.request_writeMemory( + memoryReference, encodedData, offset=offset, allowPartial=allowPartial + ) + return response diff --git a/lldb/test/API/tools/lldb-dap/memory/TestDAP_memory.py b/lldb/test/API/tools/lldb-dap/memory/TestDAP_memory.py index 55fb4a961e783..2c3c1fe9cae6b 100644 --- a/lldb/test/API/tools/lldb-dap/memory/TestDAP_memory.py +++ b/lldb/test/API/tools/lldb-dap/memory/TestDAP_memory.py @@ -125,3 +125,85 @@ def test_readMemory(self): self.assertFalse(mem["success"], "expect fail on reading memory.") self.continue_to_exit() + + def test_writeMemory(self): + """ + Tests the 'writeMemory' request + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program) + source = "main.cpp" + self.source_path = os.path.join(os.getcwd(), source) + self.set_source_breakpoints( + source, + [line_number(source, "// Breakpoint")], + ) + self.continue_to_next_stop() + + # Get the 'not_a_ptr' writable variable reference address. + ptr_deref = self.dap_server.request_evaluate("not_a_ptr")["body"] + memref = ptr_deref["memoryReference"] + + # Write the decimal value 50 (0x32 in hexadecimal) to memory. + # This corresponds to the ASCII character '2' and encodes to Base64 as "Mg==". + mem_response = self.writeMemory(memref, 50, 0, True) + self.assertEqual(mem_response["success"], True) + self.assertEqual(mem_response["body"]["bytesWritten"], 1) + + # Read back the modified memory and verify that the written data matches + # the expected result. + mem_response = self.dap_server.request_readMemory(memref, 0, 1) + self.assertEqual(mem_response["success"], True) + self.assertEqual(mem_response["body"]["data"], "Mg==") + + # Write the decimal value 100 (0x64 in hexadecimal) to memory. + # This corresponds to the ASCII character 'd' and encodes to Base64 as "ZA==". + # allowPartial=False + mem_response = self.writeMemory(memref, 100, 0, False) + self.assertEqual(mem_response["success"], True) + self.assertEqual(mem_response["body"]["bytesWritten"], 1) + + # Read back the modified memory and verify that the written data matches + # the expected result. + mem_response = self.dap_server.request_readMemory(memref, 0, 1) + self.assertEqual(mem_response["success"], True) + self.assertEqual(mem_response["body"]["data"], "ZA==") + + # Memory write failed for 0x0. + mem_response = self.writeMemory("0x0", 50, 0, True) + self.assertEqual(mem_response["success"], False) + + # Malformed memory reference. + mem_response = self.writeMemory("12345", 50, 0, True) + self.assertEqual(mem_response["success"], False) + + ptr_deref = self.dap_server.request_evaluate("nonWritable")["body"] + memref = ptr_deref["memoryReference"] + + # Writing to non-writable region should return an appropriate error. + mem_response = self.writeMemory(memref, 50, 0, False) + self.assertEqual(mem_response["success"], False) + self.assertRegex( + mem_response["body"]["error"]["format"], + r"Memory " + memref + " region is not writable", + ) + + # Trying to write empty value; data="" + mem_response = self.writeMemory(memref) + self.assertEqual(mem_response["success"], False) + self.assertRegex( + mem_response["body"]["error"]["format"], + r"Data cannot be empty value. Provide valid data", + ) + + # Verify that large memory writes fail if the range spans non-writable + # or non -contiguous regions. + data = bytes([0xFF] * 8192) + mem_response = self.writeMemory( + memref, int.from_bytes(data, byteorder="little"), 0, False + ) + self.assertEqual(mem_response["success"], False) + self.assertRegex( + mem_response["body"]["error"]["format"], + r"Memory " + memref + " region is not writable", + ) diff --git a/lldb/test/API/tools/lldb-dap/memory/main.cpp b/lldb/test/API/tools/lldb-dap/memory/main.cpp index 0db7b78e93383..26467daea4f6c 100644 --- a/lldb/test/API/tools/lldb-dap/memory/main.cpp +++ b/lldb/test/API/tools/lldb-dap/memory/main.cpp @@ -1,6 +1,8 @@ int main() { int not_a_ptr = 666; const char *rawptr = "dead"; + // Immutable variable, .rodata region. + static const int nonWritable = 100; // Breakpoint return 0; } diff --git a/lldb/tools/lldb-dap/CMakeLists.txt b/lldb/tools/lldb-dap/CMakeLists.txt index 8bbb402fdf782..4cddfb1bea1c2 100644 --- a/lldb/tools/lldb-dap/CMakeLists.txt +++ b/lldb/tools/lldb-dap/CMakeLists.txt @@ -65,7 +65,8 @@ add_lldb_library(lldbDAP Handler/TestGetTargetBreakpointsRequestHandler.cpp Handler/ThreadsRequestHandler.cpp Handler/VariablesRequestHandler.cpp - + Handler/WriteMemoryRequestHandler.cpp + Protocol/ProtocolBase.cpp Protocol/ProtocolEvents.cpp Protocol/ProtocolTypes.cpp diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 60a32393a9b91..fd89f52595ec6 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -1554,6 +1554,7 @@ void DAP::RegisterRequests() { RegisterRequest(); RegisterRequest(); RegisterRequest(); + RegisterRequest(); // Custom requests RegisterRequest(); diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.h b/lldb/tools/lldb-dap/Handler/RequestHandler.h index 3b910abe81992..07b079d19896d 100644 --- a/lldb/tools/lldb-dap/Handler/RequestHandler.h +++ b/lldb/tools/lldb-dap/Handler/RequestHandler.h @@ -604,6 +604,19 @@ class TestGetTargetBreakpointsRequestHandler : public LegacyRequestHandler { void operator()(const llvm::json::Object &request) const override; }; +class WriteMemoryRequestHandler final + : public RequestHandler> { +public: + using RequestHandler::RequestHandler; + static llvm::StringLiteral GetCommand() { return "writeMemory"; } + FeatureSet GetSupportedFeatures() const override { + return {protocol::eAdapterFeatureWriteMemoryRequest}; + } + llvm::Expected + Run(const protocol::WriteMemoryArguments &args) const override; +}; + } // namespace lldb_dap #endif diff --git a/lldb/tools/lldb-dap/Handler/WriteMemoryRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/WriteMemoryRequestHandler.cpp new file mode 100644 index 0000000000000..7bd1c9f91ff47 --- /dev/null +++ b/lldb/tools/lldb-dap/Handler/WriteMemoryRequestHandler.cpp @@ -0,0 +1,100 @@ +//===-- WriteMemoryRequestHandler.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 "DAP.h" +#include "JSONUtils.h" +#include "RequestHandler.h" +#include "lldb/API/SBMemoryRegionInfo.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/Base64.h" + +namespace lldb_dap { + +// Writes bytes to memory at the provided location. +// +// Clients should only call this request if the corresponding capability +// supportsWriteMemoryRequest is true. +llvm::Expected +WriteMemoryRequestHandler::Run( + const protocol::WriteMemoryArguments &args) const { + const lldb::addr_t address = args.memoryReference + args.offset.value_or(0); + ; + + lldb::SBProcess process = dap.target.GetProcess(); + if (!lldb::SBDebugger::StateIsStoppedState(process.GetState())) + return llvm::make_error(); + + if (args.data.empty()) { + return llvm::make_error( + "Data cannot be empty value. Provide valid data"); + } + + // The VSCode IDE or other DAP clients send memory data as a Base64 string. + // This function decodes it into raw binary before writing it to the target + // process memory. + std::vector output; + auto decode_error = llvm::decodeBase64(args.data, output); + + if (decode_error) { + return llvm::make_error( + llvm::toString(std::move(decode_error)).c_str()); + } + + lldb::SBError write_error; + uint64_t bytes_written = 0; + + // Write the memory. + if (!output.empty()) { + lldb::SBProcess process = dap.target.GetProcess(); + // If 'allowPartial' is false or missing, a debug adapter should attempt to + // verify the region is writable before writing, and fail the response if it + // is not. + if (!args.allowPartial.value_or(false)) { + // Start checking from the initial write address. + lldb::addr_t start_address = address; + // Compute the end of the write range. + lldb::addr_t end_address = start_address + output.size() - 1; + + while (start_address <= end_address) { + // Get memory region info for the given address. + // This provides the region's base, end, and permissions + // (read/write/executable). + lldb::SBMemoryRegionInfo region_info; + lldb::SBError error = + process.GetMemoryRegionInfo(start_address, region_info); + // Fail if the region info retrieval fails, is not writable, or the + // range exceeds the region. + if (!error.Success() || !region_info.IsWritable()) { + return llvm::make_error( + "Memory 0x" + llvm::utohexstr(args.memoryReference) + + " region is not writable"); + } + // If the current region covers the full requested range, stop futher + // iterations. + if (end_address <= region_info.GetRegionEnd()) { + break; + } + // Move to the start of the next memory region. + start_address = region_info.GetRegionEnd() + 1; + } + } + + bytes_written = + process.WriteMemory(address, static_cast(output.data()), + output.size(), write_error); + } + + if (bytes_written == 0) { + return llvm::make_error(write_error.GetCString()); + } + protocol::WriteMemoryResponseBody response; + response.bytesWritten = bytes_written; + return response; +} + +} // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp index 124dbace97dd6..83a205f118fc0 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp @@ -531,4 +531,37 @@ json::Value toJSON(const ModulesResponseBody &MR) { return result; } +bool fromJSON(const json::Value &Params, WriteMemoryArguments &WMA, + json::Path P) { + json::ObjectMapper O(Params, P); + + const json::Object *wma_obj = Params.getAsObject(); + constexpr llvm::StringRef ref_key = "memoryReference"; + const std::optional memory_ref = wma_obj->getString(ref_key); + if (!memory_ref) { + P.field(ref_key).report("missing value"); + return false; + } + + const std::optional addr_opt = + DecodeMemoryReference(*memory_ref); + if (!addr_opt) { + P.field(ref_key).report("Malformed memory reference"); + return false; + } + + WMA.memoryReference = *addr_opt; + + return O && O.mapOptional("allowPartial", WMA.allowPartial) && + O.mapOptional("offset", WMA.offset) && O.map("data", WMA.data); +} + +json::Value toJSON(const WriteMemoryResponseBody &WMR) { + json::Object result; + + if (WMR.bytesWritten != 0) + result.insert({"bytesWritten", WMR.bytesWritten}); + return result; +} + } // namespace lldb_dap::protocol diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h index 26eb3cb396ad1..1544815be9389 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h +++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h @@ -896,6 +896,38 @@ struct ModulesResponseBody { }; llvm::json::Value toJSON(const ModulesResponseBody &); +/// Arguments for `writeMemory` request. +struct WriteMemoryArguments { + /// Memory reference to the base location to which data should be written. + lldb::addr_t memoryReference; + + /// Offset (in bytes) to be applied to the reference location before writing + /// data. Can be negative. + std::optional offset; + + /// Property to control partial writes. If true, the debug adapter should + /// attempt to write memory even if the entire memory region is not writable. + /// In such a case the debug adapter should stop after hitting the first byte + /// of memory that cannot be written and return the number of bytes written in + /// the response via the `offset` and `bytesWritten` properties. + /// If false or missing, a debug adapter should attempt to verify the region + /// is writable before writing, and fail the response if it is not. + std::optional allowPartial; + + /// Bytes to write, encoded using base64. + std::string data; +}; +bool fromJSON(const llvm::json::Value &, WriteMemoryArguments &, + llvm::json::Path); + +/// Response to writeMemory request. +struct WriteMemoryResponseBody { + /// Property that should be returned when `allowPartial` is true to indicate + /// the number of bytes starting from address that were successfully written. + uint64_t bytesWritten = 0; +}; +llvm::json::Value toJSON(const WriteMemoryResponseBody &); + } // namespace lldb_dap::protocol #endif