From 80f770eb5752322a320b003b45dc79b9e14a5fba Mon Sep 17 00:00:00 2001 From: Nourhan Hasan Date: Tue, 2 Jun 2026 23:09:24 -0700 Subject: [PATCH] [vm/service]: Add _readNativeMemory RPC and OS::SafeReadMemory for safe native memory inspection - Declare OS::SafeReadMemory in os.h - Implement using pread64(/proc/self/mem) on Linux and Android - Add _readNativeMemory VM Service RPC with pre-checks for null and address overflow TEST=runtime/vm/service_test.cc Cq-Include-Trybots: dart/try:vm-asan-linux-release-x64-try,vm-asan-mac-release-arm64-try,vm-asan-win-release-x64-try,vm-dyn-linux-debug-x64-try,vm-dyn-mac-debug-arm64-try,vm-ffi-qemu-linux-release-arm-try,vm-ffi-qemu-linux-release-riscv64-try,vm-linux-debug-arm64-try,vm-linux-debug-ia32-try,vm-linux-debug-simriscv32-try,vm-linux-debug-simriscv64-try,vm-linux-debug-x64-try,vm-linux-debug-x64c-try,vm-linux-release-simarm-try,vm-mac-debug-arm64-try,vm-mac-debug-x64-try,vm-msan-linux-release-x64-try,vm-reload-linux-debug-x64-try,vm-tsan-linux-release-x64-try,vm-tsan-mac-release-arm64-try,vm-ubsan-linux-release-x64-try,vm-ubsan-mac-release-arm64-try,vm-ubsan-win-release-x64-try,vm-win-debug-arm64-try,vm-win-debug-x64-try,vm-win-debug-x64c-try Change-Id: Id15a82bf478bc4822c08d7fdf0a5c8bfd71a1fe0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505781 Auto-Submit: Nourhan Hasan Reviewed-by: Ben Konyi Reviewed-by: Daco Harkes --- runtime/tests/vm/vm.status | 8 ++ runtime/vm/json_stream.h | 1 + runtime/vm/os.h | 7 ++ runtime/vm/os_android.cc | 20 +++++ runtime/vm/os_linux.cc | 19 ++++ runtime/vm/service.cc | 121 ++++++++++++++++++++++++++ runtime/vm/service_test.cc | 173 +++++++++++++++++++++++++++++++++++++ 7 files changed, 349 insertions(+) diff --git a/runtime/tests/vm/vm.status b/runtime/tests/vm/vm.status index c9057f7f8e6..e796797276a 100644 --- a/runtime/tests/vm/vm.status +++ b/runtime/tests/vm/vm.status @@ -232,10 +232,18 @@ dart/data_uri_spawn_test: Skip # TODO(zra): package:unittest is not in the image dart/spawn_shutdown_test: Skip # OOM crash can bring down the OS. [ $system == macos ] +cc/Service_ReadNativeMemory_InvalidAddress: Skip +cc/Service_ReadNativeMemory_LargeRead: Skip +cc/Service_ReadNativeMemory_NullAddress: Skip +cc/Service_ReadNativeMemory_ValidAddress: Skip dart/transferable_throws_oom_test: SkipByDesign # Allocating too much memory to cause OOM doesn't work on mac [ $system == windows ] cc/CorelibCompilerStats: Skip +cc/Service_ReadNativeMemory_InvalidAddress: Skip +cc/Service_ReadNativeMemory_LargeRead: Skip +cc/Service_ReadNativeMemory_NullAddress: Skip +cc/Service_ReadNativeMemory_ValidAddress: Skip dart/disassemble_determinism_test: Slow, Pass # Times out on slower bots. [ $system != windows ] diff --git a/runtime/vm/json_stream.h b/runtime/vm/json_stream.h index 365be80f0a0..5426e9d97cd 100644 --- a/runtime/vm/json_stream.h +++ b/runtime/vm/json_stream.h @@ -69,6 +69,7 @@ enum JSONRpcErrorCode { kFileSystemAlreadyExists = 1001, kFileSystemDoesNotExist = 1002, kFileDoesNotExist = 1003, + kNativeMemoryReadError = 1004, }; // Builds on JSONWriter to provide support for serializing various objects diff --git a/runtime/vm/os.h b/runtime/vm/os.h index 000ef77dcd2..22831ade4f6 100644 --- a/runtime/vm/os.h +++ b/runtime/vm/os.h @@ -69,6 +69,13 @@ class OS { // determined. static uintptr_t CurrentRSS(); + // Safely reads size bytes from native address into buffer. + // Returns true on success, false if the address is invalid. + static bool SafeReadMemory(uintptr_t address, + uint8_t* buffer, + intptr_t size_in_bytes, + const char** error); + // Sleep the currently executing thread for millis ms. static void Sleep(int64_t millis); diff --git a/runtime/vm/os_android.cc b/runtime/vm/os_android.cc index a5cf6b52ce1..c78c789ef1c 100644 --- a/runtime/vm/os_android.cc +++ b/runtime/vm/os_android.cc @@ -12,6 +12,7 @@ #include // NOLINT #include // NOLINT #include // NOLINT +#include // NOLINT #include // NOLINT #include // NOLINT #include // NOLINT @@ -220,6 +221,25 @@ uintptr_t OS::CurrentRSS() { return current_rss_pages * getpagesize(); } +bool OS::SafeReadMemory(uintptr_t address, + uint8_t* buffer, + intptr_t size_in_bytes, + const char** error) { + int fd = open("/proc/self/mem", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + *error = strerror(errno); + return false; + } + ssize_t bytes_read = + pread64(fd, buffer, size_in_bytes, static_cast(address)); + close(fd); + if (bytes_read == -1) { + *error = strerror(errno); + return false; + } + return bytes_read == static_cast(size_in_bytes); +} + void OS::Sleep(int64_t millis) { int64_t micros = millis * kMicrosecondsPerMillisecond; SleepMicros(micros); diff --git a/runtime/vm/os_linux.cc b/runtime/vm/os_linux.cc index 9d2693a9bfc..9f48a0026ae 100644 --- a/runtime/vm/os_linux.cc +++ b/runtime/vm/os_linux.cc @@ -556,6 +556,25 @@ uintptr_t OS::CurrentRSS() { return current_rss_pages * getpagesize(); } +bool OS::SafeReadMemory(uintptr_t address, + uint8_t* buffer, + intptr_t size_in_bytes, + const char** error) { + int fd = open("/proc/self/mem", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + *error = strerror(errno); + return false; + } + ssize_t bytes_read = + pread64(fd, buffer, size_in_bytes, static_cast(address)); + close(fd); + if (bytes_read == -1) { + *error = strerror(errno); + return false; + } + return bytes_read == static_cast(size_in_bytes); +} + void OS::Sleep(int64_t millis) { int64_t micros = millis * kMicrosecondsPerMillisecond; SleepMicros(micros); diff --git a/runtime/vm/service.cc b/runtime/vm/service.cc index 0e49302c107..e2f98a81a4e 100644 --- a/runtime/vm/service.cc +++ b/runtime/vm/service.cc @@ -192,6 +192,8 @@ class NoSuchParameter : public MethodParameter { #define NO_ISOLATE_PARAMETER new NoSuchParameter("isolateId") #define RUNNABLE_ISOLATE_PARAMETER new RunnableIsolateParameter("isolateId") #define OBJECT_PARAMETER new IdParameter("objectId", true) +#define NATIVE_MEMORY_ADDRESS_PARAMETER new StringParameter("address", true) +#define NATIVE_MEMORY_SIZE_PARAMETER new UIntParameter("size", true) static bool ValidateUIntParameter(const char* value) { if (value == nullptr) { @@ -6235,6 +6237,124 @@ static void GetDefaultClassesAliases(Thread* thread, JSONStream* js) { #undef DEFINE_ADD_VALUE_F } +static constexpr const char* kNativeMemoryAddressParam = "address"; +static constexpr const char* kNativeMemorySizeParam = "size"; +static constexpr const char* kNativeMemoryBytesKey = "bytes"; +static constexpr const char* kNativeMemoryTypeKey = "type"; + +static void ReadNativeMemoryHelper(JSONStream* js, + uintptr_t address, + intptr_t size) { + if (address == 0) { + js->PrintError(kInvalidParams, "null pointer"); + return; + } + + if (address > (UINTPTR_MAX - static_cast(size))) { + js->PrintError(kInvalidParams, "address + size overflows address space"); + return; + } + + CAllocUniquePtr buffer(reinterpret_cast(malloc(size))); + + if (buffer.get() == nullptr) { + js->PrintError(kInternalError, "failed to allocate buffer"); + return; + } + + const char* read_error = nullptr; + +#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID) + bool ok = OS::SafeReadMemory(address, buffer.get(), size, &read_error); +#elif defined(DART_HOST_OS_MACOS) || defined(DART_HOST_OS_IOS) + bool ok = false; // TODO(thenourhan): implement using mach_vm_read +#elif defined(DART_HOST_OS_WINDOWS) + bool ok = false; // TODO(thenourhan): implement using ReadProcessMemory +#else + bool ok = false; // TODO(thenourhan): implement for other platforms +#endif + + if (!ok) { + const char* error_msg = + (read_error != nullptr) ? read_error : "address not readable"; + js->PrintError(kNativeMemoryReadError, "%s", error_msg); + return; + } + + CStringUniquePtr hex(reinterpret_cast(malloc(size * 2 + 1))); + for (intptr_t i = 0; i < size; i++) { + Utils::SNPrint(hex.get() + i * 2, 3, "%02x", buffer.get()[i]); + } + hex.get()[size * 2] = '\0'; + + JSONObject response(js); + response.AddProperty(kNativeMemoryTypeKey, "NativeMemory"); + response.AddPropertyF(kNativeMemoryAddressParam, "0x%" Px "", address); + response.AddProperty64(kNativeMemorySizeParam, size); + response.AddProperty(kNativeMemoryBytesKey, hex.get()); +} + +static const MethodParameter* const read_native_memory_params[] = { + NATIVE_MEMORY_ADDRESS_PARAMETER, + NATIVE_MEMORY_SIZE_PARAMETER, + nullptr, +}; + +// Parameters: +// address : string +// Hex string without '0x' prefix (e.g. "7f3a00001000"). +// +// size : int +// Number of bytes to read. Must be between 1 and 1048576 (1 MB). +// +// Responses: +// +// On success: +// { +// "type": "NativeMemory", +// "address": "0x7f3a00001000", +// "size": 8, +// "bytes": "0102030405060708" +// } +// +// On read failure (unmapped/invalid address): +// JSON-RPC error code 1004 with OS error string as details +// "Input/output error" (Linux EIO) +// "ReadProcessMemory failed (error 299)" (Windows) +// "mach_vm_read_overwrite failed" (macOS) +// +// On invalid params (null pointer, address overflow): +// JSON-RPC error code -32602 "Invalid params" +// +static void ReadNativeMemory(Thread* thread, JSONStream* js) { + const char* address_str = js->LookupParam(kNativeMemoryAddressParam); + const char* size_str = js->LookupParam(kNativeMemorySizeParam); + + if (address_str == nullptr) { + PrintMissingParamError(js, kNativeMemoryAddressParam); + return; + } + if (size_str == nullptr) { + PrintMissingParamError(js, kNativeMemorySizeParam); + return; + } + + uintptr_t address = 0; + intptr_t size = 0; + + if (!GetUnsignedIntegerId(address_str, &address, 16)) { + PrintInvalidParamError(js, kNativeMemoryAddressParam); + return; + } + if (!GetIntegerId(size_str, &size) || size <= 0 || size > 1 * MB) { + PrintInvalidParamError(js, kNativeMemorySizeParam); + return; + } + + ReadNativeMemoryHelper(js, static_cast(address), + static_cast(size)); +} + // clang-format off static const ServiceMethodDescriptor service_methods_[] = { { "_echo", Echo, @@ -6402,6 +6522,7 @@ static const ServiceMethodDescriptor service_methods_[] = { collect_all_garbage_params }, { "_getDefaultClassesAliases", GetDefaultClassesAliases, get_default_classes_aliases_params }, + { "_readNativeMemory", ReadNativeMemory, read_native_memory_params }, }; // clang-format on diff --git a/runtime/vm/service_test.cc b/runtime/vm/service_test.cc index 5d3d90eb720..a454ae2bdf4 100644 --- a/runtime/vm/service_test.cc +++ b/runtime/vm/service_test.cc @@ -692,6 +692,179 @@ ISOLATE_UNIT_TEST_CASE(Service_EmbedderIsolateHandler) { handler.msg()); } +ISOLATE_UNIT_TEST_CASE(Service_ReadNativeMemory_ValidAddress) { + const char* kScript = + "@pragma('vm:entry-point', 'set')\n" + "var port;\n" + "main() {}\n"; + + Isolate* isolate = thread->isolate(); + isolate->set_is_runnable(true); + Dart_Handle lib; + { + TransitionVMToNative transition(thread); + lib = TestCase::LoadTestScript(kScript, nullptr); + EXPECT_VALID(lib); + Dart_Handle result = Dart_Invoke(lib, NewString("main"), 0, nullptr); + EXPECT_VALID(result); + } + + uint8_t buffer[8] = {0x01, 0xAB, 0x0F, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF}; + uintptr_t address = reinterpret_cast(buffer); + + ServiceTestMessageHandler handler; + Dart_Port port_id = PortMap::CreatePort(&handler); + Dart_Handle port = Api::NewHandle(thread, SendPort::New(port_id)); + { + TransitionVMToNative transition(thread); + EXPECT_VALID(port); + EXPECT_VALID(Dart_SetField(lib, NewString("port"), port)); + } + + Array& service_msg = Array::Handle(); + + // send _readNativeMemory RPC with valid address + service_msg = EvalF(lib, + "[0, port, '0', '_readNativeMemory', false, " + "['address', 'size'], ['%" Px "', '8']]", + address); + HandleIsolateMessage(isolate, service_msg); + EXPECT_EQ(MessageHandler::kOK, handler.HandleNextMessage()); + + EXPECT_SUBSTRING("\"type\":\"NativeMemory\"", handler.msg()); + EXPECT_SUBSTRING("\"bytes\":\"01ab0fffdeadbeef\"", handler.msg()); +} + +ISOLATE_UNIT_TEST_CASE(Service_ReadNativeMemory_LargeRead) { + const char* kScript = + "@pragma('vm:entry-point', 'set')\n" + "var port;\n" + "main() {}\n"; + + Isolate* isolate = thread->isolate(); + isolate->set_is_runnable(true); + Dart_Handle lib; + { + TransitionVMToNative transition(thread); + lib = TestCase::LoadTestScript(kScript, nullptr); + EXPECT_VALID(lib); + Dart_Handle result = Dart_Invoke(lib, NewString("main"), 0, nullptr); + EXPECT_VALID(result); + } + + ServiceTestMessageHandler handler; + Dart_Port port_id = PortMap::CreatePort(&handler); + Dart_Handle port = Api::NewHandle(thread, SendPort::New(port_id)); + { + TransitionVMToNative transition(thread); + EXPECT_VALID(port); + EXPECT_VALID(Dart_SetField(lib, NewString("port"), port)); + } + + Array& service_msg = Array::Handle(); + + // allocate 1MB + const intptr_t kOneMB = 1 * MB; + CAllocUniquePtr large_buffer( + reinterpret_cast(malloc(kOneMB))); + for (intptr_t i = 0; i < kOneMB; i++) { + large_buffer.get()[i] = static_cast(i % 256); + } + + uintptr_t address = reinterpret_cast(large_buffer.get()); + + service_msg = EvalF(lib, + "[0, port, '0', '_readNativeMemory', false, " + "['address', 'size'], ['%" Px "', '%" Pd "']]", + address, kOneMB); + HandleIsolateMessage(isolate, service_msg); + EXPECT_EQ(MessageHandler::kOK, handler.HandleNextMessage()); + + EXPECT_SUBSTRING("\"type\":\"NativeMemory\"", handler.msg()); + EXPECT_SUBSTRING("\"bytes\":", handler.msg()); +} + +ISOLATE_UNIT_TEST_CASE(Service_ReadNativeMemory_InvalidAddress) { + const char* kScript = + "@pragma('vm:entry-point', 'set')\n" + "var port;\n" + "main() {}\n"; + + Isolate* isolate = thread->isolate(); + isolate->set_is_runnable(true); + Dart_Handle lib; + { + TransitionVMToNative transition(thread); + lib = TestCase::LoadTestScript(kScript, nullptr); + EXPECT_VALID(lib); + Dart_Handle result = Dart_Invoke(lib, NewString("main"), 0, nullptr); + EXPECT_VALID(result); + } + + ServiceTestMessageHandler handler; + Dart_Port port_id = PortMap::CreatePort(&handler); + Dart_Handle port = Api::NewHandle(thread, SendPort::New(port_id)); + { + TransitionVMToNative transition(thread); + EXPECT_VALID(port); + EXPECT_VALID(Dart_SetField(lib, NewString("port"), port)); + } + + Array& service_msg = Array::Handle(); + + service_msg = EvalF(lib, + "[0, port, '0', '_readNativeMemory', false, " + "['address', 'size'], ['1000', '8']]"); + + HandleIsolateMessage(isolate, service_msg); + EXPECT_EQ(MessageHandler::kOK, handler.HandleNextMessage()); + + EXPECT_SUBSTRING("\"code\":1004", handler.msg()); +#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID) + EXPECT_SUBSTRING("Input\\/output error", handler.msg()); +#elif defined(DART_HOST_OS_WINDOWS) + EXPECT_SUBSTRING("error 299", handler.msg()); +#endif +} + +ISOLATE_UNIT_TEST_CASE(Service_ReadNativeMemory_NullAddress) { + const char* kScript = + "@pragma('vm:entry-point', 'set')\n" + "var port;\n" + "main() {}\n"; + + Isolate* isolate = thread->isolate(); + isolate->set_is_runnable(true); + Dart_Handle lib; + { + TransitionVMToNative transition(thread); + lib = TestCase::LoadTestScript(kScript, nullptr); + EXPECT_VALID(lib); + Dart_Handle result = Dart_Invoke(lib, NewString("main"), 0, nullptr); + EXPECT_VALID(result); + } + + ServiceTestMessageHandler handler; + Dart_Port port_id = PortMap::CreatePort(&handler); + Dart_Handle port = Api::NewHandle(thread, SendPort::New(port_id)); + { + TransitionVMToNative transition(thread); + EXPECT_VALID(port); + EXPECT_VALID(Dart_SetField(lib, NewString("port"), port)); + } + + Array& service_msg = Array::Handle(); + + service_msg = EvalF(lib, + "[0, port, '0', '_readNativeMemory', false, " + "['address', 'size'], ['0', '8']]"); + HandleIsolateMessage(isolate, service_msg); + EXPECT_EQ(MessageHandler::kOK, handler.HandleNextMessage()); + + EXPECT_SUBSTRING("\"error\"", handler.msg()); + EXPECT_SUBSTRING("null pointer", handler.msg()); +} + // TODO(zra): Remove when tests are ready to enable. #if !defined(TARGET_ARCH_ARM64)