Compare commits

...

9 Commits

Author SHA1 Message Date
IChooseYou
078a6028f0 fix: WinDbg provider stops auto-selecting module, new tabs inherit source
- WinDbg provider no longer picks arbitrary module[0] as name/base
  (was showing "WS2_32" for kernel dumps). Name is now generic
  "WinDbg (Live)" / "WinDbg (Dump)", base stays 0 so controller
  doesn't override user's address.
- Added throttled read failure logging to WinDbg provider.
- New tabs (File→New Class, workspace right-click) inherit the
  current tab's source/provider so users don't have to re-attach.
- Updated WinDbg provider tests for new behavior.
2026-02-23 08:08:46 -07:00
IChooseYou
67218d3e48 fix: move payload init out of DllMain to avoid loader lock deadlock
RcxPayloadInit() is now an exported function called after LoadLibrary
returns. DllMain only handles cleanup on detach. Timer queue creation
under the loader lock was crashing target processes.
2026-02-22 13:14:01 -07:00
IChooseYou
f651edd740 feat: remove nonce/bootstrap from remote process IPC, use PID-only naming
Shared memory names simplified to Local\RCX_SHM_<pid>, no bootstrap
handshake needed. Payload uses CreateTimerQueueTimer (10ms poll) instead
of a dedicated server thread.
2026-02-22 11:36:24 -07:00
IChooseYou
25aaace382 Merge remote-tracking branch 'origin/fix-issue-2' 2026-02-22 11:09:05 -07:00
Sen66
b5ddb042b8 Try to fix missing DLLs at CI windows builds
Fix https://github.com/IChooseYou/Reclass/issues/2
2026-02-22 19:06:50 +01:00
IChooseYou
e900dea836 fix: menu bar item paint no longer covers title bar bottom border
Take full ownership of CE_MenuBarItem in MenuBarStyle — never
delegate to Fusion which unconditionally fills the full item rect.
Non-hovered items draw text only (transparent bg lets parent border
show through). Hover/pressed states fill adjusted rect leaving 1px
for the border. Pressed state uses darker(130) for visual feedback.
2026-02-22 11:05:54 -07:00
IChooseYou
b647a334bc docs: fix Remote Process description 2026-02-22 09:14:04 -07:00
IChooseYou
fc390bc1f7 docs: add Remote Process data source to README 2026-02-22 09:06:32 -07:00
IChooseYou
7efe740ec1 fix: hover invisible when theme.hover == background, remove CSS on QMenuBar
Move hover color fixup into Theme::fromJson so all consumers get a
visible hover automatically. Remove duplicate lighter(130) fallback
from applyGlobalTheme. Replace QMenuBar CSS with QPalette so
MenuBarStyle QProxyStyle is not bypassed. Add PE_PanelMenuBar and
CE_MenuBarEmptyArea suppression so Fusion never paints over the
title bar background.
2026-02-22 08:58:57 -07:00
22 changed files with 843 additions and 443 deletions

View File

@@ -49,6 +49,7 @@ jobs:
- name: Package release zip - name: Package release zip
shell: bash shell: bash
run: | run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
mkdir -p release mkdir -p release
cp build/Reclass.exe release/ cp build/Reclass.exe release/
cp build/ReclassMcpBridge.exe release/ cp build/ReclassMcpBridge.exe release/
@@ -57,6 +58,7 @@ jobs:
cp -r build/styles release/ 2>/dev/null || true cp -r build/styles release/ 2>/dev/null || true
cp -r build/imageformats release/ 2>/dev/null || true cp -r build/imageformats release/ 2>/dev/null || true
cp -r build/iconengines release/ 2>/dev/null || true cp -r build/iconengines release/ 2>/dev/null || true
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
mkdir -p release/Plugins mkdir -p release/Plugins
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
cp -r build/themes release/ 2>/dev/null || true cp -r build/themes release/ 2>/dev/null || true

View File

