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>
This commit is contained in:
parent
fa408c0c06
commit
4186ea490f
6 changed files with 542 additions and 2 deletions
|
|
@ -180,7 +180,9 @@ all: minimal_test.html \
|
|||
$(S)/coroutine-pthread/main_repro.html \
|
||||
$(S)/coroutine-pthread/mainloop_repro.html \
|
||||
$(S)/coroutine-pthread/nested_repro_ex.html \
|
||||
$(S)/coroutine-pthread/vcall_repro.html
|
||||
$(S)/coroutine-pthread/vcall_repro.html \
|
||||
$(S)/textctrl-reentry/textctrl-reentry_test.html \
|
||||
$(S)/tooltip-lifetime/tooltip-lifetime_test.html
|
||||
|
||||
# Main test app
|
||||
minimal_test.o: minimal_test.cpp
|
||||
|
|
@ -189,6 +191,27 @@ minimal_test.o: minimal_test.cpp
|
|||
minimal_test.html: minimal_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
# --- DOM-port bug reproductions (docs/features/wx-dom-port/branch-review.md) ---
|
||||
|
||||
# wxTextCtrl reentry-guard repro. Needs -fexceptions: it throws from a wxEVT_TEXT
|
||||
# handler to prove the OnDomEvent m_inDomInput reset must be exception-safe.
|
||||
$(S)/textctrl-reentry/textctrl-reentry_test.o: $(S)/textctrl-reentry/textctrl-reentry_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -fexceptions $< -o $@
|
||||
|
||||
$(S)/textctrl-reentry/textctrl-reentry_test.html: $(S)/textctrl-reentry/textctrl-reentry_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) -fexceptions --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
textctrl-reentry: $(S)/textctrl-reentry/textctrl-reentry_test.html
|
||||
|
||||
# wxToolTip hover-window lifetime repro (relies on wxWasmTooltipDebugHoverWindow).
|
||||
$(S)/tooltip-lifetime/tooltip-lifetime_test.o: $(S)/tooltip-lifetime/tooltip-lifetime_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
||||
$(S)/tooltip-lifetime/tooltip-lifetime_test.html: $(S)/tooltip-lifetime/tooltip-lifetime_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
tooltip-lifetime: $(S)/tooltip-lifetime/tooltip-lifetime_test.html
|
||||
|
||||
# Menu test (no GL)
|
||||
$(S)/menu/menu_test.o: $(S)/menu/menu_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
|
|
|||
104
tests/apps/standalone/textctrl-reentry/textctrl-reentry_test.cpp
Normal file
104
tests/apps/standalone/textctrl-reentry/textctrl-reentry_test.cpp
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// wxTextCtrl DOM-sync reentry-guard reproduction (DOM port) — real-path version.
|
||||
//
|
||||
// Bug (src/wasm/textctrl.cpp, OnDomEvent / wxDOM_EVENT_INPUT):
|
||||
//
|
||||
// m_inDomInput = true;
|
||||
// const wxString value = wxDomGetValue(WasmGetDomId());
|
||||
// DoSetValue(value, SetValue_SendEvent); // fires wxEVT_TEXT
|
||||
// m_inDomInput = false; // <-- skipped if a handler throws
|
||||
//
|
||||
// DoSetValue()/WriteText() only push the value into the <input> element
|
||||
// `if (!m_inDomInput)`. If a wxEVT_TEXT handler exits non-locally (throws), the
|
||||
// reset is skipped, m_inDomInput stays true, and every later programmatic
|
||||
// SetValue()/ChangeValue() silently stops updating the visible element.
|
||||
//
|
||||
// This repro uses the REAL delivery path (no synthetic OnDomEvent call, no
|
||||
// app-level try/catch that could change unwinding): the spec types into the
|
||||
// <input>, which fires a genuine DOM 'input' event -> wx-dom.js dispatch() ->
|
||||
// wx_dom_event() (extern "C") -> OnDomEvent. The bound wxEVT_TEXT handler throws
|
||||
// once; the throw escapes OnDomEvent and is caught by dispatch()'s try/catch at
|
||||
// the JS boundary (exactly as a real handler exception would be). The spec then
|
||||
// clicks a button that does a programmatic ChangeValue() and checks the element.
|
||||
//
|
||||
// RED (bug present): the <input> keeps the typed text; ChangeValue is dropped.
|
||||
// GREEN (fixed): the <input> shows the programmatic value.
|
||||
|
||||
#include "wx/wxprec.h"
|
||||
#ifndef WX_PRECOMP
|
||||
#include "wx/wx.h"
|
||||
#endif
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
enum { ID_SET_PROGRAMMATIC = wxID_HIGHEST + 1 };
|
||||
|
||||
class ReproFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
ReproFrame();
|
||||
|
||||
private:
|
||||
void OnText(wxCommandEvent &evt);
|
||||
void OnSetProgrammatic(wxCommandEvent &evt);
|
||||
|
||||
wxTextCtrl *m_text = nullptr;
|
||||
bool m_throwArmed = true;
|
||||
};
|
||||
|
||||
ReproFrame::ReproFrame()
|
||||
: wxFrame(nullptr, wxID_ANY, "wxTextCtrl reentry repro")
|
||||
{
|
||||
wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
m_text = new wxTextCtrl(this, wxID_ANY, "");
|
||||
m_text->Bind(wxEVT_TEXT, &ReproFrame::OnText, this);
|
||||
sizer->Add(m_text, 0, wxALL, 10);
|
||||
|
||||
wxButton *button = new wxButton(this, ID_SET_PROGRAMMATIC, "Set Programmatic");
|
||||
button->Bind(wxEVT_BUTTON, &ReproFrame::OnSetProgrammatic, this);
|
||||
sizer->Add(button, 0, wxALL, 10);
|
||||
|
||||
SetSizer(sizer);
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
CallAfter([] { EM_ASM({ console.log('[REPRO] textctrl ready'); }); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void ReproFrame::OnText(wxCommandEvent &evt)
|
||||
{
|
||||
evt.Skip();
|
||||
|
||||
// A handler that throws (a validator failure, a wxLogError turned into an
|
||||
// exception by a custom log target, ...). Throw once so the app survives.
|
||||
if (m_throwArmed)
|
||||
{
|
||||
m_throwArmed = false;
|
||||
throw std::runtime_error("repro: wxEVT_TEXT handler throws");
|
||||
}
|
||||
}
|
||||
|
||||
void ReproFrame::OnSetProgrammatic(wxCommandEvent &WXUNUSED(evt))
|
||||
{
|
||||
// Must reach the <input> element even after a prior wxEVT_TEXT handler threw.
|
||||
m_text->ChangeValue("PROGRAMMATIC_OK");
|
||||
}
|
||||
|
||||
class ReproApp : public wxApp
|
||||
{
|
||||
public:
|
||||
bool OnInit() override
|
||||
{
|
||||
if (!wxApp::OnInit())
|
||||
return false;
|
||||
|
||||
(new ReproFrame())->Show(true);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP(ReproApp);
|
||||
105
tests/apps/standalone/tooltip-lifetime/tooltip-lifetime_test.cpp
Normal file
105
tests/apps/standalone/tooltip-lifetime/tooltip-lifetime_test.cpp
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// 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);
|
||||
Loading…
Reference in a new issue