fix(console): audit fixes — HiDPI, fold UX, stop-confirm, RPC-trace UAF
From the console-tab audit: - HiDPI: the "New output" pill (140x24 + margin/rounding/border) and the JSON indent-guide thickness were hand-drawn in raw px; scale them by dpiScale(). - Fold triangle: size it from the DPI/density-scaled gutter width (not the zoomed font) and center it in the gutter band so glyph and clickable cell stay aligned; only draw/handle it in the unfiltered view (folding_active_) — in a filtered flat view a click silently flipped the collapsed flag with no visible effect and the glyph disagreed with the rendered block. - Selection: ignore left-clicks in the gutter (< output_origin_.x) so toggling a fold no longer clears the user's active text selection. - 'stop' confirmation: promote the function-local static to a member and reset it in clear(), so a toolbar/context-menu clear between the two 'stop's can't leave a stale arm that skips the shutdown warning. - RPC-trace callback: dereference the console pointer under the mutex (not after releasing it) so ~ConsoleTab can't destroy the object mid-call — closing a latent use-after-free on shutdown (fires on RPC worker threads). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -241,12 +241,11 @@ ConsoleTab::ConsoleTab()
|
|||||||
s_rpc_trace_console = this;
|
s_rpc_trace_console = this;
|
||||||
}
|
}
|
||||||
rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) {
|
rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) {
|
||||||
ConsoleTab* console = nullptr;
|
// Dereference under the lock, not after releasing it: ~ConsoleTab clears
|
||||||
{
|
// s_rpc_trace_console under the same lock, so holding it here blocks destruction until the
|
||||||
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
|
// call returns — closing the shutdown use-after-free window (this fires on RPC worker threads).
|
||||||
console = s_rpc_trace_console;
|
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
|
||||||
}
|
if (s_rpc_trace_console) s_rpc_trace_console->addRpcTraceLine(source, method);
|
||||||
if (console) console->addRpcTraceLine(source, method);
|
|
||||||
});
|
});
|
||||||
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
|
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
|
||||||
|
|
||||||
@@ -848,6 +847,10 @@ void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLow
|
|||||||
bool has_filter = hasTextFilter || !outputFilter.daemonMessagesEnabled ||
|
bool has_filter = hasTextFilter || !outputFilter.daemonMessagesEnabled ||
|
||||||
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
|
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
|
||||||
outputFilter.errorsOnly;
|
outputFilter.errorsOnly;
|
||||||
|
// Folding is only honored in the unfiltered view; the filtered view is flat. Record it so the
|
||||||
|
// fold triangles aren't drawn/clickable while filtering (else clicks would silently mutate the
|
||||||
|
// collapsed flag with no visible effect and the glyph would disagree with the rendered block).
|
||||||
|
folding_active_ = !has_filter;
|
||||||
visible_indices_.clear();
|
visible_indices_.clear();
|
||||||
const int n = static_cast<int>(model_.size());
|
const int n = static_cast<int>(model_.size());
|
||||||
if (has_filter) {
|
if (has_filter) {
|
||||||
@@ -883,8 +886,10 @@ void ConsoleTab::handleOutputInteraction(ImVec2 mousePos, bool mouseInOutput)
|
|||||||
if (mouseInOutput) {
|
if (mouseInOutput) {
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
|
||||||
}
|
}
|
||||||
// Selection drag lifecycle (continues even if the mouse leaves the window).
|
// Selection drag lifecycle (continues even if the mouse leaves the window). Ignore clicks in the
|
||||||
if (mouseInOutput && io.MouseClicked[0]) {
|
// left gutter (< output_origin_.x) — that strip holds the accent bar + fold triangles, so a fold
|
||||||
|
// toggle there shouldn't begin (and thus clear) a text selection.
|
||||||
|
if (mouseInOutput && io.MouseClicked[0] && mousePos.x >= output_origin_.x) {
|
||||||
selection_.beginDrag(screenToTextPos(mousePos));
|
selection_.beginDrag(screenToTextPos(mousePos));
|
||||||
}
|
}
|
||||||
if (selection_.dragging() && io.MouseDown[0]) {
|
if (selection_.dragging() && io.MouseDown[0]) {
|
||||||
@@ -995,16 +1000,19 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
|
|||||||
for (size_t c = 2; c < leading; c += 2) {
|
for (size_t c = 2; c < leading; c += 2) {
|
||||||
float gx = lineOrigin.x + static_cast<float>(c) * spaceW;
|
float gx = lineOrigin.x + static_cast<float>(c) * spaceW;
|
||||||
dl->AddLine(ImVec2(gx, lineOrigin.y), ImVec2(gx, lineOrigin.y + totalH),
|
dl->AddLine(ImVec2(gx, lineOrigin.y), ImVec2(gx, lineOrigin.y + totalH),
|
||||||
IM_COL32(255, 255, 255, 20), 1.0f);
|
IM_COL32(255, 255, 255, 20), 1.0f * Layout::dpiScale());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON fold toggle — a small triangle in the left gutter for block-opener lines.
|
// JSON fold toggle — a small triangle in the left gutter for block-opener lines.
|
||||||
// Openers carry no accent bar (result/JSON channels), so the gutter is free.
|
// Openers carry no accent bar (result/JSON channels), so the gutter is free. Only shown in the
|
||||||
if (line.foldSpan > 0) {
|
// unfiltered view, where folding actually applies (see folding_active_ in computeVisibleLines).
|
||||||
float sz = fontSize * 0.30f;
|
if (folding_active_ && line.foldSpan > 0) {
|
||||||
float cx = output_origin_.x - padX + sz + 1.0f * Layout::hScale();
|
// Size from the (DPI/density-scaled) gutter width, not the zoomed font, and center it in
|
||||||
|
// the gutter band [origin-padX, origin) so the glyph and its clickable cell stay aligned.
|
||||||
|
float sz = std::min(fontSize * 0.30f, padX * 0.34f);
|
||||||
|
float cx = output_origin_.x - padX * 0.5f;
|
||||||
float cy = lineOrigin.y + lineHeight * 0.5f;
|
float cy = lineOrigin.y + lineHeight * 0.5f;
|
||||||
ImU32 triCol = WithAlpha(OnSurfaceMedium(), 210);
|
ImU32 triCol = WithAlpha(OnSurfaceMedium(), 210);
|
||||||
if (line.collapsed) {
|
if (line.collapsed) {
|
||||||
@@ -1174,18 +1182,21 @@ void ConsoleTab::drawNewOutputIndicator()
|
|||||||
// "New output" indicator when the user is scrolled up and new lines arrived.
|
// "New output" indicator when the user is scrolled up and new lines arrived.
|
||||||
if (scroll_.autoScroll() || scroll_.newLines() <= 0) return;
|
if (scroll_.autoScroll() || scroll_.newLines() <= 0) return;
|
||||||
|
|
||||||
float indicW = 140.0f;
|
// The box geometry is hand-drawn absolute px, so scale it by dpiScale() (the font metrics below
|
||||||
float indicH = 24.0f;
|
// are already scaled — left alone). Without this the pill renders native-size on a HiDPI display.
|
||||||
|
float dp = Layout::dpiScale();
|
||||||
|
float indicW = 140.0f * dp;
|
||||||
|
float indicH = 24.0f * dp;
|
||||||
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
ImDrawList* dlInd = ImGui::GetWindowDrawList();
|
||||||
ImVec2 wMin = ImGui::GetWindowPos();
|
ImVec2 wMin = ImGui::GetWindowPos();
|
||||||
ImVec2 wSize = ImGui::GetWindowSize();
|
ImVec2 wSize = ImGui::GetWindowSize();
|
||||||
float ix = wMin.x + (wSize.x - indicW) * 0.5f;
|
float ix = wMin.x + (wSize.x - indicW) * 0.5f;
|
||||||
float iy = wMin.y + wSize.y - indicH - 8.0f;
|
float iy = wMin.y + wSize.y - indicH - 8.0f * dp;
|
||||||
ImVec2 iMin(ix, iy);
|
ImVec2 iMin(ix, iy);
|
||||||
ImVec2 iMax(ix + indicW, iy + indicH);
|
ImVec2 iMax(ix + indicW, iy + indicH);
|
||||||
|
|
||||||
dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f);
|
dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f * dp);
|
||||||
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
|
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f * dp, 0, 1.0f * dp);
|
||||||
|
|
||||||
char buf[48];
|
char buf[48];
|
||||||
snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines());
|
snprintf(buf, sizeof(buf), TR("console_new_lines"), scroll_.newLines());
|
||||||
@@ -1391,9 +1402,10 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
|||||||
std::transform(first.begin(), first.end(), first.begin(),
|
std::transform(first.begin(), first.end(), first.begin(),
|
||||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
}
|
}
|
||||||
// 'stop' shuts down the node — require a confirming second 'stop'; any other command clears the pending state.
|
// 'stop' shuts down the node — require a confirming second 'stop'; any other command clears the
|
||||||
static bool stopConfirmPending = false;
|
// pending state. Member (not a function-local static) so clear() can also cancel it — otherwise a
|
||||||
if (first != "stop") stopConfirmPending = false;
|
// toolbar/context-menu clear between the two 'stop's would leave a stale arm and skip the warning.
|
||||||
|
if (first != "stop") stop_confirm_pending_ = false;
|
||||||
auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); };
|
auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); };
|
||||||
if (first == "clear" || first == "cls") {
|
if (first == "clear" || first == "cls") {
|
||||||
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
|
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
|
||||||
@@ -1404,12 +1416,12 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
|||||||
} else if (first == "quit" || first == "exit") {
|
} else if (first == "quit" || first == "exit") {
|
||||||
addLine(TR("console_quit_note"), ConsoleChannel::Info);
|
addLine(TR("console_quit_note"), ConsoleChannel::Info);
|
||||||
} else if (first == "stop") {
|
} else if (first == "stop") {
|
||||||
if (!stopConfirmPending) {
|
if (!stop_confirm_pending_) {
|
||||||
stopConfirmPending = true;
|
stop_confirm_pending_ = true;
|
||||||
addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.",
|
addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.",
|
||||||
ConsoleChannel::Warning);
|
ConsoleChannel::Warning);
|
||||||
} else {
|
} else {
|
||||||
stopConfirmPending = false;
|
stop_confirm_pending_ = false;
|
||||||
if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
||||||
else exec.submit(cmd);
|
else exec.submit(cmd);
|
||||||
}
|
}
|
||||||
@@ -1688,6 +1700,7 @@ void ConsoleTab::clear()
|
|||||||
// View-only clear (main thread). The executor keeps its own log cursors, so new output
|
// View-only clear (main thread). The executor keeps its own log cursors, so new output
|
||||||
// still appends. The "cleared" line is ingested and appears on the next frame's drain.
|
// still appends. The "cleared" line is ingested and appears on the next frame's drain.
|
||||||
model_.clear();
|
model_.clear();
|
||||||
|
stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing
|
||||||
addLine(TR("console_cleared"), ConsoleChannel::Info);
|
addLine(TR("console_cleared"), ConsoleChannel::Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ private:
|
|||||||
std::vector<std::string> command_history_;
|
std::vector<std::string> command_history_;
|
||||||
int history_index_ = -1;
|
int history_index_ = -1;
|
||||||
char input_buffer_[4096] = {0};
|
char input_buffer_[4096] = {0};
|
||||||
|
bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop'
|
||||||
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
||||||
|
|
||||||
// Auto-scroll state machine (pin-to-bottom, wheel-up cooldown, new-line backlog count).
|
// Auto-scroll state machine (pin-to-bottom, wheel-up cooldown, new-line backlog count).
|
||||||
@@ -173,6 +174,7 @@ private:
|
|||||||
mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar)
|
mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar)
|
||||||
std::string context_token_; // hash/address under the cursor at right-click
|
std::string context_token_; // hash/address under the cursor at right-click
|
||||||
mutable std::vector<int> visible_indices_; // Cached for selection mapping
|
mutable std::vector<int> visible_indices_; // Cached for selection mapping
|
||||||
|
bool folding_active_ = true; // fold UI shown only in the unfiltered (foldable) view
|
||||||
|
|
||||||
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y),
|
// Wrap layout for the visible lines (segments + per-line heights + cumulative Y),
|
||||||
// recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and
|
// recomputed each frame by the pure BuildConsoleLayout (console_text_layout.h) and
|
||||||
|
|||||||
Reference in New Issue
Block a user