@@ -372,6 +372,21 @@ if(BUILD_TESTING)
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test) target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_options_dialog COMMAND test_options_dialog) add_test(NAME test_options_dialog COMMAND test_options_dialog)
add_executable(test_source_provider tests/test_source_provider.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
src/resources.qrc)
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
target_link_libraries(test_source_provider PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_provider COMMAND test_source_provider)
if(WIN32) if(WIN32)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp add_executable(test_windbg_provider tests/test_windbg_provider.cpp
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp) plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
@@ -381,6 +396,19 @@ if(BUILD_TESTING)
add_test(NAME test_windbg_provider COMMAND test_windbg_provider) add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
endif() endif()
add_executable(bench_large_class tests/bench_large_class.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
target_link_libraries(bench_large_class PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME bench_large_class COMMAND bench_large_class)
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe # Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
# that links the broadest set of Qt modules; all test exes share the same output dir) # that links the broadest set of Qt modules; all test exes share the same output dir)
if(TARGET ${QT}::windeployqt) if(TARGET ${QT}::windeployqt)

View File

@@ -42,6 +42,7 @@ Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as
- **File** — open any binary file and inspect its contents as structured data - **File** — open any binary file and inspect its contents as structured data
- **Process** — attach to a live process and read its memory in real time - **Process** — attach to a live process and read its memory in real time
- **Remote Process** — read another process's memory via shared memory
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions - **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
--- ---

View File

@@ -5,9 +5,7 @@
#include <QStyle> #include <QStyle>
#include <QApplication> #include <QApplication>
#include <QMessageBox> #include <QMessageBox>
#include <QInputDialog>
#include <QPushButton> #include <QPushButton>
#include <QUuid>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QPixmap> #include <QPixmap>
@@ -65,12 +63,12 @@ struct IpcClient {
/* ── connect / disconnect ──────────────────────────────────────── */ /* ── connect / disconnect ──────────────────────────────────────── */
bool connect(uint32_t pid, const QByteArray& nonce, int timeoutMs = 5000) bool connect(uint32_t pid, int timeoutMs = 5000)
{ {
char shmName[128], reqName[128], rspName[128]; char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce.constData()); rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce.constData()); rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce.constData()); rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32 #ifdef _WIN32
/* poll for shared memory to appear (payload creating it) */ /* poll for shared memory to appear (payload creating it) */
@@ -373,51 +371,6 @@ static QString payloadPath()
#endif #endif
} }
/* Create bootstrap shared memory with the nonce */
static bool createBootstrapShm(uint32_t pid, const QByteArray& nonce)
{
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
#ifdef _WIN32
HANDLE hBoot = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE,
bootName);
if (!hBoot) return false;
auto* view = static_cast<RcxRpcBootHeader*>(
MapViewOfFile(hBoot, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE));
if (!view) { CloseHandle(hBoot); return false; }
memset(view, 0, RCX_RPC_BOOT_SIZE);
view->nonceLength = (uint32_t)nonce.size();
memcpy(view->nonce, nonce.constData(), qMin(nonce.size(), 59));
UnmapViewOfFile(view);
/* keep hBoot open until payload reads it (payload unlinks after reading) */
/* leak intentional: closed when process exits or payload consumes it */
return true;
#else
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
if (fd < 0) return false;
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return false; }
void* view = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
if (view == MAP_FAILED) return false;
auto* boot = static_cast<RcxRpcBootHeader*>(view);
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)nonce.size();
memcpy(boot->nonce, nonce.constData(), qMin(nonce.size(), 59));
munmap(view, RCX_RPC_BOOT_SIZE);
/* payload unlinks after consuming */
return true;
#endif
}
#ifdef _WIN32 #ifdef _WIN32
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */ /* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
@@ -447,7 +400,7 @@ static bool injectPayload(uint32_t pid, QString* errorMsg)
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr); WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
/* create remote thread calling LoadLibraryA(path) */ /* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
HMODULE hK32 = GetModuleHandleA("kernel32.dll"); HMODULE hK32 = GetModuleHandleA("kernel32.dll");
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>( auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
GetProcAddress(hK32, "LoadLibraryA")); GetProcAddress(hK32, "LoadLibraryA"));
@@ -464,19 +417,81 @@ static bool injectPayload(uint32_t pid, QString* errorMsg)
WaitForSingleObject(hThread, 10000); WaitForSingleObject(hThread, 10000);
/* check if LoadLibrary returned non-null */
DWORD exitCode = 0; DWORD exitCode = 0;
GetExitCodeThread(hThread, &exitCode); GetExitCodeThread(hThread, &exitCode);
CloseHandle(hThread); CloseHandle(hThread);
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE); VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
CloseHandle(hProc);
if (exitCode == 0) { if (exitCode == 0) {
CloseHandle(hProc);
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n" if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
"Ensure rcx_payload.dll is in: %1").arg(path); "Ensure rcx_payload.dll is in: %1").arg(path);
return false; return false;
} }
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
(loader lock is no longer held after LoadLibrary returned) */
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
GetProcAddress(hK32, "GetProcAddress"));
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
const char initName[] = "RcxPayloadInit";
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remoteInitName) {
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
Simpler approach: write small shellcode that does both calls. */
uint8_t shellcode[128];
int off = 0;
/* sub rsp, 40 ; shadow space + alignment */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
/* mov rcx, hPayloadRemote ; first arg = module handle */
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
memcpy(shellcode + off, &hMod, 8); off += 8;
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
memcpy(shellcode + off, &pName, 8); off += 8;
/* mov rax, GetProcAddress */
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
memcpy(shellcode + off, &pGPA, 8); off += 8;
/* call rax ; rax = RcxPayloadInit */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* test rax, rax */
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
/* jz skip (jump over the call if null) */
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
/* call rax ; RcxPayloadInit() */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* skip: add rsp, 40 */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
/* ret */
shellcode[off++] = 0xC3;
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (remoteCode) {
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
if (hThread2) {
WaitForSingleObject(hThread2, 10000);
CloseHandle(hThread2);
}
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
}
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
}
CloseHandle(hProc);
return true; return true;
} }
@@ -717,24 +732,23 @@ bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const
std::unique_ptr<rcx::Provider> std::unique_ptr<rcx::Provider>
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg) RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{ {
/* target = "rpm:{pid}:{nonce}:{name}" */ /* target = "rpm:{pid}:{name}" */
QStringList parts = target.split(':'); QStringList parts = target.split(':');
if (parts.size() < 4 || parts[0] != QStringLiteral("rpm")) { if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target; if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
return nullptr; return nullptr;
} }
bool ok; bool ok;
uint32_t pid = parts[1].toUInt(&ok); uint32_t pid = parts[1].toUInt(&ok);
QString nonce = parts[2]; QString name = parts.mid(2).join(':'); /* name may contain colons */
QString name = parts.mid(3).join(':'); /* name may contain colons */
if (!ok || pid == 0) { if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target."); if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
return nullptr; return nullptr;
} }
auto ipc = getOrCreateConnection(pid, nonce, errorMsg); auto ipc = getOrCreateConnection(pid, errorMsg);
if (!ipc) return nullptr; if (!ipc) return nullptr;
return std::make_unique<RemoteProcessProvider>(pid, name, ipc); return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
@@ -745,7 +759,7 @@ uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target)
/* Read imageBase directly from the shared-memory header -- zero IPC cost. /* Read imageBase directly from the shared-memory header -- zero IPC cost.
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */ The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
QStringList parts = target.split(':'); QStringList parts = target.split(':');
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
return 0; return 0;
bool ok; bool ok;
@@ -793,35 +807,17 @@ bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
QAbstractButton* clicked = box.clickedButton(); QAbstractButton* clicked = box.clickedButton();
if (clicked == injectBtn) { if (clicked == injectBtn) {
/* generate nonce */
QString nonce = QUuid::createUuid().toString(QUuid::Id128).left(16);
QByteArray nonceUtf8 = nonce.toUtf8();
/* create bootstrap, inject */
if (!createBootstrapShm(pid, nonceUtf8)) {
QMessageBox::critical(parent, QStringLiteral("Error"),
QStringLiteral("Failed to create bootstrap shared memory."));
return false;
}
QString injectErr; QString injectErr;
if (!injectPayload(pid, &injectErr)) { if (!injectPayload(pid, &injectErr)) {
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr); QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
return false; return false;
} }
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name); *target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
return true; return true;
} }
else if (clicked == connectBtn) { else if (clicked == connectBtn) {
bool ok; *target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
QString nonce = QInputDialog::getText(parent,
QStringLiteral("Connect to Payload"),
QStringLiteral("Enter the payload nonce:"),
QLineEdit::Normal, QString(), &ok);
if (!ok || nonce.isEmpty()) return false;
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name);
return true; return true;
} }
@@ -903,7 +899,7 @@ QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
std::shared_ptr<IpcClient> std::shared_ptr<IpcClient>
RemoteProcessMemoryPlugin::getOrCreateConnection( RemoteProcessMemoryPlugin::getOrCreateConnection(
uint32_t pid, const QString& nonce, QString* errorMsg) uint32_t pid, QString* errorMsg)
{ {
QMutexLocker lock(&m_connectionsMutex); QMutexLocker lock(&m_connectionsMutex);
@@ -912,7 +908,7 @@ RemoteProcessMemoryPlugin::getOrCreateConnection(
return *it; return *it;
auto ipc = std::make_shared<IpcClient>(); auto ipc = std::make_shared<IpcClient>();
if (!ipc->connect(pid, nonce.toUtf8())) { if (!ipc->connect(pid)) {
if (errorMsg) if (errorMsg)
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n" *errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
"Is the payload running?").arg(pid); "Is the payload running?").arg(pid);

View File

@@ -77,7 +77,7 @@ public:
private: private:
std::shared_ptr<IpcClient> getOrCreateConnection( std::shared_ptr<IpcClient> getOrCreateConnection(
uint32_t pid, const QString& nonce, QString* errorMsg); uint32_t pid, QString* errorMsg);
mutable QMutex m_connectionsMutex; mutable QMutex m_connectionsMutex;
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections; QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;

View File

@@ -2,9 +2,8 @@
* rcx_payload -- injected into target process. * rcx_payload -- injected into target process.
* *
* Pure Win32 / POSIX, NO Qt, minimal footprint. * Pure Win32 / POSIX, NO Qt, minimal footprint.
* Reads a nonce from bootstrap shared memory, creates the main IPC * Creates the main IPC channel (shared memory + events/semaphores)
* channel (shared memory + events/semaphores), and runs a server * using PID-only naming and uses a timer queue for polling.
* thread that handles RPC commands from the editor plugin.
*/ */
#include "../rcx_rpc_protocol.h" #include "../rcx_rpc_protocol.h"
@@ -22,8 +21,9 @@ static HANDLE g_hShm = nullptr;
static void* g_mappedView = nullptr; static void* g_mappedView = nullptr;
static HANDLE g_hReqEvent = nullptr; static HANDLE g_hReqEvent = nullptr;
static HANDLE g_hRspEvent = nullptr; static HANDLE g_hRspEvent = nullptr;
static HANDLE g_hThread = nullptr; static HANDLE g_hTimerQueue = nullptr;
static volatile LONG g_shutdown = 0; static HANDLE g_hPollTimer = nullptr;
static volatile LONG g_initialized = 0;
/* ── memory safety via VirtualQuery ────────────────────────────────── */ /* ── memory safety via VirtualQuery ────────────────────────────────── */
@@ -167,23 +167,24 @@ static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
hdr->status = RCX_RPC_STATUS_OK; hdr->status = RCX_RPC_STATUS_OK;
} }
/* ── server thread ────────────────────────────────────────────────── */ /* forward declaration */
void RcxPayloadCleanup();
static DWORD WINAPI ServerThread(LPVOID) /* ── timer callback (non-blocking poll) ───────────────────────────── */
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
{ {
if (!g_mappedView || !g_hReqEvent || !g_hRspEvent)
return;
/* non-blocking check: is there a pending request? */
DWORD rc = WaitForSingleObject(g_hReqEvent, 0);
if (rc != WAIT_OBJECT_0)
return;
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView); auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET; auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
/* signal readiness */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
while (!InterlockedCompareExchange(&g_shutdown, 0, 0)) {
DWORD rc = WaitForSingleObject(g_hReqEvent, 250);
if (rc == WAIT_TIMEOUT)
continue;
if (rc != WAIT_OBJECT_0)
break;
hdr->status = RCX_RPC_STATUS_OK; hdr->status = RCX_RPC_STATUS_OK;
switch (static_cast<RcxRpcCommand>(hdr->command)) { switch (static_cast<RcxRpcCommand>(hdr->command)) {
@@ -192,110 +193,121 @@ static DWORD WINAPI ServerThread(LPVOID)
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break; case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
case RPC_CMD_PING: break; case RPC_CMD_PING: break;
case RPC_CMD_SHUTDOWN: case RPC_CMD_SHUTDOWN:
InterlockedExchange(&g_shutdown, 1); RcxPayloadCleanup();
break; return;
default: default:
hdr->status = RCX_RPC_STATUS_ERROR; hdr->status = RCX_RPC_STATUS_ERROR;
break; break;
} }
SetEvent(g_hRspEvent); SetEvent(g_hRspEvent);
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
break;
}
/* mark not-ready so the host process can detect shutdown */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
return 0;
} }
/* ── cleanup ──────────────────────────────────────────────────────── */ /* ── cleanup ──────────────────────────────────────────────────────── */
static void Cleanup(bool waitThread) void RcxPayloadCleanup()
{ {
InterlockedExchange(&g_shutdown, 1); if (!InterlockedCompareExchange(&g_initialized, 0, 0))
return;
/* wake the thread if it's blocked on REQ */ /* stop the poll timer first */
if (g_hReqEvent) SetEvent(g_hReqEvent); if (g_hTimerQueue) {
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
if (waitThread && g_hThread) { g_hTimerQueue = nullptr;
WaitForSingleObject(g_hThread, 2000); g_hPollTimer = nullptr;
} }
if (g_hThread) { CloseHandle(g_hThread); g_hThread = nullptr; }
if (g_mappedView){ UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; } /* mark not-ready */
if (g_mappedView) {
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
}
if (g_mappedView) { UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; } if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; } if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; } if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
InterlockedExchange(&g_initialized, 0);
} }
/* ── DllMain ──────────────────────────────────────────────────────── */ /* ── init (called AFTER DllMain returns — safe for timer queues) ── */
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID reserved) extern "C" __declspec(dllexport)
bool RcxPayloadInit()
{ {
if (reason == DLL_PROCESS_ATTACH) { if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
return true; /* already initialized */
uint32_t pid = GetCurrentProcessId(); uint32_t pid = GetCurrentProcessId();
/* ── read nonce from bootstrap shm ── */
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
HANDLE hBoot = OpenFileMappingA(FILE_MAP_READ, FALSE, bootName);
if (!hBoot) return TRUE; /* no bootstrap = nothing to do */
auto* bootView = static_cast<const RcxRpcBootHeader*>(
MapViewOfFile(hBoot, FILE_MAP_READ, 0, 0, RCX_RPC_BOOT_SIZE));
if (!bootView) { CloseHandle(hBoot); return TRUE; }
char nonce[64] = {};
uint32_t nLen = bootView->nonceLength;
if (nLen > 59) nLen = 59;
memcpy(nonce, bootView->nonce, nLen);
nonce[nLen] = '\0';
UnmapViewOfFile(bootView);
CloseHandle(hBoot);
/* ── create main shared memory ── */
char shmName[128], reqName[128], rspName[128]; char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce); rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce); rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce); rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName); PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
if (!g_hShm) return TRUE; if (!g_hShm) {
InterlockedExchange(&g_initialized, 0);
return false;
}
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE); g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!g_mappedView) { CloseHandle(g_hShm); g_hShm = nullptr; return TRUE; } if (!g_mappedView) {
CloseHandle(g_hShm); g_hShm = nullptr;
InterlockedExchange(&g_initialized, 0);
return false;
}
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE); memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView); auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION; hdr->version = RCX_RPC_VERSION;
/* image base from PEB: gs:[0x60] → PEB, +0x18 → Ldr, Flink → first entry, +0x30 → DllBase */ /* image base from PEB */
{ {
uint64_t peb; uint64_t peb;
asm volatile("mov %%gs:0x60, %0" : "=r"(peb)); asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18); uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10); /* InLoadOrderModuleList.Flink */ uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10);
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30); /* DllBase */ hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30);
} }
/* ── create events ── */
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName); g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName); g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
if (!g_hReqEvent || !g_hRspEvent) { Cleanup(false); return TRUE; } if (!g_hReqEvent || !g_hRspEvent) {
RcxPayloadCleanup();
/* ── start server thread (payloadReady set by the thread) ── */ return false;
g_hThread = CreateThread(nullptr, 0, ServerThread, nullptr, 0, nullptr);
if (!g_hThread) { Cleanup(false); return TRUE; }
}
else if (reason == DLL_PROCESS_DETACH) {
/* reserved != NULL → process is terminating (threads already dead) */
Cleanup(reserved == nullptr);
} }
/* create dedicated timer queue + fast poll timer (10ms interval) */
g_hTimerQueue = CreateTimerQueue();
if (!g_hTimerQueue) {
RcxPayloadCleanup();
return false;
}
if (!CreateTimerQueueTimer(&g_hPollTimer, g_hTimerQueue,
RcxPollTimerCallback, nullptr,
0, /* start immediately */
10, /* 10ms repeat */
WT_EXECUTEDEFAULT)) {
RcxPayloadCleanup();
return false;
}
/* mark ready */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
return true;
}
/* ── DllMain — minimal, no heavy work under loader lock ───────────── */
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_DETACH) {
RcxPayloadCleanup();
}
return TRUE; return TRUE;
} }
@@ -529,37 +541,14 @@ static void payload_init()
{ {
uint32_t pid = (uint32_t)getpid(); uint32_t pid = (uint32_t)getpid();
/* ── read nonce from bootstrap shm ── */
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
int bootFd = shm_open(bootName, O_RDONLY, 0);
if (bootFd < 0) return;
void* bootView = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ,
MAP_SHARED, bootFd, 0);
close(bootFd);
if (bootView == MAP_FAILED) return;
auto* boot = static_cast<const RcxRpcBootHeader*>(bootView);
char nonce[64] = {};
uint32_t nLen = boot->nonceLength;
if (nLen > 59) nLen = 59;
memcpy(nonce, boot->nonce, nLen);
nonce[nLen] = '\0';
munmap(bootView, RCX_RPC_BOOT_SIZE);
/* one-shot, unlink bootstrap */
shm_unlink(bootName);
/* ── open /proc/self/mem for safe access ── */ /* ── open /proc/self/mem for safe access ── */
g_memFd = open("/proc/self/mem", O_RDWR); g_memFd = open("/proc/self/mem", O_RDWR);
if (g_memFd < 0) return; if (g_memFd < 0) return;
/* ── create main shared memory ── */ /* ── create main shared memory (PID-only naming) ── */
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid, nonce); rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid);
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid, nonce); rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid);
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid, nonce); rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid);
g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600); g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600);
if (g_shmFd < 0) return; if (g_shmFd < 0) return;

