feat: kernel memory plugin + unified source menu + driver improvements

- KernelMemory plugin: kernel-mode process/physical memory R/W via IOCTL driver
- rcxdrv.sys: MmCopyMemory for reads, MDL mapping with correct cache types
  (MmCached for RAM, MmNonCached for MMIO only — fixes cache corruption BSOD)
- Driver reconnect: ensureDriverLoaded tries device handle first, no auto
  stop+delete cycle. Manual unload closes handle only, service stays running.
- Unified source menu: ProviderRegistry::populateSourceMenu() shared by both
  main window Data Source menu and RcxEditor inline picker (icons + dll names)
- IProviderPlugin::populatePluginMenu() for conditional plugin actions
  (e.g. "Unload Kernel Driver" only when loaded)
- Physical memory mode removed from selectTarget (access via context menu only)
- requestOpenProviderTab sets base address from provider after template load
- Address parser: vtop(), cr3(), physRead() callbacks for kernel paging expressions
This commit is contained in:
IChooseYou
2026-03-13 14:46:22 -06:00
committed by IChooseYou
parent 7f7bbdcc45
commit b08736245b
22 changed files with 2671 additions and 120 deletions

View File

@@ -273,6 +273,7 @@ private:
// Identifier or hex literal disambiguation.
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
// Otherwise → backtrack and parse as hex number.
// If the identifier is followed by '(', try to parse as a built-in function call.
bool parseIdentifierOrHex(uint64_t& result) {
int start = m_pos;
bool hasNonHex = false;
@@ -292,6 +293,11 @@ private:
return parseHexNumber(result);
}
// Check for function call syntax: identifier '(' args ')'
skipSpaces();
if (peek() == '(')
return parseFunctionCall(token, result);
// It's an identifier — resolve via callback
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
result = 0;
@@ -305,6 +311,71 @@ private:
return true;
}
// Built-in function call: vtop(pid, va), cr3(pid), phys(addr)
bool parseFunctionCall(const QString& name, uint64_t& result) {
advance(); // skip '('
if (name == QStringLiteral("vtop")) {
// vtop(pid, virtualAddress) → physical address
uint64_t pid = 0;
if (!parseBitwiseOr(pid)) return false;
skipSpaces();
if (peek() != ',')
return fail("vtop() requires 2 arguments: vtop(pid, va)");
advance(); // skip ','
uint64_t va = 0;
if (!parseBitwiseOr(va)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->vtop) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->vtop((uint32_t)pid, va, &ok);
if (!ok)
return fail(QStringLiteral("vtop(0x%1, 0x%2) failed")
.arg(pid, 0, 16).arg(va, 0, 16));
return true;
}
if (name == QStringLiteral("cr3")) {
// cr3(pid) → CR3 value
uint64_t pid = 0;
if (!parseBitwiseOr(pid)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->cr3) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->cr3((uint32_t)pid, &ok);
if (!ok)
return fail(QStringLiteral("cr3(%1) failed").arg(pid));
return true;
}
if (name == QStringLiteral("phys")) {
// phys(addr) → read 8 bytes from physical address
uint64_t addr = 0;
if (!parseBitwiseOr(addr)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->physRead) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->physRead(addr, &ok);
if (!ok)
return fail(QStringLiteral("phys(0x%1) failed").arg(addr, 0, 16));
return true;
}
return fail(QStringLiteral("unknown function '%1'").arg(name));
}
// '[' bitwiseOr ']' — read the pointer value at the computed address
bool parseDereference(uint64_t& result) {
advance(); // skip '['

View File

@@ -16,6 +16,11 @@ struct AddressParserCallbacks {
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
// Kernel paging functions (optional — only wired when kernel provider active)
std::function<uint64_t(uint32_t pid, uint64_t va, bool* ok)> vtop;
std::function<uint64_t(uint32_t pid, bool* ok)> cr3;
std::function<uint64_t(uint64_t physAddr, bool* ok)> physRead;
};
class AddressParser {

View File

@@ -695,6 +695,11 @@ void composeParent(ComposeState& state, const NodeTree& tree,
*ok = false;
return 0;
};
cbs.resolveModule = [&prov](const QString& name, bool* ok) -> uint64_t {
uint64_t base = prov.symbolToAddress(name);
*ok = (base != 0);
return base;
};
return cbs;
};
@@ -827,6 +832,43 @@ void composeParent(ComposeState& state, const NodeTree& tree,
}
}
// Static pointer: read pointer value at evaluated addr, expand ref struct
if (exprOk && sf.refId != 0
&& (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)) {
int psz = sf.byteSize();
uint64_t ptrVal = 0;
if (prov.isValid() && psz > 0 && prov.isReadable(staticAddr, psz)) {
ptrVal = (sf.kind == NodeKind::Pointer32)
? (uint64_t)prov.readU32(staticAddr) : prov.readU64(staticAddr);
if (ptrVal == UINT64_MAX || (sf.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
ptrVal = 0;
}
// Relative pointer (RVA): target = base + value
if (sf.isRelative && ptrVal != 0)
ptrVal += absAddr;
if (ptrVal != 0) {
uint64_t pBase = ptrVal;
bool ptrReadable = prov.isReadable(pBase, 1);
static NullProvider s_nullProv2;
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv2);
if (!ptrReadable) pBase = 0;
int refIdx = tree.indexOfId(sf.refId);
if (refIdx >= 0) {
const Node& ref = tree.nodes[refIdx];
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
uint64_t savedPtrBase = state.currentPtrBase;
state.currentPtrBase = pBase;
composeParent(state, tree, childProv, refIdx,
childDepth, pBase, ref.id,
/*isArrayChild=*/true);
state.currentPtrBase = savedPtrBase;
}
}
}
}
// Footer line: "};"
{
LineMeta flm;
@@ -893,6 +935,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
&& node.refId != 0) {
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
if (node.isRelative)
ptrTypeOverride += QStringLiteral(" rva");
// Check if this pointer has materialized children (from materializeRefChildren)
const QVector<int>& ptrChildren = childIndices(state, node.id);
@@ -961,7 +1005,10 @@ void composeNode(ComposeState& state, const NodeTree& tree,
}
}
// Pointer target address is used directly (absolute)
// Relative pointer (RVA): target = base + value
if (node.isRelative && ptrVal != 0)
ptrVal += base;
uint64_t pBase = ptrVal;
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);

