pcbjam/tests/apps/standalone/tooltip-lifetime/tooltip-lifetime_test.cpp
Viktor Vaczi 4186ea490f test(wasm-dom): repros + fixes for the text-ctrl reentry and tooltip UAF bugs
Bump wxwidgets (8814ddb) for the two DOM-port fixes and add their reproductions:

- tests/apps/standalone/{textctrl-reentry,tooltip-lifetime}: standalone wx repro
  apps + Makefile.wasm targets (textctrl links -fexceptions to throw from a
  wxEVT_TEXT handler), driven by tests/e2e/dom-port-bugs.spec.ts. Each app is
  deterministic and self-contained (no UB, ASAN, or timing dependence).
- docs/features/wx-dom-port/branch-review.md: branch review with findings #2/#3
  marked fixed and a "Bug reproductions and fixes" section, including the
  asyncify + legacy-EH gotcha (catch/destructor landing pads are unreliable
  while unwinding through asyncify frames).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:39:34 +02:00

105 lines
3.3 KiB
C++

// wxToolTip hover-window lifetime reproduction (DOM port).
//
// Bug (src/wasm/tooltip.cpp):
//
// wxWindow *gs_hoverWindow = NULL; // raw pointer, set on hover
// ... wxWasmTooltipTimer::Notify() {
// wxWindow *win = FindTooltipWindow(gs_hoverWindow); // 600 ms later:
// ... // win->GetParent()/
// } // GetToolTip()/...
//
// Nothing clears gs_hoverWindow when the hovered window is destroyed, so a
// window destroyed within the 600 ms tooltip delay leaves gs_hoverWindow
// dangling -> use-after-free when the timer fires.
//
// ASAN can't catch this here (the read lives in the wx library, which is not
// instrumented), so the repro checks the invariant the bug violates directly:
// it arms the hover for a window (the same call wxApp::HandleMouseEvent makes on
// hover-in), destroys that window, and asks — via a diagnostic accessor — whether
// the hovered-window pointer was cleared.
//
// RED (bug present): gs_hoverWindow still points at the freed window.
// GREEN (fixed): gs_hoverWindow was cleared on destruction.
#include "wx/wxprec.h"
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#include <cstdint>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
// Hooks defined in src/wasm/tooltip.cpp.
extern void wxWasmTooltipOnHoverChange(wxWindow *win);
extern wxWindow *wxWasmTooltipDebugHoverWindow();
static void Report(const char *name, bool pass, const wxString &detail)
{
#ifdef __EMSCRIPTEN__
EM_ASM({
var msg = '[REPRO] ' + UTF8ToString($0) + ': ' + ($1 ? 'PASS' : 'FAIL')
+ ' - ' + UTF8ToString($2);
if ($1) { console.log(msg); } else { console.error(msg); }
}, name, pass ? 1 : 0, (const char *)detail.utf8_str());
#endif
}
class ReproFrame : public wxFrame
{
public:
ReproFrame();
private:
void RunTest();
};
ReproFrame::ReproFrame()
: wxFrame(nullptr, wxID_ANY, "wxToolTip lifetime repro")
{
CallAfter(&ReproFrame::RunTest);
}
void ReproFrame::RunTest()
{
wxWindow *victim = new wxPanel(this, wxID_ANY,
wxDefaultPosition, wxSize(120, 60));
victim->SetToolTip("VICTIM_TOOLTIP");
// Arm the hover exactly like wxApp::HandleMouseEvent does on hover-in:
// gs_hoverWindow = victim, and the 600 ms tooltip timer starts.
wxWasmTooltipOnHoverChange(victim);
const bool armed = (wxWasmTooltipDebugHoverWindow() == victim);
const uintptr_t victimAddr = reinterpret_cast<uintptr_t>(victim);
// Destroy the hovered window while the tooltip timer is still pending.
delete victim;
// Invariant: the hovered-window pointer must not outlive its window.
wxWindow *hover = wxWasmTooltipDebugHoverWindow();
const bool cleared = (hover == nullptr);
const bool pass = armed && cleared;
Report("tooltip_hover_window_cleared_on_destroy", pass,
wxString::Format("armed=%d hover=%p victim=0x%lx",
armed ? 1 : 0, (void *)hover,
static_cast<unsigned long>(victimAddr)));
}
class ReproApp : public wxApp
{
public:
bool OnInit() override
{
if (!wxApp::OnInit())
return false;
(new ReproFrame())->Show(true);
return true;
}
};
wxIMPLEMENT_APP(ReproApp);