View File

@@ -15,7 +15,6 @@
#define RCX_RPC_HEADER_SIZE 4096 #define RCX_RPC_HEADER_SIZE 4096
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE #define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET) #define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
#define RCX_RPC_BOOT_SIZE 64
/* status codes */ /* status codes */
#define RCX_RPC_STATUS_OK 0 #define RCX_RPC_STATUS_OK 0
@@ -83,47 +82,32 @@ struct RcxRpcHeader {
uint8_t _pad[RCX_RPC_HEADER_SIZE - 48]; uint8_t _pad[RCX_RPC_HEADER_SIZE - 48];
}; };
/* Bootstrap shm -- 64 bytes, carries the nonce from plugin to payload */ /* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
struct RcxRpcBootHeader {
uint32_t nonceLength;
char nonce[60];
};
/* ── name formatting helpers ───────────────────────────────────────── */ static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
static inline void rcx_rpc_boot_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32 #ifdef _WIN32
snprintf(buf, n, "Local\\RCX_BOOT_%u", pid); snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
#else #else
snprintf(buf, n, "/rcx_boot_%u", pid); snprintf(buf, n, "/rcx_shm_%u", pid);
#endif #endif
} }
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid, const char* nonce) { static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32 #ifdef _WIN32
snprintf(buf, n, "Local\\RCX_SHM_%u_%s", pid, nonce); snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
#else #else
snprintf(buf, n, "/rcx_shm_%u_%s", pid, nonce); snprintf(buf, n, "/rcx_req_%u", pid);
#endif #endif
} }
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid, const char* nonce) { static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32 #ifdef _WIN32
snprintf(buf, n, "Local\\RCX_REQ_%u_%s", pid, nonce); snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
#else #else
snprintf(buf, n, "/rcx_req_%u_%s", pid, nonce); snprintf(buf, n, "/rcx_rsp_%u", pid);
#endif
}
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid, const char* nonce) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_RSP_%u_%s", pid, nonce);
#else
snprintf(buf, n, "/rcx_rsp_%u_%s", pid, nonce);
#endif #endif
} }
#ifdef __cplusplus #ifdef __cplusplus
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes"); static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
static_assert(sizeof(RcxRpcBootHeader) <= RCX_RPC_BOOT_SIZE, "Boot header must fit 64 bytes");
#endif #endif

View File