View File

@@ -17,6 +17,7 @@
#include <QFileDialog>
#include <QMessageBox>
#include <QSettings>
#include <QRegularExpression>
#include <QtConcurrent/QtConcurrentRun>
#include <limits>
@@ -441,13 +442,35 @@ void RcxController::connectEditor(RcxEditor* editor) {
*ok = prov->read(addr, &val, ptrSz);
return val;
};
// Wire kernel paging callbacks if provider supports it
if (prov->hasKernelPaging()) {
cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t {
Q_UNUSED(pid);
auto r = prov->translateAddress(va);
*ok = r.valid;
return r.physical;
};
cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t {
Q_UNUSED(pid);
uint64_t cr3 = prov->getCr3();
*ok = (cr3 != 0);
return cr3;
};
cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t {
auto entries = prov->readPageTable(physAddr, 0, 1);
*ok = !entries.isEmpty();
return entries.isEmpty() ? 0 : entries[0];
};
}
}
auto result = AddressParser::evaluate(s, m_doc->tree.pointerSize, &cbs);
if (result.ok && result.value != m_doc->tree.baseAddress) {
uint64_t oldBase = m_doc->tree.baseAddress;
QString oldFormula = m_doc->tree.baseAddressFormula;
// Store formula if input uses module/deref syntax, otherwise clear
QString newFormula = (s.contains('<') || s.contains('[')) ? s : QString();
// Store formula if input uses module/deref/kernel-function syntax
static const QRegularExpression formulaRx(
QStringLiteral("[<\\[]|\\b(?:vtop|cr3|phys)\\s*\\("));
QString newFormula = formulaRx.match(s).hasMatch() ? s : QString();
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula}));
}
@@ -2440,6 +2463,103 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
});
// ── Kernel paging menu items ──
if (m_doc->provider && m_doc->provider->hasKernelPaging()) {
menu.addSeparator();
auto* kernelMenu = menu.addMenu(icon("symbol-key.svg"), "Kernel");
// Show Physical Address — translate the node's VA to physical
if (hasNode) {
uint64_t nodeAddr = m_doc->tree.baseAddress
+ m_doc->tree.computeOffset(nodeIdx);
kernelMenu->addAction("Show Physical Address", [this, nodeAddr, &menu]() {
auto result = m_doc->provider->translateAddress(nodeAddr);
if (result.valid) {
const char* pageSz = result.pageSize == 2 ? "1 GB"
: result.pageSize == 1 ? "2 MB" : "4 KB";
QString msg = QStringLiteral(
"Virtual: 0x%1\n"
"Physical: 0x%2\n"
"Page Size: %3\n\n"
"PML4E: 0x%4\n"
"PDPTE: 0x%5\n"
"PDE: 0x%6\n"
"PTE: 0x%7")
.arg(nodeAddr, 16, 16, QChar('0'))
.arg(result.physical, 16, 16, QChar('0'))
.arg(pageSz)
.arg(result.pml4e, 16, 16, QChar('0'))
.arg(result.pdpte, 16, 16, QChar('0'))
.arg(result.pde, 16, 16, QChar('0'))
.arg(result.pte, 16, 16, QChar('0'));
QMessageBox::information(
qobject_cast<QWidget*>(parent()),
QStringLiteral("Physical Address"), msg);
} else {
QMessageBox::warning(
qobject_cast<QWidget*>(parent()),
QStringLiteral("Translation Failed"),
QStringLiteral("Address 0x%1 is not mapped")
.arg(nodeAddr, 16, 16, QChar('0')));
}
});
}
// Browse Page Tables — open PML4 in a new physical tab
kernelMenu->addAction("Browse Page Tables", [this]() {
uint64_t cr3 = m_doc->provider->getCr3();
if (cr3 == 0) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()),
QStringLiteral("Error"),
QStringLiteral("Failed to read CR3"));
return;
}
emit requestOpenProviderTab(
QStringLiteral("kernelmemory"),
QStringLiteral("phys:%1").arg(cr3, 0, 16),
QStringLiteral("PML4 @ 0x%1").arg(cr3, 0, 16));
});
// Follow Physical Frame — on a PTE bitfield, extract PhysAddr and open
if (hasNode) {
const auto& node = m_doc->tree.nodes[nodeIdx];
if (node.classKeyword == QStringLiteral("bitfield")) {
for (const auto& bf : node.bitfieldMembers) {
if (bf.name == QStringLiteral("PhysAddr")) {
int bitOff = bf.bitOffset;
int bitWid = bf.bitWidth;
uint64_t nodeAddr = m_doc->tree.baseAddress
+ m_doc->tree.computeOffset(nodeIdx);
kernelMenu->addAction("Follow Physical Frame",
[this, nodeAddr, bitOff, bitWid]() {
uint64_t pteValue = 0;
if (!m_doc->provider->read(nodeAddr, &pteValue, 8)) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()),
QStringLiteral("Error"),
QStringLiteral("Failed to read PTE at 0x%1")
.arg(nodeAddr, 0, 16));
return;
}
uint64_t mask = (1ULL << bitWid) - 1;
uint64_t frame = ((pteValue >> bitOff) & mask) << bitOff;
if (frame == 0) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()),
QStringLiteral("Error"),
QStringLiteral("Physical frame is zero (not present?)"));
return;
}
emit requestOpenProviderTab(
QStringLiteral("kernelmemory"),
QStringLiteral("phys:%1").arg(frame, 0, 16),
QStringLiteral("PT @ 0x%1").arg(frame, 0, 16));
});
break;
}
}
}
}
}
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
}
@@ -3208,6 +3328,26 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
*ok = prov->read(addr, &val, ptrSz);
return val;
};
// Wire kernel paging callbacks if provider supports it
if (prov->hasKernelPaging()) {
cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t {
Q_UNUSED(pid); // current provider already targets a specific process
auto r = prov->translateAddress(va);
*ok = r.valid;
return r.physical;
};
cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t {
Q_UNUSED(pid);
uint64_t cr3 = prov->getCr3();
*ok = (cr3 != 0);
return cr3;
};
cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t {
auto entries = prov->readPageTable(physAddr, 0, 1);
*ok = !entries.isEmpty();
return entries.isEmpty() ? 0 : entries[0];
};
}
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, ptrSz, &cbs);
if (result.ok)
m_doc->tree.baseAddress = result.value;
@@ -3330,6 +3470,26 @@ void RcxController::selectSource(const QString& text) {
*ok = prov->read(addr, &val, ptrSz);
return val;
};
// Wire kernel paging callbacks if provider supports it
if (prov->hasKernelPaging()) {
cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t {
Q_UNUSED(pid);
auto r = prov->translateAddress(va);
*ok = r.valid;
return r.physical;
};
cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t {
Q_UNUSED(pid);
uint64_t cr3 = prov->getCr3();
*ok = (cr3 != 0);
return cr3;
};
cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t {
auto entries = prov->readPageTable(physAddr, 0, 1);
*ok = !entries.isEmpty();
return entries.isEmpty() ? 0 : entries[0];
};
}
auto result = AddressParser::evaluate(
m_doc->tree.baseAddressFormula, ptrSz, &cbs);
if (result.ok)

