feat: Remote Process Memory plugin, source menu icons, base address fix

- Remote Process Memory plugin: shared-memory IPC payload injected into
  target process (CreateRemoteThread on Win, ptrace+dlopen on Linux),
  VirtualQuery-based memory safety, PEB-based image base, batch reads
- Source dropdown: SVG icons per provider type, DLL filename shown
- Fix base address not updating when switching to a new source provider
- ProviderRegistry carries DLL filename from PluginManager

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
IChooseYou
2026-02-22 07:29:56 -07:00
parent 1d7d384b93
commit 5e11ff5496
15 changed files with 2813 additions and 21 deletions

View File

@@ -2302,8 +2302,7 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
// Re-evaluate stored formula against the new provider
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
@@ -2352,6 +2351,9 @@ void RcxController::switchToSavedSource(int idx) {
// Restore formula before attach so it can be re-evaluated against the new provider
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
attachViaPlugin(entry.kind, entry.providerTarget);
// Restore saved base address (user may have navigated away from provider default)
if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty())
m_doc->tree.baseAddress = entry.baseAddress;
}
}
@@ -2421,8 +2423,7 @@ void RcxController::selectSource(const QString& text) {
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
resetSnapshot();
emit m_doc->documentChanged();

View File

@@ -909,7 +909,7 @@ void RcxEditor::reformatMargins() {
// Place offset in the parent's indent slot (one level above the field's own indent)
// so the field's own 3-char indent acts as visual separator from the type column
int col = kFoldCol + (lm.depth - 2) * 3;
int slotWidth = 3;
int slotWidth = 5;
auto pos = [&](int c) -> long {
return m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
@@ -1756,8 +1756,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
}
commitInlineEdit();
m_currentSelIds.clear(); // stale — normal handler will re-establish
// Fall through to normal click handler below
m_currentSelIds.clear();
return true; // consume — metadata was recomposed; stale coords unsafe
}
// Single-click on fold column (" - " / " + ") toggles fold
// Other left-clicks emit nodeClicked for selection

View File

@@ -97,6 +97,53 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
#endif
fflush(stderr);
// Phase 1.5: write a full minidump next to the executable
{
// Build dump path: <exe_dir>/reclass_crash_<YYYYMMDD_HHMMSS>.dmp
wchar_t exePath[MAX_PATH] = {};
GetModuleFileNameW(NULL, exePath, MAX_PATH);
// Strip exe filename to get directory
wchar_t* lastSlash = wcsrchr(exePath, L'\\');
if (lastSlash) *(lastSlash + 1) = L'\0';
SYSTEMTIME st;
GetLocalTime(&st);
wchar_t dumpPath[MAX_PATH];
_snwprintf(dumpPath, MAX_PATH,
L"%sreclass_crash_%04d%02d%02d_%02d%02d%02d.dmp",
exePath, st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond);
HANDLE hFile = CreateFileW(dumpPath, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION mei;
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = ep;
mei.ClientPointers = FALSE;
// MiniDumpWithFullMemory: captures entire process address space
// so we can inspect all heap objects, Qt state, node trees, etc.
BOOL ok = MiniDumpWriteDump(
GetCurrentProcess(), GetCurrentProcessId(), hFile,
static_cast<MINIDUMP_TYPE>(MiniDumpWithFullMemory
| MiniDumpWithHandleData
| MiniDumpWithThreadInfo
| MiniDumpWithUnloadedModules),
&mei, NULL, NULL);
CloseHandle(hFile);
if (ok) {
fprintf(stderr, "Dump : %ls\n", dumpPath);
} else {
fprintf(stderr, "Dump : FAILED (error %lu)\n", GetLastError());
}
} else {
fprintf(stderr, "Dump : could not create file (error %lu)\n", GetLastError());
}
fflush(stderr);
}
// Phase 2: attempt symbol resolution + stack walk
// Copy context so StackWalk64 can mutate it safely
CONTEXT ctxCopy = *ep->ContextRecord;
@@ -689,9 +736,11 @@ private:
label->setGeometry(tw + 1 + gutter, 0,
qMax(0, width() - (tw + 1 + gutter)), h);
// Shared baseline so tab text and status text align
// Shared baseline so tab text and status text align.
// Nudge up by half the accent-line height so text centres
// in the visible area below the accent bar, not in the full bar.
QFontMetrics fm(font());
int by = (h + fm.ascent()) / 2;
int by = (h + fm.ascent()) / 2 - (ViewTabButton::kAccentH + 1) / 2;
// Push baseline to buttons
auto* lay = tabRow->layout();
@@ -1136,6 +1185,7 @@ void MainWindow::selfTest() {
// Attach process memory to self — provider base will be set to the editor address
DWORD pid = GetCurrentProcessId();
QString target = QString("%1:Reclass.exe").arg(pid);
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
#else
project_new();
@@ -2222,14 +2272,31 @@ void MainWindow::populateSourceMenu() {
m_sourceMenu->clear();
auto* ctrl = activeController();
m_sourceMenu->addAction("File", this, [this]() {
// Icon map for known provider identifiers
static const QHash<QString, QString> s_providerIcons = {
{QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")},
{QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")},
{QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")},
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
};
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")),
QStringLiteral("File"), this, [this]() {
if (auto* c = activeController()) c->selectSource(QStringLiteral("File"));
});
const auto& providers = ProviderRegistry::instance().providers();
for (const auto& prov : providers) {
QString name = prov.name;
m_sourceMenu->addAction(name, this, [this, name]() {
auto it = s_providerIcons.constFind(prov.identifier);
QIcon icon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg"));
QString label = prov.dllFileName.isEmpty()
? name
: QStringLiteral("%1 (%2)").arg(name, prov.dllFileName);
m_sourceMenu->addAction(icon, label, this, [this, name]() {
if (auto* c = activeController()) c->selectSource(name);
});
}
@@ -2247,7 +2314,8 @@ void MainWindow::populateSourceMenu() {
act->setChecked(i == ctrl->activeSourceIndex());
}
m_sourceMenu->addSeparator();
m_sourceMenu->addAction("Clear All", this, [this]() {
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/clear-all.svg")),
QStringLiteral("Clear All"), this, [this]() {
if (auto* c = activeController()) c->clearSources();
});
}

View File

@@ -92,7 +92,8 @@ bool PluginManager::LoadPlugin(const QString& path)
IProviderPlugin* provider = static_cast<IProviderPlugin*>(plugin);
QString name = QString::fromStdString(plugin->Name());
QString identifier = name.toLower().replace(" ", "");
ProviderRegistry::instance().registerProvider(name, identifier, provider);
QString dllFileName = QFileInfo(path).fileName();
ProviderRegistry::instance().registerProvider(name, identifier, provider, dllFileName);
}
return true;

View File

@@ -6,7 +6,8 @@ ProviderRegistry& ProviderRegistry::instance() {
return s_instance;
}
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) {
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier,
IProviderPlugin* plugin, const QString& dllFileName) {
// Check if already registered
for (const auto& info : m_providers) {
if (info.identifier == identifier) {
@@ -14,8 +15,8 @@ void ProviderRegistry::registerProvider(const QString& name, const QString& iden
return;
}
}
m_providers.append(ProviderInfo(name, identifier, plugin));
m_providers.append(ProviderInfo(name, identifier, plugin, dllFileName));
qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")";
}

View File

@@ -25,10 +25,13 @@ public:
IProviderPlugin* plugin; // Plugin (if plugin-based)
BuiltinFactory factory; // Factory (if built-in)
bool isBuiltin;
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p)
: name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {}
QString dllFileName; // Original DLL/SO filename (plugin-based only)
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p,
const QString& dll = {})
: name(n), identifier(id), plugin(p), factory(nullptr),
isBuiltin(false), dllFileName(dll) {}
ProviderInfo(const QString& n, const QString& id, BuiltinFactory f)
: name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {}
};
@@ -36,7 +39,8 @@ public:
static ProviderRegistry& instance();
// Register a plugin-based provider
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin);
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin,
const QString& dllFileName = {});
// Register a built-in provider with a factory function
void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory);

View File

@@ -51,5 +51,9 @@
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
<file alias="folder.svg">vsicons/folder.svg</file>
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
<file alias="server-process.svg">vsicons/server-process.svg</file>
<file alias="remote.svg">vsicons/remote.svg</file>
<file alias="plug.svg">vsicons/plug.svg</file>
<file alias="clear-all.svg">vsicons/clear-all.svg</file>
</qresource>
</RCC>