@@ -4,7 +4,7 @@
* *
* Usage: * Usage:
* test_rpc_client (auto-spawn host) * test_rpc_client (auto-spawn host)
* test_rpc_client <pid> <nonce> [testbuf_hex testlen] * test_rpc_client <pid> [testbuf_hex testlen]
*/ */
#include "../rcx_rpc_protocol.h" #include "../rcx_rpc_protocol.h"
@@ -45,12 +45,12 @@ struct TestIpcClient {
void* view = nullptr; void* view = nullptr;
bool ok = false; bool ok = false;
bool connect(uint32_t pid, const char* nonce, int timeoutMs = 5000) bool connect(uint32_t pid, int timeoutMs = 5000)
{ {
char shmName[128], reqName[128], rspName[128]; char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce); rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce); rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce); rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32 #ifdef _WIN32
ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs; ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs;
@@ -268,7 +268,7 @@ static pid_t g_hostPid = 0;
#endif #endif
static FILE* g_hostPipe = nullptr; static FILE* g_hostPipe = nullptr;
static bool spawn_host(uint32_t* outPid, char* outNonce, static bool spawn_host(uint32_t* outPid,
uint64_t* outTestBuf, uint32_t* outTestLen) uint64_t* outTestBuf, uint32_t* outTestLen)
{ {
/* resolve path to test_rpc_host next to ourselves */ /* resolve path to test_rpc_host next to ourselves */
@@ -302,11 +302,11 @@ static bool spawn_host(uint32_t* outPid, char* outNonce,
return false; return false;
} }
/* parse: READY pid=X nonce=Y testbuf=0xZ testlen=N */ /* parse: READY pid=X testbuf=0xZ testlen=N */
unsigned long long tbuf = 0; unsigned long long tbuf = 0;
unsigned tlen = 0; unsigned tlen = 0;
if (sscanf(line, "READY pid=%u nonce=%63s testbuf=0x%llx testlen=%u", if (sscanf(line, "READY pid=%u testbuf=0x%llx testlen=%u",
outPid, outNonce, &tbuf, &tlen) < 2) { outPid, &tbuf, &tlen) < 1) {
fprintf(stderr, "ERROR: cannot parse host output: %s\n", line); fprintf(stderr, "ERROR: cannot parse host output: %s\n", line);
return false; return false;
} }
@@ -341,30 +341,28 @@ static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
uint32_t pid = 0; uint32_t pid = 0;
char nonce[64] = {};
uint64_t testBuf = 0; uint64_t testBuf = 0;
uint32_t testLen = 0; uint32_t testLen = 0;
bool autoMode = false; bool autoMode = false;
if (argc >= 3) { if (argc >= 2) {
pid = (uint32_t)atoi(argv[1]); pid = (uint32_t)atoi(argv[1]);
strncpy(nonce, argv[2], 63); if (argc >= 4) {
if (argc >= 5) { testBuf = (uint64_t)strtoull(argv[2], nullptr, 0);
testBuf = (uint64_t)strtoull(argv[3], nullptr, 0); testLen = (uint32_t)atoi(argv[3]);
testLen = (uint32_t)atoi(argv[4]);
} }
} else { } else {
autoMode = true; autoMode = true;
printf("Auto-spawning test_rpc_host...\n"); printf("Auto-spawning test_rpc_host...\n");
if (!spawn_host(&pid, nonce, &testBuf, &testLen)) return 1; if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
} }
printf("Connecting to PID=%u nonce=%s testbuf=0x%llx testlen=%u\n\n", printf("Connecting to PID=%u testbuf=0x%llx testlen=%u\n\n",
pid, nonce, (unsigned long long)testBuf, testLen); pid, (unsigned long long)testBuf, testLen);
/* ── connect ── */ /* ── connect ── */
TestIpcClient ipc; TestIpcClient ipc;
if (!ipc.connect(pid, nonce)) { if (!ipc.connect(pid)) {
fprintf(stderr, "ERROR: IPC connect failed\n"); fprintf(stderr, "ERROR: IPC connect failed\n");
if (autoMode) cleanup_host(); if (autoMode) cleanup_host();
return 1; return 1;

View File

@@ -1,7 +1,7 @@
/* /*
* test_rpc_host -- loads rcx_payload in-process, acts as the "target". * test_rpc_host -- loads rcx_payload in-process, acts as the "target".
* *
* Usage: test_rpc_host [nonce] * Usage: test_rpc_host
* *
* Prints a READY line (machine-parseable), then waits for the payload * Prints a READY line (machine-parseable), then waits for the payload
* to shut down (RPC_CMD_SHUTDOWN from the client). * to shut down (RPC_CMD_SHUTDOWN from the client).
@@ -68,50 +68,11 @@ static int payload_path(char* out, int outLen)
return 0; return 0;
} }
/* Create bootstrap shared memory with the nonce */
static int create_bootstrap(uint32_t pid, const char* nonce)
{
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
#ifdef _WIN32
HANDLE h = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE, bootName);
if (!h) return -1;
void* v = MapViewOfFile(h, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE);
if (!v) { CloseHandle(h); return -1; }
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)strlen(nonce);
strncpy(boot->nonce, nonce, 59);
UnmapViewOfFile(v);
/* keep h open for payload to read */
return 0;
#else
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
if (fd < 0) return -1;
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return -1; }
void* v = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
if (v == MAP_FAILED) return -1;
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)strlen(nonce);
strncpy(boot->nonce, nonce, 59);
munmap(v, RCX_RPC_BOOT_SIZE);
return 0;
#endif
}
/* Open the main shared memory (read-only, just to monitor payloadReady) */ /* Open the main shared memory (read-only, just to monitor payloadReady) */
static void* open_main_shm(uint32_t pid, const char* nonce) static void* open_main_shm(uint32_t pid)
{ {
char shmName[128]; char shmName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce); rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
#ifdef _WIN32 #ifdef _WIN32
HANDLE h = nullptr; HANDLE h = nullptr;
@@ -142,21 +103,14 @@ static uint8_t g_testBuf[65536];
/* ── main ─────────────────────────────────────────────────────────── */ /* ── main ─────────────────────────────────────────────────────────── */
int main(int argc, char** argv) int main(int, char**)
{ {
const char* nonce = (argc > 1) ? argv[1] : "test0001";
uint32_t pid = current_pid(); uint32_t pid = current_pid();
/* fill test buffer with known pattern */ /* fill test buffer with known pattern */
for (int i = 0; i < (int)sizeof(g_testBuf); ++i) for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
g_testBuf[i] = (uint8_t)(i & 0xFF); g_testBuf[i] = (uint8_t)(i & 0xFF);
/* create bootstrap shm */
if (create_bootstrap(pid, nonce) != 0) {
fprintf(stderr, "ERROR: failed to create bootstrap shm\n");
return 1;
}
/* load payload */ /* load payload */
char plPath[1024]; char plPath[1024];
if (payload_path(plPath, sizeof(plPath)) != 0) { if (payload_path(plPath, sizeof(plPath)) != 0) {
@@ -171,6 +125,15 @@ int main(int argc, char** argv)
plPath, GetLastError()); plPath, GetLastError());
return 1; return 1;
} }
/* Call RcxPayloadInit() — DllMain is minimal, init must be explicit */
typedef bool (*RcxPayloadInitFn)();
auto pfnInit = (RcxPayloadInitFn)GetProcAddress(hPayload, "RcxPayloadInit");
if (!pfnInit || !pfnInit()) {
fprintf(stderr, "ERROR: RcxPayloadInit() failed or not found\n");
FreeLibrary(hPayload);
return 1;
}
#else #else
void* hPayload = dlopen(plPath, RTLD_NOW); void* hPayload = dlopen(plPath, RTLD_NOW);
if (!hPayload) { if (!hPayload) {
@@ -180,7 +143,7 @@ int main(int argc, char** argv)
#endif #endif
/* open main shm and wait for payloadReady */ /* open main shm and wait for payloadReady */
void* shmView = open_main_shm(pid, nonce); void* shmView = open_main_shm(pid);
if (!shmView) { if (!shmView) {
fprintf(stderr, "ERROR: failed to open main shared memory\n"); fprintf(stderr, "ERROR: failed to open main shared memory\n");
return 1; return 1;
@@ -197,8 +160,8 @@ int main(int argc, char** argv)
} }
/* print READY line for the client to parse */ /* print READY line for the client to parse */
printf("READY pid=%u nonce=%s testbuf=0x%llx testlen=%u\n", printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
pid, nonce, pid,
(unsigned long long)(uintptr_t)g_testBuf, (unsigned long long)(uintptr_t)g_testBuf,
(unsigned)sizeof(g_testBuf)); (unsigned)sizeof(g_testBuf));
fflush(stdout); fflush(stdout);
@@ -210,7 +173,7 @@ int main(int argc, char** argv)
printf("Payload shut down, exiting.\n"); printf("Payload shut down, exiting.\n");
#ifdef _WIN32 #ifdef _WIN32
/* give the server thread a moment to exit */ /* give the timer queue a moment to drain */
Sleep(200); Sleep(200);
FreeLibrary(hPayload); FreeLibrary(hPayload);
if (shmView) UnmapViewOfFile(shmView); if (shmView) UnmapViewOfFile(shmView);

View File

@@ -197,53 +197,15 @@ void WinDbgMemoryProvider::querySessionInfo()
} }
} }
if (m_symbols) { // WinDbg provides access to the entire virtual address space.
ULONG numModules = 0, numUnloaded = 0; // Do NOT auto-select a module as base — let the user set their
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded); // own base address. m_base stays 0 so the controller won't
qDebug() << "[WinDbg] GetNumberModules hr=" << Qt::hex << (unsigned long)hr // override tree.baseAddress.
<< "loaded=" << numModules << "unloaded=" << numUnloaded; m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
if (SUCCEEDED(hr) && numModules > 0) { : QStringLiteral("WinDbg (Dump)");
char modName[256] = {};
ULONG modSize = 0;
hr = m_symbols->GetModuleNames(0, 0, nullptr, 0, nullptr,
modName, sizeof(modName), &modSize,
nullptr, 0, nullptr);
if (SUCCEEDED(hr) && modSize > 0)
m_name = QString::fromUtf8(modName);
}
}
if (m_name.isEmpty())
m_name = m_isLive ? QStringLiteral("DbgEng (Live)") : QStringLiteral("DbgEng (Dump)");
if (m_symbols) {
ULONG numModules = 0, numUnloaded = 0;
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
if (SUCCEEDED(hr) && numModules > 0) {
ULONG64 moduleBase = 0;
hr = m_symbols->GetModuleByIndex(0, &moduleBase);
qDebug() << "[WinDbg] Module 0 base=" << Qt::hex << moduleBase;
if (SUCCEEDED(hr))
m_base = moduleBase;
}
}
if (m_base && m_dataSpaces) {
uint8_t probe[2] = {};
ULONG got = 0;
hr = m_dataSpaces->ReadVirtual(m_base, probe, 2, &got);
qDebug() << "[WinDbg] Probe read at" << Qt::hex << m_base
<< "hr=" << (unsigned long)hr << "got=" << got
<< "bytes:" << (int)probe[0] << (int)probe[1];
if (FAILED(hr) || got == 0) {
qWarning() << "[WinDbg] Probe read FAILED — cleaning up";
cleanup();
return;
}
}
qDebug() << "[WinDbg] Ready. name=" << m_name qDebug() << "[WinDbg] Ready. name=" << m_name
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive; << "isLive=" << m_isLive;
#endif #endif
} }
@@ -305,8 +267,18 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
dispatchToOwner([&]() { dispatchToOwner([&]() {
ULONG bytesRead = 0; ULONG bytesRead = 0;
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead); HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
if (FAILED(hr) || (int)bytesRead < len) if (SUCCEEDED(hr) && (int)bytesRead >= len) {
result = true;
return;
}
// Partial or failed read — zero-fill remainder and log
memset((char*)buf + bytesRead, 0, len - bytesRead); memset((char*)buf + bytesRead, 0, len - bytesRead);
++m_readFailCount;
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
<< "len=" << Qt::dec << len
<< "hr=0x" << Qt::hex << (unsigned long)hr
<< "got=" << Qt::dec << bytesRead;
result = bytesRead > 0; result = bytesRead > 0;
}); });
return result; return result;

