[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 <nourhan.m.hasan@gmail.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Reviewed-by: Daco Harkes <dacoharkes@google.com>
This commit is contained in:
Nourhan Hasan
2026-06-02 23:09:24 -07:00
committed by Daco Harkes
parent 1b3e9d510e
commit 80f770eb57
7 changed files with 349 additions and 0 deletions
+8
View File
@@ -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 ]
+1
View File
@@ -69,6 +69,7 @@ enum JSONRpcErrorCode {
kFileSystemAlreadyExists = 1001,
kFileSystemDoesNotExist = 1002,
kFileDoesNotExist = 1003,
kNativeMemoryReadError = 1004,
};
// Builds on JSONWriter to provide support for serializing various objects
+7
View File
@@ -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);
+20
View File
@@ -12,6 +12,7 @@
#include <dlfcn.h> // NOLINT
#include <elf.h> // NOLINT
#include <errno.h> // NOLINT
#include <fcntl.h> // NOLINT
#include <limits.h> // NOLINT
#include <malloc.h> // NOLINT
#include <sys/resource.h> // 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<off64_t>(address));
close(fd);
if (bytes_read == -1) {
*error = strerror(errno);
return false;
}
return bytes_read == static_cast<ssize_t>(size_in_bytes);
}
void OS::Sleep(int64_t millis) {
int64_t micros = millis * kMicrosecondsPerMillisecond;
SleepMicros(micros);
+19
View File
@@ -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<off64_t>(address));
close(fd);
if (bytes_read == -1) {
*error = strerror(errno);
return false;
}
return bytes_read == static_cast<ssize_t>(size_in_bytes);
}
void OS::Sleep(int64_t millis) {
int64_t micros = millis * kMicrosecondsPerMillisecond;
SleepMicros(micros);
+121
View File
@@ -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<uintptr_t>(size))) {
js->PrintError(kInvalidParams, "address + size overflows address space");
return;
}
CAllocUniquePtr<uint8_t> buffer(reinterpret_cast<uint8_t*>(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<char*>(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<uintptr_t>(address),
static_cast<intptr_t>(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
+173
View File
@@ -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<uintptr_t>(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<uint8_t> large_buffer(
reinterpret_cast<uint8_t*>(malloc(kOneMB)));
for (intptr_t i = 0; i < kOneMB; i++) {
large_buffer.get()[i] = static_cast<uint8_t>(i % 256);
}
uintptr_t address = reinterpret_cast<uintptr_t>(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)