diff --git a/news/240.feature.rst b/news/240.feature.rst new file mode 100644 index 00000000..053975b9 --- /dev/null +++ b/news/240.feature.rst @@ -0,0 +1 @@ +Support kernels that don't have CONFIG_CROSS_MEMORY_ATTACH set. diff --git a/src/pystack/_pystack/mem.cpp b/src/pystack/_pystack/mem.cpp index 4590fead..825f3c62 100644 --- a/src/pystack/_pystack/mem.cpp +++ b/src/pystack/_pystack/mem.cpp @@ -17,7 +17,6 @@ namespace pystack { using elf_unique_ptr = std::unique_ptr>; -using file_unique_ptr = std::unique_ptr>; static ssize_t _process_vm_readv( @@ -228,6 +227,16 @@ ProcessMemoryManager::ProcessMemoryManager(pid_t pid) ssize_t ProcessMemoryManager::readChunk(remote_addr_t addr, size_t len, char* dst) const +{ + if (d_memfile || getenv("_PYSTACK_NO_PROCESS_VM_READV") != nullptr) { + return readChunkThroughMemFile(addr, len, dst); + } else { + return readChunkDirect(addr, len, dst); + } +} + +ssize_t +ProcessMemoryManager::readChunkDirect(remote_addr_t addr, size_t len, char* dst) const { struct iovec local[1]; struct iovec remote[1]; @@ -246,6 +255,9 @@ ProcessMemoryManager::readChunk(remote_addr_t addr, size_t len, char* dst) const throw InvalidRemoteAddress(); } else if (errno == EPERM) { throw std::runtime_error(PERM_MESSAGE); + } else if (errno == ENOSYS) { + LOG(DEBUG) << "process_vm_readv not compiled in kernel, falling back to /proc/PID/mem"; + return readChunkThroughMemFile(addr, len, dst); } throw std::system_error(errno, std::generic_category()); } @@ -256,6 +268,30 @@ ProcessMemoryManager::readChunk(remote_addr_t addr, size_t len, char* dst) const return result; } +ssize_t +ProcessMemoryManager::readChunkThroughMemFile(remote_addr_t addr, size_t len, char* dst) const +{ + if (!d_memfile) { + std::string filepath = "/proc/" + std::to_string(d_pid) + "/mem"; + d_memfile = file_unique_ptr(fopen(filepath.c_str(), "r"), fclose); + if (!d_memfile) { + if (errno == EPERM || errno == EACCES) { + LOG(ERROR) << "Permission denied opening file " << filepath; + throw std::runtime_error(PERM_MESSAGE); + } + LOG(ERROR) << "Failed to open file " << filepath << ": " << std::strerror(errno); + throw std::runtime_error("Failed to open " + filepath); + } + } + fseeko(d_memfile.get(), addr, SEEK_SET); + if (static_cast(addr) != ftello(d_memfile.get()) + || len != fread(dst, 1, len, d_memfile.get())) + { + throw InvalidRemoteAddress(); + } + return static_cast(len); +} + ssize_t ProcessMemoryManager::copyMemoryFromProcess(remote_addr_t addr, size_t len, void* dst) const { diff --git a/src/pystack/_pystack/mem.h b/src/pystack/_pystack/mem.h index dd4ef557..b78b6674 100644 --- a/src/pystack/_pystack/mem.h +++ b/src/pystack/_pystack/mem.h @@ -12,9 +12,11 @@ #include #include -#include +#include "elf_common.h" namespace pystack { + +using file_unique_ptr = std::unique_ptr>; typedef uintptr_t remote_addr_t; struct RemoteMemCopyError : public std::exception @@ -174,9 +176,12 @@ class ProcessMemoryManager : public AbstractRemoteMemoryManager pid_t d_pid; std::vector d_vmaps; mutable LRUCache d_lru_cache; + mutable file_unique_ptr d_memfile; // Methods ssize_t readChunk(remote_addr_t addr, size_t len, char* dst) const; + ssize_t readChunkDirect(remote_addr_t addr, size_t len, char* dst) const; + ssize_t readChunkThroughMemFile(remote_addr_t addr, size_t len, char* dst) const; }; struct SimpleVirtualMap diff --git a/tests/integration/test_smoke.py b/tests/integration/test_smoke.py index 1a3278c6..a370b40d 100644 --- a/tests/integration/test_smoke.py +++ b/tests/integration/test_smoke.py @@ -38,13 +38,23 @@ @pytest.mark.parametrize("method", STACK_METHODS) @pytest.mark.parametrize("blocking", [True, False], ids=["blocking", "non-blocking"]) -def test_simple_execution(method, blocking, tmpdir): +@pytest.mark.parametrize( + "use_process_vm_readv", [True, False], ids=["process_vm_readv", "/proc/PID/mem"] +) +def test_simple_execution(method, blocking, use_process_vm_readv, tmpdir, monkeypatch): """Test that we can retrieve the thread state of a single process. This test is specially useful to run under valgrind or similar tools and to quickly check that the very basic functionality works as expected. """ + # GIVEN + env_var = "_PYSTACK_NO_PROCESS_VM_READV" + if use_process_vm_readv: + monkeypatch.delenv(env_var, raising=False) + else: + monkeypatch.setenv(env_var, "1") + # WHEN with spawn_child_process( sys.executable, TEST_SINGLE_THREAD_FILE, tmpdir