View File

@@ -83,6 +83,7 @@ private:
bool m_isLive = false; bool m_isLive = false;
bool m_writable = false; bool m_writable = false;
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe) bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
mutable int m_readFailCount = 0;
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe // Dedicated thread for DbgEng COM operations. The remote TCP/pipe
// transport is thread-affine — all calls must happen on the thread // transport is thread-affine — all calls must happen on the thread

View File

@@ -303,6 +303,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field; lm.lineKind = LineKind::Field;
lm.nodeKind = node.elementKind; lm.nodeKind = node.elementKind;
lm.isArrayElement = true; lm.isArrayElement = true;
lm.arrayElementIdx = i;
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = elemAddr; lm.offsetAddr = elemAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;

View File

@@ -569,9 +569,9 @@ void RcxController::refresh() {
// Prune stale selections (nodes removed by undo/redo/delete) // Prune stale selections (nodes removed by undo/redo/delete)
QSet<uint64_t> valid; QSet<uint64_t> valid;
for (uint64_t id : m_selIds) { for (uint64_t id : m_selIds) {
uint64_t nodeId = id & ~kFooterIdBit; // Strip footer bit for lookup uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
if (m_doc->tree.indexOfId(nodeId) >= 0) if (m_doc->tree.indexOfId(nodeId) >= 0)
valid.insert(id); // Keep original ID (with footer bit if present) valid.insert(id); // Keep original ID (with footer/array bits if present)
} }
m_selIds = valid; m_selIds = valid;
@@ -1583,13 +1583,35 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// ── Always-available actions ── // ── Always-available actions ──
menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() { menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() {
bool ok;
QString input = QInputDialog::getText(menu.parentWidget(),
QStringLiteral("Append bytes"),
QStringLiteral("Byte count (decimal or 0x hex):"),
QLineEdit::Normal, QStringLiteral("128"), &ok);
if (!ok || input.trimmed().isEmpty()) return;
QString trimmed = input.trimmed();
int byteCount = 0;
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
byteCount = trimmed.mid(2).toInt(&ok, 16);
else
byteCount = trimmed.toInt(&ok, 10);
if (!ok || byteCount <= 0) return;
uint64_t target = m_viewRootId ? m_viewRootId : 0; uint64_t target = m_viewRootId ? m_viewRootId : 0;
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true; m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append 128 bytes")); m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
for (int i = 0; i < 16; i++) int idx = 0;
for (int i = 0; i < hex64Count; i++, idx++)
insertNode(target, -1, NodeKind::Hex64, insertNode(target, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(i)); QStringLiteral("field_%1").arg(idx));
for (int i = 0; i < remainBytes; i++, idx++)
insertNode(target, -1, NodeKind::Hex8,
QStringLiteral("field_%1").arg(idx));
m_doc->undoStack.endMacro(); m_doc->undoStack.endMacro();
m_suppressRefresh = false; m_suppressRefresh = false;
refresh(); refresh();
@@ -1674,11 +1696,17 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
bool ctrl = mods & Qt::ControlModifier; bool ctrl = mods & Qt::ControlModifier;
bool shift = mods & Qt::ShiftModifier; bool shift = mods & Qt::ShiftModifier;
// Compute effective selection ID: footers use nodeId | kFooterIdBit // Compute effective selection ID:
// footers → nodeId | kFooterIdBit
// array elements → nodeId | kArrayElemBit | (elemIdx << 48)
// everything else → nodeId
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t { auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
if (ln >= 0 && ln < m_lastResult.meta.size() && if (ln < 0 || ln >= m_lastResult.meta.size()) return nid;
m_lastResult.meta[ln].lineKind == LineKind::Footer) const auto& lm = m_lastResult.meta[ln];
if (lm.lineKind == LineKind::Footer)
return nid | kFooterIdBit; return nid | kFooterIdBit;
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
return makeArrayElemSelId(nid, lm.arrayElementIdx);
return nid; return nid;
}; };
@@ -1727,8 +1755,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
if (m_selIds.size() == 1) { if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin(); uint64_t sid = *m_selIds.begin();
// Strip footer bit for node lookup // Strip footer/array bits for node lookup
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit); int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask));
if (idx >= 0) emit nodeSelected(idx); if (idx >= 0) emit nodeSelected(idx);
} }
} }
@@ -2298,11 +2326,11 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
return; return;
} }
uint64_t newBase = provider->base();
m_doc->undoStack.clear(); m_doc->undoStack.clear();
m_doc->provider = std::move(provider); m_doc->provider = std::move(provider);
m_doc->dataPath.clear(); m_doc->dataPath.clear();
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress; // Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
// User-initiated source switches go through selectSource() which does update it.
// Re-evaluate stored formula against the new provider // Re-evaluate stored formula against the new provider
if (!m_doc->tree.baseAddressFormula.isEmpty()) { if (!m_doc->tree.baseAddressFormula.isEmpty()) {
@@ -2467,6 +2495,12 @@ void RcxController::clearSources() {
refresh(); refresh();
} }
void RcxController::copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx) {
m_savedSources = sources;
m_activeSourceIdx = activeIdx;
pushSavedSourcesToEditors();
}
void RcxController::pushSavedSourcesToEditors() { void RcxController::pushSavedSourcesToEditors() {
QVector<SavedSourceDisplay> display; QVector<SavedSourceDisplay> display;
display.reserve(m_savedSources.size()); display.reserve(m_savedSources.size());

View File

@@ -131,6 +131,7 @@ public:
void switchSource(int idx) { switchToSavedSource(idx); } void switchSource(int idx) { switchToSavedSource(idx); }
void clearSources(); void clearSources();
void selectSource(const QString& text); void selectSource(const QString& text);
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
// Value tracking toggle (per-tab, off by default) // Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; } bool trackValues() const { return m_trackValues; }

View File

@@ -481,6 +481,17 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr int kCommandRowLine = 0; static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1; static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL; static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
}
inline int arrayElemIdxFromSelId(uint64_t selId) {
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
}
struct LineMeta { struct LineMeta {
int nodeIdx = -1; int nodeIdx = -1;

View File

@@ -787,6 +787,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_meta = result.meta; m_meta = result.meta;
m_layout = result.layout; m_layout = result.layout;
// Build nodeId → display-line index for O(1) hover/selection lookup
m_nodeLineIndex.clear();
m_nodeLineIndex.reserve(m_meta.size());
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId != 0)
m_nodeLineIndex[m_meta[i].nodeId].append(i);
}
// Dynamically resize margin to fit the current hex digit tier // Dynamically resize margin to fit the current hex digit tier
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0')); QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
m_sci->setMarginWidth(0, marginSizer); m_sci->setMarginWidth(0, marginSizer);
@@ -835,9 +843,12 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_applyingDocument = false; m_applyingDocument = false;
// Re-apply hover markers (setText() clears all Scintilla markers). // Re-apply hover markers (setText() clears all Scintilla markers).
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
// applyHoverCursor() is NOT called here — it evaluates hitTest() against // applyHoverCursor() is NOT called here — it evaluates hitTest() against
// composed text that updateCommandRow() will overwrite. The correct call // composed text that updateCommandRow() will overwrite. The correct call
// happens via applySelectionOverlays() after all text is finalized. // happens via applySelectionOverlays() after all text is finalized.
m_prevHoveredNodeId = 0;
m_prevHoveredLine = -1;
applyHoverHighlight(); applyHoverHighlight();
} }
@@ -1064,18 +1075,33 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE); m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
for (int i = 0; i < m_meta.size(); i++) { // Use index: iterate selected IDs, look up their lines
if (isSyntheticLine(m_meta[i])) continue; for (uint64_t selId : selIds) {
uint64_t nodeId = m_meta[i].nodeId; bool isFooterSel = (selId & kFooterIdBit) != 0;
bool isFooter = (m_meta[i].lineKind == LineKind::Footer); bool isArrayElemSel = (selId & kArrayElemBit) != 0;
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
// Footers check for footerId, non-footers check for plain nodeId uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId; auto it = m_nodeLineIndex.constFind(nodeId);
if (selIds.contains(checkId)) { if (it == m_nodeLineIndex.constEnd()) continue;
m_sci->markerAdd(i, M_SELECTED); for (int ln : *it) {
m_sci->markerAdd(i, M_ACCENT); if (isSyntheticLine(m_meta[ln])) continue;
bool isFooter = (m_meta[ln].lineKind == LineKind::Footer);
// Match selection type to line type
if (isFooterSel && !isFooter) continue;
if (!isFooterSel && isFooter) continue;
// Array element: match by element index
if (isArrayElemSel) {
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
continue;
} else if (m_meta[ln].isArrayElement) {
// Plain nodeId selection shouldn't highlight individual array elements
// (the header line is enough)
continue;
}
m_sci->markerAdd(ln, M_SELECTED);
m_sci->markerAdd(ln, M_ACCENT);
if (!isFooter) if (!isFooter)
paintEditableSpans(i); paintEditableSpans(ln);
} }
} }
@@ -1088,28 +1114,63 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
} }
void RcxEditor::applyHoverHighlight() { void RcxEditor::applyHoverHighlight() {
m_sci->markerDeleteAll(M_HOVER); uint64_t prevId = m_prevHoveredNodeId;
int prevLine = m_prevHoveredLine;
m_prevHoveredNodeId = m_hoveredNodeId;
m_prevHoveredLine = m_hoveredLine;
// Fast path: nothing changed (same node AND same line)
if (prevId == m_hoveredNodeId && prevLine == m_hoveredLine
&& m_hoveredNodeId != 0) return;
// Remove old hover markers
if (prevId != 0) {
// Check if old hovered line was a single-line highlight (footer or array element)
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement));
if (prevSingleLine) {
m_sci->markerDelete(prevLine, M_HOVER);
} else {
auto it = m_nodeLineIndex.constFind(prevId);
if (it != m_nodeLineIndex.constEnd()) {
for (int ln : *it)
m_sci->markerDelete(ln, M_HOVER);
}
}
}
if (m_editState.active) return; if (m_editState.active) return;
if (!m_hoverInside) return; if (!m_hoverInside) return;
if (m_hoveredNodeId == 0) return; if (m_hoveredNodeId == 0) return;
// Check if hovered line is a footer - footers highlight independently // Footer and array elements highlight only the specific line
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() && bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
m_meta[m_hoveredLine].lineKind == LineKind::Footer); m_meta[m_hoveredLine].lineKind == LineKind::Footer);
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
m_meta[m_hoveredLine].isArrayElement);
// Check if the hovered item is already selected (using appropriate ID) // Check if the hovered item is already selected (using appropriate ID)
uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId; uint64_t checkId;
if (hoveringFooter)
checkId = m_hoveredNodeId | kFooterIdBit;
else if (hoveringArrayElem)
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
else
checkId = m_hoveredNodeId;
if (m_currentSelIds.contains(checkId)) return; if (m_currentSelIds.contains(checkId)) return;
if (hoveringFooter) { if (hoveringFooter || hoveringArrayElem) {
// Footer: only highlight this specific line // Single-line highlight for footers and array elements
m_sci->markerAdd(m_hoveredLine, M_HOVER); m_sci->markerAdd(m_hoveredLine, M_HOVER);
} else { } else {
// Non-footer: highlight all matching lines except footers // Non-footer, non-array-element: highlight all lines for this node
for (int i = 0; i < m_meta.size(); i++) { auto it = m_nodeLineIndex.constFind(m_hoveredNodeId);
if (m_meta[i].nodeId == m_hoveredNodeId && if (it != m_nodeLineIndex.constEnd()) {
m_meta[i].lineKind != LineKind::Footer) for (int ln : *it) {
m_sci->markerAdd(i, M_HOVER); if (m_meta[ln].lineKind != LineKind::Footer &&
!m_meta[ln].isArrayElement)
m_sci->markerAdd(ln, M_HOVER);
}
} }
} }
} }
@@ -2617,11 +2678,16 @@ void RcxEditor::updateEditableIndicators(int line) {
return; return;
} }
// Helper to check if a line's node is selected (handles footer IDs) // Helper to check if a line's node is selected (handles footer/array element IDs)
auto isLineSelected = [this](const LineMeta* lm) -> bool { auto isLineSelected = [this](const LineMeta* lm) -> bool {
if (!lm) return false; if (!lm) return false;
bool isFooter = (lm->lineKind == LineKind::Footer); uint64_t checkId;
uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId; if (lm->lineKind == LineKind::Footer)
checkId = lm->nodeId | kFooterIdBit;
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
else
checkId = lm->nodeId;
return m_currentSelIds.contains(checkId); return m_currentSelIds.contains(checkId);
}; };

