Added linux support (tested on Ubuntu)

CMakeList: fixed for building on linux
processpicker: linux process enumeration
main.cpp: "_Exit()" works on linux & windows
"ProcessMemory" plugin: added linux support
This commit is contained in:
Sen66
2026-02-09 15:09:42 +01:00
parent 0e65b9997e
commit 4029b05298
7 changed files with 421 additions and 77 deletions

View File

@@ -43,9 +43,10 @@ target_link_libraries(ReclassX PRIVATE
Qt6::Svg
Qt6::Concurrent
QScintilla::QScintilla
dbghelp
psapi
)
if(WIN32)
target_link_libraries(ReclassX PRIVATE dbghelp psapi)
endif()
add_custom_target(screenshot ALL
COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
@@ -140,7 +141,10 @@ if(BUILD_TESTING)
target_include_directories(test_controller PRIVATE src)
target_link_libraries(test_controller PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_controller PRIVATE dbghelp psapi)
endif()
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
@@ -149,7 +153,10 @@ if(BUILD_TESTING)
target_include_directories(test_validation PRIVATE src)
target_link_libraries(test_validation PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_validation PRIVATE dbghelp psapi)
endif()
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_generator tests/test_generator.cpp
@@ -164,7 +171,10 @@ if(BUILD_TESTING)
target_include_directories(test_context_menu PRIVATE src)
target_link_libraries(test_context_menu PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_context_menu PRIVATE dbghelp psapi)
endif()
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_new_features tests/test_new_features.cpp
@@ -173,7 +183,10 @@ if(BUILD_TESTING)
target_include_directories(test_new_features PRIVATE src)
target_link_libraries(test_new_features PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_new_features PRIVATE dbghelp psapi)
endif()
add_test(NAME test_new_features COMMAND test_new_features)
endif()
add_subdirectory(plugins/ProcessMemory)

View File

@@ -26,6 +26,16 @@ add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
# Link Qt
target_link_libraries(ProcessMemoryPlugin PRIVATE Qt6::Widgets)
# Platform-specific linking
if(WIN32)
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
endif()
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
if(UNIX AND NOT APPLE)
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
# Include directories
target_include_directories(ProcessMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src

View File

@@ -1,17 +1,40 @@
#include "ProcessMemoryPlugin.h"
#include "../../src/processpicker.h"
#include <QStyle>
#include <QApplication>
#include <QRegularExpression>
#include <QMessageBox>
#include <QPixmap>
#include <QImage>
#include <QDir>
#include <QFileInfo>
#ifdef _WIN32
#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <shellapi.h>
#elif defined(__linux__)
#include <climits>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <fstream>
#include <sstream>
#include <cstring>
#endif
// ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryProvider implementation
// ──────────────────────────────────────────────────────────────────────────
ProcessMemoryProvider::ProcessMemoryProvider(DWORD pid, const QString& processName)
#ifdef _WIN32
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_handle(nullptr)
, m_pid(pid)
, m_processName(processName)
@@ -31,15 +54,7 @@ ProcessMemoryProvider::ProcessMemoryProvider(DWORD pid, const QString& processNa
}
if (m_handle)
{
cacheModules();
}
}
ProcessMemoryProvider::~ProcessMemoryProvider()
{
if (m_handle)
CloseHandle(m_handle);
}
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
@@ -64,9 +79,16 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
// TODO: Implement module enumeration with EnumProcessModules
// For now, just return empty (no symbol resolution)
Q_UNUSED(addr);
for (const auto& mod : m_modules)
{
if (addr >= mod.base && addr < mod.base + mod.size)
{
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {};
}
@@ -98,6 +120,190 @@ void ProcessMemoryProvider::cacheModules()
}
}
#elif defined(__linux__)
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_fd(-1)
, m_pid(pid)
, m_processName(processName)
, m_writable(false)
, m_base(0)
{
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
QByteArray pathUtf8 = memPath.toUtf8();
// Try read-write first
m_fd = ::open(pathUtf8.constData(), O_RDWR);
if (m_fd >= 0)
m_writable = true;
else
{
// Fall back to read-only
m_fd = ::open(pathUtf8.constData(), O_RDONLY);
m_writable = false;
}
if (m_fd >= 0)
cacheModules();
}
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (m_fd < 0 || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_readv first (faster, no fd seek contention)
struct iovec local;
local.iov_base = buf;
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
if (nread == static_cast<ssize_t>(len))
return true;
// Fallback: pread on /proc/<pid>/mem
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
return nread == static_cast<ssize_t>(len);
}
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (m_fd < 0 || !m_writable || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_writev first
struct iovec local;
local.iov_base = const_cast<void*>(buf);
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
if (nwritten == static_cast<ssize_t>(len))
return true;
// Fallback: pwrite on /proc/<pid>/mem
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
return nwritten == static_cast<ssize_t>(len);
}
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
if (addr >= mod.base && addr < mod.base + mod.size)
{
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {};
}
void ProcessMemoryProvider::cacheModules()
{
// Parse /proc/<pid>/maps to discover loaded modules
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
std::ifstream mapsFile(mapsPath.toStdString());
if (!mapsFile.is_open()) return;
// Accumulate base/end per path, then convert to ModuleInfo
struct Range { uint64_t base; uint64_t end; };
QMap<QString, Range> moduleRanges;
std::string line;
bool firstExec = true;
while (std::getline(mapsFile, line))
{
// Format: addr_start-addr_end perms offset dev inode pathname
// Example: 00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/foo
std::istringstream iss(line);
std::string addrRange, perms, offset, dev, inode, pathname;
iss >> addrRange >> perms >> offset >> dev >> inode;
std::getline(iss, pathname);
// Trim leading whitespace from pathname
size_t start = pathname.find_first_not_of(" \t");
if (start == std::string::npos) continue;
pathname = pathname.substr(start);
// Skip non-file mappings
if (pathname.empty() || pathname[0] != '/') continue;
// Skip special mappings
if (pathname.find("/dev/") == 0 || pathname.find("/memfd:") == 0) continue;
// Parse address range
auto dash = addrRange.find('-');
if (dash == std::string::npos) continue;
uint64_t addrStart = std::stoull(addrRange.substr(0, dash), nullptr, 16);
uint64_t addrEnd = std::stoull(addrRange.substr(dash + 1), nullptr, 16);
QString qpath = QString::fromStdString(pathname);
// Track first executable mapping as the base address
if (firstExec && perms.size() >= 3 && perms[2] == 'x')
{
m_base = addrStart;
firstExec = false;
}
auto it = moduleRanges.find(qpath);
if (it != moduleRanges.end())
{
if (addrStart < it->base) it->base = addrStart;
if (addrEnd > it->end) it->end = addrEnd;
}
else
{
moduleRanges.insert(qpath, {addrStart, addrEnd});
}
}
m_modules.reserve(moduleRanges.size());
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
{
QFileInfo fi(it.key());
m_modules.append({
fi.fileName(),
it->base,
it->end - it->base
});
}
}
#endif // platform
ProcessMemoryProvider::~ProcessMemoryProvider()
{
#ifdef _WIN32
if (m_handle)
CloseHandle(m_handle);
#elif defined(__linux__)
if (m_fd >= 0)
::close(m_fd);
#endif
}
int ProcessMemoryProvider::size() const
{
#ifdef _WIN32
return m_handle ? INT_MAX : 0;
#elif defined(__linux__)
return m_fd ? INT_MAX : 0;
#endif
}
// ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryPlugin implementation
// ──────────────────────────────────────────────────────────────────────────
@@ -119,9 +325,10 @@ std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString
// Parse target: "pid:name" or just "pid"
QStringList parts = target.split(':');
bool ok = false;
DWORD pid = parts[0].toUInt(&ok);
uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) {
if (!ok || pid == 0)
{
if (errorMsg) *errorMsg = "Invalid PID: " + target;
return nullptr;
}
@@ -132,11 +339,9 @@ std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString
if (!provider->isValid())
{
if (errorMsg)
{
*errorMsg = QString("Failed to open process %1 (PID: %2)\n"
"Ensure the process is running and you have sufficient permissions.")
.arg(name).arg(pid);
}
return nullptr;
}
@@ -164,13 +369,36 @@ uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
MODULEINFO mi{};
if (GetModuleInformation(hProc, hMod, &mi, sizeof(mi)))
{
base = (uint64_t)mi.lpBaseOfDll;
}
}
CloseHandle(hProc);
return base;
#elif defined(__linux__)
// Parse PID from target
QStringList parts = target.split(':');
bool ok = false;
uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) return 0;
// Find first executable mapping from /proc/<pid>/maps
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(pid);
std::ifstream mapsFile(mapsPath.toStdString());
if (!mapsFile.is_open()) return 0;
std::string line;
while (std::getline(mapsFile, line)) {
std::istringstream iss(line);
std::string addrRange, perms;
iss >> addrRange >> perms;
if (perms.size() >= 3 && perms[2] == 'x') {
auto dash = addrRange.find('-');
if (dash != std::string::npos) {
return std::stoull(addrRange.substr(0, dash), nullptr, 16);
}
}
}
return 0;
#else
Q_UNUSED(target);
return 0;
@@ -257,6 +485,45 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
}
CloseHandle(snapshot);
#elif defined(__linux__)
QDir procDir("/proc");
QStringList entries = procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QIcon defaultIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
for (const QString& entry : entries) {
bool ok = false;
uint32_t pid = entry.toUInt(&ok);
if (!ok || pid == 0) continue;
// Read process name from /proc/<pid>/comm
QString commPath = QStringLiteral("/proc/%1/comm").arg(pid);
QFile commFile(commPath);
QString procName;
if (commFile.open(QIODevice::ReadOnly)) {
procName = QString::fromUtf8(commFile.readAll()).trimmed();
commFile.close();
}
if (procName.isEmpty()) continue; // Skip kernel threads with no name
// Read exe path from /proc/<pid>/exe symlink
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
QFileInfo exeInfo(exePath);
QString resolvedPath;
if (exeInfo.exists())
resolvedPath = exeInfo.symLinkTarget();
// Skip if we can't read the process memory (no access)
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
if (::access(memPath.toUtf8().constData(), R_OK) != 0)
continue;
PluginProcessInfo info;
info.pid = pid;
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
processes.append(info);
}
#endif
return processes;
@@ -266,7 +533,7 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
// Plugin factory
// ──────────────────────────────────────────────────────────────────────────
extern "C" __declspec(dllexport) IPlugin* CreatePlugin()
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new ProcessMemoryPlugin();
}