View File

@@ -163,6 +163,8 @@ signals:
void nodeSelected(int nodeIdx);
void selectionChanged(int count);
void contextMenuAboutToShow(QMenu* menu, int line);
void requestOpenProviderTab(const QString& pluginId, const QString& target,
const QString& title);
private:
RcxDocument* m_doc;

View File

@@ -197,6 +197,7 @@ struct Node {
int offset = 0;
bool isStatic = false; // static field — excluded from struct layout
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
bool isRelative = false; // Pointer: target = base + value (RVA) instead of absolute
int arrayLen = 1; // Array: element count
int strLen = 64;
bool collapsed = true;
@@ -242,6 +243,8 @@ struct Node {
o["isStatic"] = true;
if (!offsetExpr.isEmpty())
o["offsetExpr"] = offsetExpr;
if (isRelative)
o["isRelative"] = true;
o["arrayLen"] = arrayLen;
o["strLen"] = strLen;
o["collapsed"] = collapsed;
@@ -283,6 +286,7 @@ struct Node {
n.offset = o["offset"].toInt(0);
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
n.offsetExpr = o["offsetExpr"].toString();
n.isRelative = o["isRelative"].toBool(false);
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
n.collapsed = o["collapsed"].toBool(true);
@@ -677,6 +681,7 @@ namespace cmd {
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
struct ToggleRelative { uint64_t nodeId; bool oldVal, newVal; };
}
using Command = std::variant<
@@ -684,7 +689,7 @@ using Command = std::variant<
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
cmd::ChangeOffsetExpr, cmd::ToggleStatic
cmd::ChangeOffsetExpr, cmd::ToggleStatic, cmd::ToggleRelative
>;
// ── Column spans (for inline editing) ──

View File

@@ -2377,12 +2377,6 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
int line, col;
m_sci->getCursorPosition(&line, &col);
int minCol = m_editState.spanStart;
// Don't allow backing into "0x" prefix
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
QString lineText = getLineText(m_sci, m_editState.line);
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
minCol = m_editState.spanStart + 2;
}
// If there's an active selection, collapse it to the left end (Left only, not Backspace)
if (ke->key() == Qt::Key_Left) {
int sL, sC, eL, eC;
@@ -2410,17 +2404,9 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
if (col >= editEndCol()) return true; // block past end
return false;
}
case Qt::Key_Home: {
int home = m_editState.spanStart;
// Skip "0x" prefix for hex values
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
QString lineText = getLineText(m_sci, m_editState.line);
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
home = m_editState.spanStart + 2;
}
m_sci->setCursorPosition(m_editState.line, home);
case Qt::Key_Home:
m_sci->setCursorPosition(m_editState.line, m_editState.spanStart);
return true;
}
case Qt::Key_End:
m_sci->setCursorPosition(m_editState.line, editEndCol());
return true;
@@ -2865,21 +2851,21 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|| target == EditTarget::PointerTarget
|| target == EditTarget::RootClassType);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
if (!isPicker)
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
ThemeManager::instance().current().selection);
if (!isPicker) {
// Subtle tint derived from theme background (neutral, not blue)
const auto& bg = ThemeManager::instance().current().background;
int shift = (bg.lightness() < 128) ? 25 : -25;
QColor tint(qBound(0, bg.red() + shift, 255),
qBound(0, bg.green() + shift, 255),
qBound(0, bg.blue() + shift, 255));
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, tint);
}
// Use correct UTF-8 position conversion (not lineStart + col!)
m_editState.posStart = posFromCol(m_sci, line, norm.start);
m_editState.posEnd = posFromCol(m_sci, line, norm.end);
// For Value/BaseAddress: skip 0x prefix in selection (select only the number)
long selStart = m_editState.posStart;
if ((target == EditTarget::Value || target == EditTarget::BaseAddress) &&
trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
selStart = m_editState.posStart + 2; // Skip "0x"
}
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, selStart, m_editState.posEnd);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, m_editState.posStart, m_editState.posEnd);
// Hex overwrite: place cursor at start, no selection
if (m_editState.hexOverwrite)
@@ -3062,26 +3048,8 @@ void RcxEditor::showSourcePicker() {
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
menuFont.setPointSize(menuFont.pointSize() + zoom);
menu.setFont(menuFont);
menu.addAction("File");
// Add all registered providers from global registry
const auto& providers = ProviderRegistry::instance().providers();
for (const auto& provider : providers)
menu.addAction(provider.name);
// Saved sources below separator (with checkmarks)
if (!m_savedSourceDisplay.isEmpty()) {
menu.addSeparator();
for (int i = 0; i < m_savedSourceDisplay.size(); i++) {
auto* act = menu.addAction(m_savedSourceDisplay[i].text);
act->setCheckable(true);
act->setChecked(m_savedSourceDisplay[i].active);
act->setData(i);
}
menu.addSeparator();
auto* clearAct = menu.addAction("Clear All");
clearAct->setData(QStringLiteral("#clear"));
}
ProviderRegistry::populateSourceMenu(&menu, m_savedSourceDisplay);
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
@@ -3095,11 +3063,13 @@ void RcxEditor::showSourcePicker() {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit();
QString text = sel->text();
if (sel->data().toString() == QStringLiteral("#clear"))
text = QStringLiteral("#clear");
else if (sel->data().isValid())
text = QStringLiteral("#saved:") + QString::number(sel->data().toInt());
// Route via action data (set by populateSourceMenu)
QString text = sel->data().toString();
if (text.isEmpty()) {
// Plugin action (e.g. "Unload Driver") — already handled by its own lambda
cancelInlineEdit();
return;
}
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
} else {
cancelInlineEdit();

View File

@@ -1,5 +1,6 @@
#pragma once
#include "core.h"
#include "providerregistry.h"
#include "themes/theme.h"
#include <QWidget>
#include <QSet>
@@ -12,11 +13,6 @@ class QsciLexerCPP;
namespace rcx {
struct SavedSourceDisplay {
QString text;
bool active = false;
};
class RcxEditor : public QWidget {
Q_OBJECT
public:

View File

@@ -10,8 +10,9 @@
#define RCX_PLUGIN_EXPORT __attribute__((visibility("default")))
#endif
// Forward declaration
// Forward declarations
namespace rcx { class Provider; }
class QMenu;
/**
* Plugin interface for Reclass
@@ -129,6 +130,13 @@ public:
* @return true if enumerateProcesses() should be called
*/
virtual bool providesProcessList() const { return false; }
/**
* Add plugin-specific actions to the source menu (optional).
* Called each time the source menu is shown. Only add items when relevant
* (e.g., "Unload Driver" only when the driver is loaded).
*/
virtual void populatePluginMenu(QMenu*) {}
};
// Plugin factory function signature

View File

@@ -58,6 +58,7 @@
#include <windowsx.h>
#include <dwmapi.h>
#include <dbghelp.h>
#include <shellapi.h>
#include <cstdio>
static void setDarkTitleBar(QWidget* widget) {
@@ -552,7 +553,7 @@ void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setWindowTitle("Reclass");
resize(1200, 800);
resize(1080, 720);
#ifndef __APPLE__
// Frameless window with system menu (Alt+Space) and min/max/close support.
@@ -755,6 +756,52 @@ void MainWindow::createMenus() {
file->addSeparator();
Qt5Qt6AddAction(file, "&Close Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
file->addSeparator();
#ifdef _WIN32
{
// "Relaunch as Administrator" — hidden when already elevated
bool elevated = false;
HANDLE token = nullptr;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
TOKEN_ELEVATION elev{};
DWORD sz = sizeof(elev);
if (GetTokenInformation(token, TokenElevation, &elev, sizeof(elev), &sz))
elevated = (elev.TokenIsElevated != 0);
CloseHandle(token);
}
if (!elevated) {
Qt5Qt6AddAction(file, "Relaunch as &Administrator",
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A),
makeIcon(":/vsicons/shield.svg"), this, [this]() {
wchar_t exePath[MAX_PATH];
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
SHELLEXECUTEINFOW sei{};
sei.cbSize = sizeof(sei);
sei.lpVerb = L"runas";
sei.lpFile = exePath;
sei.nShow = SW_SHOWNORMAL;
if (ShellExecuteExW(&sei))
QCoreApplication::quit();
// If UAC was cancelled, do nothing
});
file->addSeparator();
}
}
#endif
m_sourceMenu = file->addMenu("&Data Source");
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
connect(m_sourceMenu, &QMenu::triggered, this, [this](QAction* act) {
auto* c = activeController();
if (!c) return;
QString data = act->data().toString();
if (data.isEmpty()) return; // plugin actions handle themselves via lambda
if (data == QStringLiteral("#clear"))
c->clearSources();
else if (data.startsWith(QStringLiteral("#saved:")))
c->switchSource(data.mid(7).toInt());
else
c->selectSource(data);
});
file->addSeparator();
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
// Edit
@@ -785,9 +832,6 @@ void MainWindow::createMenus() {
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
});
view->addSeparator();
m_sourceMenu = view->addMenu("&Data Source");
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
view->addSeparator();
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
auto* fontGroup = new QActionGroup(this);
fontGroup->setExclusive(true);
@@ -1888,6 +1932,33 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
menu->addAction("Close Tab", [dock]() { dock->close(); });
});
// Open a new tab with a plugin-provided provider (e.g. kernel physical memory)
connect(ctrl, &RcxController::requestOpenProviderTab,
this, [this](const QString& pluginId, const QString& target,
const QString& title) {
auto* newDoc = new RcxDocument(this);
QByteArray data(4096, '\0');
newDoc->loadData(data);
newDoc->tree.baseAddress = 0;
auto* newDock = createTab(newDoc);
auto it = m_tabs.find(newDock);
if (it != m_tabs.end()) {
it->ctrl->attachViaPlugin(pluginId, target);
// Try to load PageTables.rcx template for physical kernel tabs
QString examplesPath = QCoreApplication::applicationDirPath()
+ QStringLiteral("/examples/PageTables.rcx");
if (QFile::exists(examplesPath))
newDoc->load(examplesPath);
// Set base address from provider (template has baseAddress=0,
// but we want to start at the target physical address)
if (newDoc->provider)
newDoc->tree.baseAddress = newDoc->provider->base();
}
newDock->setWindowTitle(title);
rebuildWorkspaceModelNow();
});
// Update rendered panes and workspace on document changes and undo/redo
// Use QPointer to guard against dock being destroyed before deferred timer fires
QPointer<QDockWidget> dockGuard = dock;
@@ -4633,63 +4704,19 @@ void MainWindow::populateSourceMenu() {
m_sourceMenu->clear();
auto* ctrl = activeController();
// 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")},
};
auto addSourceAction = [this](const QString& text, const QIcon& icon, auto&& slot) {
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"));
});
const auto& providers = ProviderRegistry::instance().providers();
for (const auto& prov : providers) {
QString name = prov.name;
auto it = s_providerIcons.constFind(prov.identifier);
QIcon icon = makeIcon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg"));
QString label = prov.dllFileName.isEmpty()
? name
: QStringLiteral("%1 (%2)").arg(name, prov.dllFileName);
addSourceAction(label, icon, [this, name]() {
if (auto* c = activeController()) c->selectSource(name);
});
}
if (ctrl && !ctrl->savedSources().isEmpty()) {
m_sourceMenu->addSeparator();
for (int i = 0; i < ctrl->savedSources().size(); i++) {
const auto& e = ctrl->savedSources()[i];
auto* act = m_sourceMenu->addAction(
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName));
act->setCheckable(true);
act->setChecked(i == ctrl->activeSourceIndex());
connect(act, &QAction::triggered, this, [this, i]() {
if (auto* c = activeController()) c->switchSource(i);
});
// Build saved sources for the shared menu builder
QVector<SavedSourceDisplay> saved;
if (ctrl) {
const auto& ss = ctrl->savedSources();
for (int i = 0; i < ss.size(); i++) {
SavedSourceDisplay d;
d.text = QStringLiteral("%1 '%2'").arg(ss[i].kind, ss[i].displayName);
d.active = (i == ctrl->activeSourceIndex());
saved.append(d);
}
m_sourceMenu->addSeparator();
auto* clearAct = addSourceAction(QStringLiteral("Clear All"),
makeIcon(QStringLiteral(":/vsicons/clear-all.svg")),
[this]() {
if (auto* c = activeController()) c->clearSources();
});
Q_UNUSED(clearAct);
}
ProviderRegistry::populateSourceMenu(m_sourceMenu, saved);
}
void MainWindow::showPluginsDialog() {

View File

@@ -1,5 +1,8 @@
#include "providerregistry.h"
#include <QDebug>
#include <QMenu>
#include <QIcon>
#include <QHash>
ProviderRegistry& ProviderRegistry::instance() {
static ProviderRegistry s_instance;
@@ -56,3 +59,57 @@ const ProviderRegistry::ProviderInfo* ProviderRegistry::findProvider(const QStri
void ProviderRegistry::clear() {
m_providers.clear();
}
void ProviderRegistry::populateSourceMenu(QMenu* menu,
const QVector<SavedSourceDisplay>& savedSources)
{
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")},
};
// File source
auto* fileAct = menu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")),
QStringLiteral("File"));
fileAct->setIconVisibleInMenu(true);
fileAct->setData(QStringLiteral("File"));
// Registered providers
const auto& providers = instance().providers();
for (const auto& prov : providers) {
auto it = s_providerIcons.constFind(prov.identifier);
QIcon icon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg"));
QString label = prov.dllFileName.isEmpty()
? prov.name
: QStringLiteral("%1 (%2)").arg(prov.name, prov.dllFileName);
auto* act = menu->addAction(icon, label);
act->setIconVisibleInMenu(true);
act->setData(prov.name); // routing key for selectSource()
// Plugin-specific actions (e.g. "Unload Driver" when loaded)
if (prov.plugin)
prov.plugin->populatePluginMenu(menu);
}
// Saved sources
if (!savedSources.isEmpty()) {
menu->addSeparator();
for (int i = 0; i < savedSources.size(); i++) {
auto* act = menu->addAction(savedSources[i].text);
act->setCheckable(true);
act->setChecked(savedSources[i].active);
act->setData(QStringLiteral("#saved:%1").arg(i));
}
menu->addSeparator();
auto* clearAct = menu->addAction(
QIcon(QStringLiteral(":/vsicons/clear-all.svg")),
QStringLiteral("Clear All"));
clearAct->setIconVisibleInMenu(true);
clearAct->setData(QStringLiteral("#clear"));
}
}

View File

@@ -7,6 +7,13 @@
// Forward declarations
namespace rcx { class Provider; }
class QWidget;
class QMenu;
// Lightweight struct for saved source display in menus
struct SavedSourceDisplay {
QString text;
bool active = false;
};
/**
* Global registry for data source providers
@@ -56,7 +63,13 @@ public:
// Clear all providers
void clear();
// Populate a QMenu with source items (File, providers with icons/dll names,
// plugin actions, saved sources). Used by both the main window Data Source
// menu and the RcxEditor inline source picker.
static void populateSourceMenu(QMenu* menu,
const QVector<SavedSourceDisplay>& savedSources = {});
private:
ProviderRegistry() = default;
QList<ProviderInfo> m_providers;

View File

@@ -16,6 +16,13 @@ struct MemoryRegion {
QString moduleName;
};
struct VtopResult {
uint64_t physical = 0;
uint64_t pml4e = 0, pdpte = 0, pde = 0, pte = 0;
uint8_t pageSize = 0; // 0=4KB, 1=2MB, 2=1GB
bool valid = false;
};
class Provider {
public:
virtual ~Provider() = default;
@@ -80,6 +87,19 @@ public:
struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; };
virtual QVector<ThreadInfo> tebs() const { return {}; }
// --- Kernel paging capabilities (override in kernel providers) ---
virtual bool hasKernelPaging() const { return false; }
virtual uint64_t getCr3() const { return 0; }
virtual VtopResult translateAddress(uint64_t va) const {
Q_UNUSED(va); return {};
}
virtual QVector<uint64_t> readPageTable(uint64_t physAddr,
int startIdx = 0,
int count = 512) const {
Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count);
return {};
}
// --- Derived convenience (non-virtual, never override) ---
bool isValid() const { return size() > 0; }