View File

@@ -4,6 +4,7 @@
#include <QWidget> #include <QWidget>
#include <QSet> #include <QSet>
#include <QPoint> #include <QPoint>
#include <QHash>
class QsciScintilla; class QsciScintilla;
class QsciLexerCPP; class QsciLexerCPP;
@@ -95,8 +96,12 @@ private:
bool m_hoverInside = false; bool m_hoverInside = false;
uint64_t m_hoveredNodeId = 0; uint64_t m_hoveredNodeId = 0;
int m_hoveredLine = -1; int m_hoveredLine = -1;
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
int m_prevHoveredLine = -1; // for incremental marker update
QSet<uint64_t> m_currentSelIds; QSet<uint64_t> m_currentSelIds;
QVector<int> m_hoverSpanLines; // Lines with hover span indicators QVector<int> m_hoverSpanLines; // Lines with hover span indicators
// ── nodeId → display-line index (built in applyDocument) ──
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
// ── Drag selection ── // ── Drag selection ──
bool m_dragging = false; bool m_dragging = false;
bool m_dragStarted = false; // true once drag threshold exceeded bool m_dragStarted = false; // true once drag threshold exceeded

View File

@@ -251,6 +251,9 @@ public:
// Kill the 1px frame margin Fusion reserves around QMenu contents // Kill the 1px frame margin Fusion reserves around QMenu contents
if (metric == PM_MenuPanelWidth) if (metric == PM_MenuPanelWidth)
return 0; return 0;
// Kill the separator between dock widgets / central widget
if (metric == PM_DockWidgetSeparatorExtent)
return 0;
return QProxyStyle::pixelMetric(metric, opt, w); return QProxyStyle::pixelMetric(metric, opt, w);
} }
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt, void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
@@ -261,21 +264,37 @@ public:
// Kill the status bar item frame and panel border // Kill the status bar item frame and panel border
if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar) if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar)
return; return;
// Transparent menu bar background (no CSS needed)
if (elem == PE_PanelMenuBar)
return;
QProxyStyle::drawPrimitive(elem, opt, p, w); QProxyStyle::drawPrimitive(elem, opt, p, w);
} }
void drawControl(ControlElement element, const QStyleOption* opt, void drawControl(ControlElement element, const QStyleOption* opt,
QPainter* p, const QWidget* w) const override { QPainter* p, const QWidget* w) const override {
// Menu bar items (File, Edit, View…) — direct paint, Fusion ignores palette // Suppress Fusion's CE_MenuBarEmptyArea — it fills with palette.window()
// bypassing PE_PanelMenuBar. TitleBarWidget paints the background.
if (element == CE_MenuBarEmptyArea)
return;
// Menu bar items — fully owned painting (Fusion fills full rect, hiding border)
if (element == CE_MenuBarItem) { if (element == CE_MenuBarItem) {
if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) { if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) {
if (mi->state & (State_Selected | State_Sunken)) { QRect area = mi->rect.adjusted(0, 0, 0, -1); // leave 1px for border
QStyleOptionMenuItem patched = *mi; bool selected = mi->state & State_Selected;
patched.state &= ~(State_Selected | State_Sunken); bool sunken = mi->state & State_Sunken;
patched.palette.setColor(QPalette::ButtonText,
mi->palette.color(QPalette::Link)); // amber text only // Only fill background for hover/pressed — non-hovered stays
QProxyStyle::drawControl(element, &patched, p, w); // transparent so the parent's border line shows through.
return; if (sunken)
} p->fillRect(area, mi->palette.color(QPalette::Mid).darker(130));
else if (selected)
p->fillRect(area, mi->palette.color(QPalette::Mid));
QColor fg = (selected || sunken)
? mi->palette.color(QPalette::Link)
: mi->palette.color(QPalette::ButtonText);
p->setPen(fg);
p->drawText(area, Qt::AlignCenter | Qt::TextShowMnemonic, mi->text);
return; // never delegate to Fusion
} }
} }
// Popup menu items — palette patch then delegate to Fusion // Popup menu items — palette patch then delegate to Fusion
@@ -285,7 +304,7 @@ public:
&& mi->menuItemType != QStyleOptionMenuItem::Separator) { && mi->menuItemType != QStyleOptionMenuItem::Separator) {
QStyleOptionMenuItem patched = *mi; QStyleOptionMenuItem patched = *mi;
patched.palette.setColor(QPalette::Highlight, patched.palette.setColor(QPalette::Highlight,
mi->palette.color(QPalette::Mid)); // theme.border mi->palette.color(QPalette::Mid)); // theme.hover
patched.palette.setColor(QPalette::HighlightedText, patched.palette.setColor(QPalette::HighlightedText,
mi->palette.color(QPalette::Link)); // theme.indHoverSpan mi->palette.color(QPalette::Link)); // theme.indHoverSpan
QProxyStyle::drawControl(element, &patched, p, w); QProxyStyle::drawControl(element, &patched, p, w);
@@ -1352,8 +1371,7 @@ void MainWindow::toggleMcp() {
void MainWindow::applyTheme(const Theme& theme) { void MainWindow::applyTheme(const Theme& theme) {
applyGlobalTheme(theme); applyGlobalTheme(theme);
// Kill the 1px separator line between central widget and status bar // Separator killed via PM_DockWidgetSeparatorExtent in MenuBarStyle
setStyleSheet("QMainWindow::separator { height: 0px; width: 0px; }");
// Custom title bar // Custom title bar
m_titleBar->applyTheme(theme); m_titleBar->applyTheme(theme);
@@ -1976,7 +1994,22 @@ QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
buildEmptyStruct(doc->tree, classKeyword); buildEmptyStruct(doc->tree, classKeyword);
// Inherit source from current tab (if any)
auto* currentCtrl = activeController();
if (currentCtrl && currentCtrl->document()->provider
&& currentCtrl->document()->provider->isValid()) {
doc->provider = currentCtrl->document()->provider;
}
auto* sub = createTab(doc); auto* sub = createTab(doc);
// Copy saved sources to new tab's controller
if (currentCtrl && !currentCtrl->savedSources().isEmpty()) {
auto& newTab = m_tabs[sub];
newTab.ctrl->copySavedSources(currentCtrl->savedSources(),
currentCtrl->activeSourceIndex());
}
rebuildWorkspaceModel(); rebuildWorkspaceModel();
return sub; return sub;
} }
@@ -2280,8 +2313,16 @@ void MainWindow::populateSourceMenu() {
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")}, {QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
}; };
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")), auto addSourceAction = [this](const QString& text, const QIcon& icon, auto&& slot) {
QStringLiteral("File"), this, [this]() { auto* act = m_sourceMenu->addAction(icon, text);
act->setIconVisibleInMenu(true);
connect(act, &QAction::triggered, this, std::forward<decltype(slot)>(slot));
return act;
};
addSourceAction(QStringLiteral("File"),
makeIcon(QStringLiteral(":/vsicons/file-binary.svg")),
[this]() {
if (auto* c = activeController()) c->selectSource(QStringLiteral("File")); if (auto* c = activeController()) c->selectSource(QStringLiteral("File"));
}); });
@@ -2289,14 +2330,14 @@ void MainWindow::populateSourceMenu() {
for (const auto& prov : providers) { for (const auto& prov : providers) {
QString name = prov.name; QString name = prov.name;
auto it = s_providerIcons.constFind(prov.identifier); auto it = s_providerIcons.constFind(prov.identifier);
QIcon icon(it != s_providerIcons.constEnd() ? *it QIcon icon = makeIcon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg")); : QStringLiteral(":/vsicons/extensions.svg"));
QString label = prov.dllFileName.isEmpty() QString label = prov.dllFileName.isEmpty()
? name ? name
: QStringLiteral("%1 (%2)").arg(name, prov.dllFileName); : QStringLiteral("%1 (%2)").arg(name, prov.dllFileName);
m_sourceMenu->addAction(icon, label, this, [this, name]() { addSourceAction(label, icon, [this, name]() {
if (auto* c = activeController()) c->selectSource(name); if (auto* c = activeController()) c->selectSource(name);
}); });
} }
@@ -2306,18 +2347,20 @@ void MainWindow::populateSourceMenu() {
for (int i = 0; i < ctrl->savedSources().size(); i++) { for (int i = 0; i < ctrl->savedSources().size(); i++) {
const auto& e = ctrl->savedSources()[i]; const auto& e = ctrl->savedSources()[i];
auto* act = m_sourceMenu->addAction( auto* act = m_sourceMenu->addAction(
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName), QStringLiteral("%1 '%2'").arg(e.kind, e.displayName));
this, [this, i]() {
if (auto* c = activeController()) c->switchSource(i);
});
act->setCheckable(true); act->setCheckable(true);
act->setChecked(i == ctrl->activeSourceIndex()); act->setChecked(i == ctrl->activeSourceIndex());
connect(act, &QAction::triggered, this, [this, i]() {
if (auto* c = activeController()) c->switchSource(i);
});
} }
m_sourceMenu->addSeparator(); m_sourceMenu->addSeparator();
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/clear-all.svg")), auto* clearAct = addSourceAction(QStringLiteral("Clear All"),
QStringLiteral("Clear All"), this, [this]() { makeIcon(QStringLiteral(":/vsicons/clear-all.svg")),
[this]() {
if (auto* c = activeController()) c->clearSources(); if (auto* c = activeController()) c->clearSources();
}); });
Q_UNUSED(clearAct);
} }
} }