View File

@@ -1,23 +1,22 @@
#pragma once
#include "../../src/iplugin.h"
#include "../../src/core.h"
#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <shellapi.h>
#include <cstdint>
/**
* Windows process memory provider
* Reads/writes memory from a live process using Win32 API
* Process memory provider
* Reads/writes memory from a live process using platform APIs
*/
class ProcessMemoryProvider : public rcx::Provider {
class ProcessMemoryProvider : public rcx::Provider
{
public:
ProcessMemoryProvider(DWORD pid, const QString& processName);
ProcessMemoryProvider(uint32_t pid, const QString& processName);
~ProcessMemoryProvider() override;
// Required overrides
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override { return m_handle ? INT_MAX : NULL; } // Process memory has no fixed size
int size() const override;
// Optional overrides
bool write(uint64_t addr, const void* buf, int len) override;
@@ -27,7 +26,7 @@ public:
QString getSymbol(uint64_t addr) const override;
// Process-specific helpers
DWORD pid() const { return m_pid; }
uint32_t pid() const { return m_pid; }
uint64_t baseAddress() const { return m_base; }
void refreshModules() { m_modules.clear(); cacheModules(); }
@@ -35,8 +34,12 @@ private:
void cacheModules();
private:
HANDLE m_handle;
DWORD m_pid;
#ifdef _WIN32
void* m_handle;
#elif defined(__linux__)
int m_fd;
#endif
uint32_t m_pid;
QString m_processName;
bool m_writable;
uint64_t m_base;
@@ -52,12 +55,13 @@ private:
/**
* Plugin that provides ProcessMemoryProvider
*/
class ProcessMemoryPlugin : public IProviderPlugin {
class ProcessMemoryPlugin : public IProviderPlugin
{
public:
std::string Name() const override { return "Process Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "ReclassX"; }
std::string Description() const override { return "Read and write memory from local running Windows processes"; }
std::string Description() const override { return "Read and write memory from local running processes"; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override;
@@ -72,4 +76,4 @@ public:
};
// Plugin export
extern "C" __declspec(dllexport) IPlugin* CreatePlugin();
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

View File

@@ -4,14 +4,20 @@
#include <memory>
#include <string>
#ifdef _WIN32
#define RCX_PLUGIN_EXPORT __declspec(dllexport)
#else
#define RCX_PLUGIN_EXPORT __attribute__((visibility("default")))
#endif
// Forward declaration
namespace rcx { class Provider; }
/**
* Plugin interface for ReclassX
*
* Plugins are loaded from the "Plugins" folder as DLLs.
* Each plugin must export a C function: extern "C" __declspec(dllexport) IPlugin* CreatePlugin();
* Plugins are loaded from the "Plugins" folder as shared libraries.
* Each plugin must export a C function: extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
*/
class IPlugin {
public:

View File

@@ -1188,7 +1188,7 @@ int main(int argc, char* argv[]) {
QTimer::singleShot(1000, [&window, out]() {
QDir().mkpath(QFileInfo(out).absolutePath());
window.grab().save(out);
::_exit(0); // immediate exit — no need for clean shutdown in screenshot mode
::_Exit(0); // immediate exit — no need for clean shutdown in screenshot mode
});
}

View File

@@ -11,6 +11,11 @@
#include <tlhelp32.h>
#include <psapi.h>
#include <shellapi.h>
#elif defined(__linux__)
#include <QDir>
#include <QStyle>
#include <QApplication>
#include <unistd.h>
#endif
ProcessPicker::ProcessPicker(QWidget *parent)
@@ -155,6 +160,45 @@ void ProcessPicker::enumerateProcesses()
}
CloseHandle(snapshot);
#elif defined(__linux__)
QDir procDir("/proc");
QStringList entries = procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QIcon defaultIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
for (const QString& entry : entries) {
bool ok = false;
uint32_t pid = entry.toUInt(&ok);
if (!ok || pid == 0) continue;
// Read process name from /proc/<pid>/comm
QString commPath = QStringLiteral("/proc/%1/comm").arg(pid);
QFile commFile(commPath);
QString procName;
if (commFile.open(QIODevice::ReadOnly)) {
procName = QString::fromUtf8(commFile.readAll()).trimmed();
commFile.close();
}
if (procName.isEmpty()) continue;
// Read exe path from /proc/<pid>/exe symlink
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
QFileInfo exeInfo(exePath);
QString resolvedPath;
if (exeInfo.exists())
resolvedPath = exeInfo.symLinkTarget();
// Skip if we can't read the process memory
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
if (::access(memPath.toUtf8().constData(), R_OK) != 0)
continue;
ProcessInfo info;
info.pid = pid;
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
processes.append(info);
}
#else
// Platform not supported
QMessageBox::warning(this, "Error", "Process enumeration not supported on this platform.");