View File

@@ -1,4 +1,5 @@
#include "theme.h" #include "theme.h"
#include <QtGlobal>
#include <type_traits> #include <type_traits>
namespace rcx { namespace rcx {
@@ -61,6 +62,15 @@ Theme Theme::fromJson(const QJsonObject& o) {
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString; t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
if (!t.indHeatHot.isValid()) if (!t.indHeatHot.isValid())
t.indHeatHot = t.markerPtr; t.indHeatHot = t.markerPtr;
// Ensure hover is visually distinct from background
if (t.hover.isValid() && t.background.isValid()) {
int dist = qAbs(t.hover.red() - t.background.red())
+ qAbs(t.hover.green() - t.background.green())
+ qAbs(t.hover.blue() - t.background.blue());
if (dist < 20)
t.hover = t.background.lighter(130);
}
return t; return t;
} }

View File

@@ -76,14 +76,16 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }") QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
.arg(theme.textDim.name())); .arg(theme.textDim.name()));
// Menu bar styling — transparent background, themed text // Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
m_menuBar->setStyleSheet( // Set Window + Button to background so Fusion never paints a foreign color.
QStringLiteral( {
"QMenuBar { background: transparent; border: none; }" QPalette mbPal = m_menuBar->palette();
"QMenuBar::item { background: transparent; color: %1; padding: 8px 8px 4px 8px; }" mbPal.setColor(QPalette::Window, theme.background);
"QMenuBar::item:selected { background: %2; }" mbPal.setColor(QPalette::Button, theme.background);
"QMenuBar::item:pressed { background: %2; }") mbPal.setColor(QPalette::ButtonText, theme.textDim);
.arg(theme.textDim.name(), theme.hover.name())); m_menuBar->setPalette(mbPal);
m_menuBar->setAutoFillBackground(false);
}
// Chrome buttons // Chrome buttons
QString btnStyle = QStringLiteral( QString btnStyle = QStringLiteral(

View File

@@ -256,8 +256,9 @@ private slots:
{ {
WinDbgMemoryProvider prov(m_connString); WinDbgMemoryProvider prov(m_connString);
QVERIFY(prov.isValid()); QVERIFY(prov.isValid());
QVERIFY2(prov.base() != 0, "Should have a non-zero base from first module"); // WinDbg provider no longer auto-selects a module base — it returns 0
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16); // so the controller doesn't override the user's chosen base address.
QCOMPARE(prov.base(), (uint64_t)0);
} }
// ── Read: MZ header on main thread ── // ── Read: MZ header on main thread ──
@@ -446,6 +447,139 @@ private slots:
QCOMPARE(raw->Name(), std::string("WinDbg Memory")); QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
delete raw; delete raw;
} }
// ── Kernel session tests ──
// Requires a WinDbg instance with a kernel dump loaded and
// .server tcp:port=5055 running. Skipped automatically if
// no server is available. Override with WINDBG_KERNEL_CONN env var.
void provider_kernel_connect()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available (set WINDBG_KERNEL_CONN)");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY2(prov.isValid(), "Should connect to kernel debug server");
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
qDebug() << "Kernel provider name:" << prov.name();
qDebug() << "Kernel provider base:" << QString("0x%1").arg(prov.base(), 0, 16);
qDebug() << "Kernel provider isLive:" << prov.isLive();
// Name should not be an arbitrary user-mode DLL
QVERIFY2(!prov.name().contains("WS2_32", Qt::CaseInsensitive),
qPrintable("Name should not be 'WS2_32', got: " + prov.name()));
}
void provider_kernel_read_base()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY(prov.isValid());
// Provider no longer auto-selects a base. Use a known kernel address
// from env, or skip.
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
if (addrStr.isEmpty())
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
bool ok = false;
uint64_t addr = addrStr.toULongLong(&ok, 16);
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
uint8_t buf[16] = {};
ok = prov.read(addr, buf, 16);
QVERIFY2(ok, "Should read from kernel address");
bool allZero = true;
for (int i = 0; i < 16; ++i) {
if (buf[i] != 0) { allZero = false; break; }
}
QVERIFY2(!allZero, "Kernel read returned all zeros");
}
void provider_kernel_read_high_address()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY(prov.isValid());
// Use env var for a specific kernel address (e.g. _EPROCESS),
// otherwise fall back to the provider's base.
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
uint64_t addr = 0;
if (!addrStr.isEmpty()) {
bool ok = false;
addr = addrStr.toULongLong(&ok, 16);
if (!ok) addr = 0;
}
if (addr == 0) addr = prov.base();
uint8_t buf[64] = {};
bool ok = prov.read(addr, buf, 64);
QVERIFY2(ok, qPrintable(QString("Should read kernel addr 0x%1")
.arg(addr, 0, 16)));
bool allZero = true;
for (int i = 0; i < 64; ++i) {
if (buf[i] != 0) { allZero = false; break; }
}
QVERIFY2(!allZero, "Kernel high-address read returned all zeros");
qDebug() << "Read 64 bytes at" << QString("0x%1").arg(addr, 0, 16)
<< "first 8:" << QString("%1 %2 %3 %4 %5 %6 %7 %8")
.arg(buf[0], 2, 16, QChar('0'))
.arg(buf[1], 2, 16, QChar('0'))
.arg(buf[2], 2, 16, QChar('0'))
.arg(buf[3], 2, 16, QChar('0'))
.arg(buf[4], 2, 16, QChar('0'))
.arg(buf[5], 2, 16, QChar('0'))
.arg(buf[6], 2, 16, QChar('0'))
.arg(buf[7], 2, 16, QChar('0'));
}
void provider_kernel_read_backgroundThread()
{
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
"tcp:Port=5055,Server=localhost");
if (!canConnect(kernelConn))
QSKIP("No kernel debug server available");
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
if (addrStr.isEmpty())
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
bool ok = false;
uint64_t addr = addrStr.toULongLong(&ok, 16);
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
WinDbgMemoryProvider prov(kernelConn);
QVERIFY(prov.isValid());
// Simulate the controller's async refresh pattern
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
return prov.readBytes(addr, 4096);
});
future.waitForFinished();
QByteArray data = future.result();
QCOMPARE(data.size(), 4096);
bool allZero = true;
for (int i = 0; i < data.size(); ++i) {
if (data[i] != '\0') { allZero = false; break; }
}
QVERIFY2(!allZero, "Kernel background read returned all zeros");
}
}; };
QTEST_MAIN(TestWinDbgProvider) QTEST_MAIN(TestWinDbgProvider)

159
tools/test_hover.py Normal file
View File

@@ -0,0 +1,159 @@
"""
Structural hover test: validate that all themes produce visible hover colors
and that the QProxyStyle code handles the required control elements.
No pixel sampling — checks theme JSON values and source code patterns.
"""
import json
import os
import re
import sys
def hex_to_rgb(h):
h = h.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def color_dist(c1, c2):
return sum(abs(a - b) for a, b in zip(c1, c2))
def lighter_130(rgb):
"""Approximate Qt's QColor::lighter(130) for dark grays."""
r, g, b = rgb
return (min(255, int(r * 1.3) + 1),
min(255, int(g * 1.3) + 1),
min(255, int(b * 1.3) + 1))
def load_themes():
themes = {}
theme_dir = os.path.join(os.path.dirname(__file__),
'..', 'src', 'themes', 'defaults')
if not os.path.isdir(theme_dir):
return themes
for name in os.listdir(theme_dir):
if name.endswith('.json'):
with open(os.path.join(theme_dir, name)) as f:
themes[name] = json.load(f)
return themes
def test_hover_visibility(themes):
"""Every theme must have hover visually distinct from background.
If raw values are identical, Theme::fromJson applies lighter(130)."""
ok = True
for name, data in sorted(themes.items()):
bg = hex_to_rgb(data['background'])
hover = hex_to_rgb(data['hover'])
dist = color_dist(bg, hover)
if dist < 20:
# fromJson will fix this — verify the fix produces sufficient contrast
fixed = lighter_130(bg)
fixed_dist = color_dist(bg, fixed)
if fixed_dist < 15:
print(f" FAIL: {name}: hover==bg and lighter(130) still too close "
f"(dist={fixed_dist})")
ok = False
else:
print(f" OK: {name}: hover==bg, fromJson fixup -> "
f"dist {dist}->{fixed_dist}")
else:
print(f" OK: {name}: hover distinct (dist={dist})")
return ok
def test_proxystyle_handlers():
"""Verify MenuBarStyle handles CE_MenuBarItem, CE_MenuItem, CE_MenuBarEmptyArea."""
src = os.path.join(os.path.dirname(__file__), '..', 'src', 'main.cpp')
with open(src) as f:
code = f.read()
required = {
'CE_MenuBarItem': r'element\s*==\s*CE_MenuBarItem',
'CE_MenuItem': r'element\s*==\s*CE_MenuItem',
'CE_MenuBarEmptyArea': r'element\s*==\s*CE_MenuBarEmptyArea',
'State_Selected': r'State_Selected',
'QPalette::Mid': r'QPalette::Mid',
}
ok = True
for label, pattern in required.items():
if re.search(pattern, code):
print(f" OK: MenuBarStyle handles {label}")
else:
print(f" FAIL: MenuBarStyle missing {label}")
ok = False
return ok
def test_no_menubar_css():
"""Verify no CSS stylesheet is set on QMenuBar (would bypass QProxyStyle)."""
src_dir = os.path.join(os.path.dirname(__file__), '..', 'src')
ok = True
for root, _, files in os.walk(src_dir):
for fname in files:
if not fname.endswith('.cpp'):
continue
path = os.path.join(root, fname)
with open(path, encoding='utf-8', errors='replace') as f:
for i, line in enumerate(f, 1):
# Check for menuBar/m_menuBar stylesheet calls
if ('menuBar' in line or 'm_menuBar' in line) and \
'setStyleSheet' in line:
print(f" FAIL: CSS on QMenuBar at {fname}:{i}: "
f"{line.strip()}")
ok = False
if ok:
print(" OK: No CSS on QMenuBar")
return ok
def test_hover_fixup_in_fromjson():
"""Verify Theme::fromJson applies the hover fixup."""
src = os.path.join(os.path.dirname(__file__),
'..', 'src', 'themes', 'theme.cpp')
with open(src) as f:
code = f.read()
if 'lighter(130)' in code and 't.hover' in code:
print(" OK: Theme::fromJson has hover fixup")
return True
else:
print(" FAIL: Theme::fromJson missing hover fixup")
return False
def main():
themes = load_themes()
if not themes:
print("FAIL: No theme files found")
return 1
all_ok = True
print("--- Test 1: Hover visibility across themes ---")
all_ok &= test_hover_visibility(themes)
print("\n--- Test 2: QProxyStyle handles required elements ---")
all_ok &= test_proxystyle_handlers()
print("\n--- Test 3: No CSS on QMenuBar ---")
all_ok &= test_no_menubar_css()
print("\n--- Test 4: Theme::fromJson hover fixup ---")
all_ok &= test_hover_fixup_in_fromjson()
print(f"\n{'='*50}")
if all_ok:
print("ALL HOVER TESTS PASSED")
return 0
else:
print("SOME HOVER TESTS FAILED")
return 1
if __name__ == '__main__':
sys.exit(main())